speed up a logical replica setup

Started by Euler Taveiraalmost 4 years ago327 messages
#1Euler Taveira
euler@eulerto.com
2 attachment(s)

Hi,

Logical replication has been used to migration with minimal downtime. However,
if you are dealing with a big database, the amount of required resources (disk
-- due to WAL retention) increases as the backlog (WAL) increases. Unless you
have a generous amount of resources and can wait for long period of time until
the new replica catches up, creating a logical replica is impracticable on
large databases.

The general idea is to create and convert a physical replica or a base backup
(archived WAL files available) into a logical replica. The initial data copy
and catchup tends to be faster on a physical replica. This technique has been
successfully used in pglogical_create_subscriber [1]https://github.com/2ndQuadrant/pglogical.

A new tool called pg_subscriber does this conversion and is tightly integrated
with Postgres.

DESIGN

The conversion requires 8 steps.

1. Check if the target data directory has the same system identifier than the
source data directory.
2. Stop the target server if it is running as a standby server. (Modify
recovery parameters requires a restart.)
3. Create one replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent LSN
(This consistent LSN will be used as (a) a stopping point for the recovery
process and (b) a starting point for the subscriptions).
4. Write recovery parameters into the target data directory and start the
target server (Wait until the target server is promoted).
5. Create one publication (FOR ALL TABLES) per specified database on the source
server.
6. Create one subscription per specified database on the target server (Use
replication slot and publication created in a previous step. Don't enable the
subscriptions yet).
7. Sets the replication progress to the consistent LSN that was got in a
previous step.
8. Enable the subscription for each specified database on the target server.

This tool does not take a base backup. It can certainly be included later.
There is already a tool do it: pg_basebackup.

There is a --subscriber-conninfo option to inform the subscriber connection
string, however, we could remove it since this tool runs on the subscriber and
we can build a connection string.

NAME

I'm not sure about the proposed name. I came up with this one because it is not
so long. The last added tools uses pg_ prefix, verb (action) and object.
pg_initsubscriber and pg_createsubscriber are names that I thought but I'm not
excited about it.

DOCUMENTATION

It is available and describes this tool.

TESTS

Basic tests are included. It requires some tests to exercise this tool.

Comments?

[1]: https://github.com/2ndQuadrant/pglogical

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v1-0001-Move-readfile-and-free_readfile-to-file_utils.h.patchtext/x-patch; name=v1-0001-Move-readfile-and-free_readfile-to-file_utils.h.patchDownload
From 5827245b8a06f906f603100c8fb27be533a6c0a1 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Fri, 11 Feb 2022 03:05:58 -0300
Subject: [PATCH v1 1/2] Move readfile() and free_readfile() to file_utils.h

Allow these functions to be used by other binaries.

There is a static function called readfile() in initdb.c too. Rename it
to avoid conflicting with the exposed function.
---
 src/bin/initdb/initdb.c         |  26 +++----
 src/bin/pg_ctl/pg_ctl.c         | 122 +-------------------------------
 src/common/file_utils.c         | 119 +++++++++++++++++++++++++++++++
 src/include/common/file_utils.h |   3 +
 4 files changed, 136 insertions(+), 134 deletions(-)

diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 97f15971e2..a4cbfeb954 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -243,8 +243,8 @@ static char **replace_token(char **lines,
 #ifndef HAVE_UNIX_SOCKETS
 static char **filter_lines_with_token(char **lines, const char *token);
 #endif
-static char **readfile(const char *path);
-static void writefile(char *path, char **lines);
+static char **read_text_file(const char *path);
+static void write_text_file(char *path, char **lines);
 static FILE *popen_check(const char *command, const char *mode);
 static char *get_id(void);
 static int	get_encoding_id(const char *encoding_name);
@@ -453,7 +453,7 @@ filter_lines_with_token(char **lines, const char *token)
  * get the lines from a text file
  */
 static char **
-readfile(const char *path)
+read_text_file(const char *path)
 {
 	char	  **result;
 	FILE	   *infile;
@@ -500,7 +500,7 @@ readfile(const char *path)
  * so that the resulting configuration files are nicely editable on Windows.
  */
 static void
-writefile(char *path, char **lines)
+write_text_file(char *path, char **lines)
 {
 	FILE	   *out_file;
 	char	  **line;
@@ -1063,7 +1063,7 @@ setup_config(void)
 
 	/* postgresql.conf */
 
-	conflines = readfile(conf_file);
+	conflines = read_text_file(conf_file);
 
 	snprintf(repltok, sizeof(repltok), "max_connections = %d", n_connections);
 	conflines = replace_token(conflines, "#max_connections = 100", repltok);
@@ -1214,7 +1214,7 @@ setup_config(void)
 
 	snprintf(path, sizeof(path), "%s/postgresql.conf", pg_data);
 
-	writefile(path, conflines);
+	write_text_file(path, conflines);
 	if (chmod(path, pg_file_create_mode) != 0)
 	{
 		pg_log_error("could not change permissions of \"%s\": %m", path);
@@ -1233,7 +1233,7 @@ setup_config(void)
 
 	sprintf(path, "%s/postgresql.auto.conf", pg_data);
 
-	writefile(path, autoconflines);
+	write_text_file(path, autoconflines);
 	if (chmod(path, pg_file_create_mode) != 0)
 	{
 		pg_log_error("could not change permissions of \"%s\": %m", path);
@@ -1245,7 +1245,7 @@ setup_config(void)
 
 	/* pg_hba.conf */
 
-	conflines = readfile(hba_file);
+	conflines = read_text_file(hba_file);
 
 #ifndef HAVE_UNIX_SOCKETS
 	conflines = filter_lines_with_token(conflines, "@remove-line-for-nolocal@");
@@ -1319,7 +1319,7 @@ setup_config(void)
 
 	snprintf(path, sizeof(path), "%s/pg_hba.conf", pg_data);
 
-	writefile(path, conflines);
+	write_text_file(path, conflines);
 	if (chmod(path, pg_file_create_mode) != 0)
 	{
 		pg_log_error("could not change permissions of \"%s\": %m", path);
@@ -1330,11 +1330,11 @@ setup_config(void)
 
 	/* pg_ident.conf */
 
-	conflines = readfile(ident_file);
+	conflines = read_text_file(ident_file);
 
 	snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
 
-	writefile(path, conflines);
+	write_text_file(path, conflines);
 	if (chmod(path, pg_file_create_mode) != 0)
 	{
 		pg_log_error("could not change permissions of \"%s\": %m", path);
@@ -1362,7 +1362,7 @@ bootstrap_template1(void)
 	printf(_("running bootstrap script ... "));
 	fflush(stdout);
 
-	bki_lines = readfile(bki_file);
+	bki_lines = read_text_file(bki_file);
 
 	/* Check that bki file appears to be of the right version */
 
@@ -1547,7 +1547,7 @@ setup_run_file(FILE *cmdfd, const char *filename)
 {
 	char	  **lines;
 
-	lines = readfile(filename);
+	lines = read_text_file(filename);
 
 	for (char **line = lines; *line != NULL; line++)
 	{
diff --git a/src/bin/pg_ctl/pg_ctl.c b/src/bin/pg_ctl/pg_ctl.c
index 3c182c97d4..b87beb7380 100644
--- a/src/bin/pg_ctl/pg_ctl.c
+++ b/src/bin/pg_ctl/pg_ctl.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_control.h"
 #include "common/controldata_utils.h"
 #include "common/file_perm.h"
+#include "common/file_utils.h"
 #include "common/logging.h"
 #include "common/string.h"
 #include "getopt_long.h"
@@ -150,8 +151,6 @@ static PTOKEN_PRIVILEGES GetPrivilegesToDelete(HANDLE hToken);
 #endif
 
 static pgpid_t get_pgpid(bool is_status_request);
-static char **readfile(const char *path, int *numlines);
-static void free_readfile(char **optlines);
 static pgpid_t start_postmaster(void);
 static void read_post_opts(void);
 
@@ -307,125 +306,6 @@ get_pgpid(bool is_status_request)
 }
 
 
-/*
- * get the lines from a text file - return NULL if file can't be opened
- *
- * Trailing newlines are deleted from the lines (this is a change from pre-v10)
- *
- * *numlines is set to the number of line pointers returned; there is
- * also an additional NULL pointer after the last real line.
- */
-static char **
-readfile(const char *path, int *numlines)
-{
-	int			fd;
-	int			nlines;
-	char	  **result;
-	char	   *buffer;
-	char	   *linebegin;
-	int			i;
-	int			n;
-	int			len;
-	struct stat statbuf;
-
-	*numlines = 0;				/* in case of failure or empty file */
-
-	/*
-	 * Slurp the file into memory.
-	 *
-	 * The file can change concurrently, so we read the whole file into memory
-	 * with a single read() call. That's not guaranteed to get an atomic
-	 * snapshot, but in practice, for a small file, it's close enough for the
-	 * current use.
-	 */
-	fd = open(path, O_RDONLY | PG_BINARY, 0);
-	if (fd < 0)
-		return NULL;
-	if (fstat(fd, &statbuf) < 0)
-	{
-		close(fd);
-		return NULL;
-	}
-	if (statbuf.st_size == 0)
-	{
-		/* empty file */
-		close(fd);
-		result = (char **) pg_malloc(sizeof(char *));
-		*result = NULL;
-		return result;
-	}
-	buffer = pg_malloc(statbuf.st_size + 1);
-
-	len = read(fd, buffer, statbuf.st_size + 1);
-	close(fd);
-	if (len != statbuf.st_size)
-	{
-		/* oops, the file size changed between fstat and read */
-		free(buffer);
-		return NULL;
-	}
-
-	/*
-	 * Count newlines. We expect there to be a newline after each full line,
-	 * including one at the end of file. If there isn't a newline at the end,
-	 * any characters after the last newline will be ignored.
-	 */
-	nlines = 0;
-	for (i = 0; i < len; i++)
-	{
-		if (buffer[i] == '\n')
-			nlines++;
-	}
-
-	/* set up the result buffer */
-	result = (char **) pg_malloc((nlines + 1) * sizeof(char *));
-	*numlines = nlines;
-
-	/* now split the buffer into lines */
-	linebegin = buffer;
-	n = 0;
-	for (i = 0; i < len; i++)
-	{
-		if (buffer[i] == '\n')
-		{
-			int			slen = &buffer[i] - linebegin;
-			char	   *linebuf = pg_malloc(slen + 1);
-
-			memcpy(linebuf, linebegin, slen);
-			/* we already dropped the \n, but get rid of any \r too */
-			if (slen > 0 && linebuf[slen - 1] == '\r')
-				slen--;
-			linebuf[slen] = '\0';
-			result[n++] = linebuf;
-			linebegin = &buffer[i + 1];
-		}
-	}
-	result[n] = NULL;
-
-	free(buffer);
-
-	return result;
-}
-
-
-/*
- * Free memory allocated for optlines through readfile()
- */
-static void
-free_readfile(char **optlines)
-{
-	char	   *curr_line = NULL;
-	int			i = 0;
-
-	if (!optlines)
-		return;
-
-	while ((curr_line = optlines[i++]))
-		free(curr_line);
-
-	free(optlines);
-}
-
 /*
  * start/test/stop routines
  */
diff --git a/src/common/file_utils.c b/src/common/file_utils.c
index 7138068633..1f53f088c6 100644
--- a/src/common/file_utils.c
+++ b/src/common/file_utils.c
@@ -398,6 +398,125 @@ durable_rename(const char *oldfile, const char *newfile)
 	return 0;
 }
 
+/*
+ * get the lines from a text file - return NULL if file can't be opened
+ *
+ * Trailing newlines are deleted from the lines (this is a change from pre-v10)
+ *
+ * *numlines is set to the number of line pointers returned; there is
+ * also an additional NULL pointer after the last real line.
+ */
+char **
+readfile(const char *path, int *numlines)
+{
+	int			fd;
+	int			nlines;
+	char	  **result;
+	char	   *buffer;
+	char	   *linebegin;
+	int			i;
+	int			n;
+	int			len;
+	struct stat statbuf;
+
+	*numlines = 0;				/* in case of failure or empty file */
+
+	/*
+	 * Slurp the file into memory.
+	 *
+	 * The file can change concurrently, so we read the whole file into memory
+	 * with a single read() call. That's not guaranteed to get an atomic
+	 * snapshot, but in practice, for a small file, it's close enough for the
+	 * current use.
+	 */
+	fd = open(path, O_RDONLY | PG_BINARY, 0);
+	if (fd < 0)
+		return NULL;
+	if (fstat(fd, &statbuf) < 0)
+	{
+		close(fd);
+		return NULL;
+	}
+	if (statbuf.st_size == 0)
+	{
+		/* empty file */
+		close(fd);
+		result = (char **) pg_malloc(sizeof(char *));
+		*result = NULL;
+		return result;
+	}
+	buffer = pg_malloc(statbuf.st_size + 1);
+
+	len = read(fd, buffer, statbuf.st_size + 1);
+	close(fd);
+	if (len != statbuf.st_size)
+	{
+		/* oops, the file size changed between fstat and read */
+		free(buffer);
+		return NULL;
+	}
+
+	/*
+	 * Count newlines. We expect there to be a newline after each full line,
+	 * including one at the end of file. If there isn't a newline at the end,
+	 * any characters after the last newline will be ignored.
+	 */
+	nlines = 0;
+	for (i = 0; i < len; i++)
+	{
+		if (buffer[i] == '\n')
+			nlines++;
+	}
+
+	/* set up the result buffer */
+	result = (char **) pg_malloc((nlines + 1) * sizeof(char *));
+	*numlines = nlines;
+
+	/* now split the buffer into lines */
+	linebegin = buffer;
+	n = 0;
+	for (i = 0; i < len; i++)
+	{
+		if (buffer[i] == '\n')
+		{
+			int			slen = &buffer[i] - linebegin;
+			char	   *linebuf = pg_malloc(slen + 1);
+
+			memcpy(linebuf, linebegin, slen);
+			/* we already dropped the \n, but get rid of any \r too */
+			if (slen > 0 && linebuf[slen - 1] == '\r')
+				slen--;
+			linebuf[slen] = '\0';
+			result[n++] = linebuf;
+			linebegin = &buffer[i + 1];
+		}
+	}
+	result[n] = NULL;
+
+	free(buffer);
+
+	return result;
+}
+
+
+/*
+ * Free memory allocated for optlines through readfile()
+ */
+void
+free_readfile(char **optlines)
+{
+	char	   *curr_line = NULL;
+	int			i = 0;
+
+	if (!optlines)
+		return;
+
+	while ((curr_line = optlines[i++]))
+		free(curr_line);
+
+	free(optlines);
+}
+
 #endif							/* FRONTEND */
 
 /*
diff --git a/src/include/common/file_utils.h b/src/include/common/file_utils.h
index 2811744c12..27736e3dd7 100644
--- a/src/include/common/file_utils.h
+++ b/src/include/common/file_utils.h
@@ -30,6 +30,9 @@ extern void fsync_pgdata(const char *pg_data, int serverVersion);
 extern void fsync_dir_recurse(const char *dir);
 extern int	durable_rename(const char *oldfile, const char *newfile);
 extern int	fsync_parent_path(const char *fname);
+
+extern char **readfile(const char *path, int *numlines);
+extern void free_readfile(char **optlines);
 #endif
 
 extern PGFileType get_dirent_type(const char *path,
-- 
2.30.2

v1-0002-Create-a-new-logical-replica-from-a-base-backup-o.patchtext/x-patch; name=v1-0002-Create-a-new-logical-replica-from-a-base-backup-o.patchDownload
From 81788de158abbe1ffb378ffb0884b943232e51b0 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Fri, 11 Feb 2022 03:17:57 -0300
Subject: [PATCH v1 2/2] Create a new logical replica from a base backup or
 standby server.

A new tool called pg_subscriber can convert a physical replica or a base
backup into a logical replica. It runs on the target server and should
be able to connect to the source server (publisher) and the target
server (subscriber).

The conversion requires eight steps. Check if the target data directory
ha the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource contraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml        |    1 +
 doc/src/sgml/ref/pg_subscriber.sgml   |  242 ++++
 doc/src/sgml/reference.sgml           |    1 +
 src/bin/Makefile                      |    1 +
 src/bin/pg_subscriber/Makefile        |   39 +
 src/bin/pg_subscriber/pg_subscriber.c | 1463 +++++++++++++++++++++++++
 src/bin/pg_subscriber/t/001_basic.pl  |   41 +
 src/tools/msvc/Mkvcbuild.pm           |    2 +-
 8 files changed, 1789 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_subscriber.sgml
 create mode 100644 src/bin/pg_subscriber/Makefile
 create mode 100644 src/bin/pg_subscriber/pg_subscriber.c
 create mode 100644 src/bin/pg_subscriber/t/001_basic.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index d67270ccc3..eab7f2f616 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -212,6 +212,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgSubscriber       SYSTEM "pg_subscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
new file mode 100644
index 0000000000..e68a19092e
--- /dev/null
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -0,0 +1,242 @@
+<!--
+doc/src/sgml/ref/pg_subscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgsubscriber">
+ <indexterm zone="app-pgsubscriber">
+  <primary>pg_subscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_subscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>create a new logical replica from a base backup of a
+  <productname>PostgreSQL</productname> cluster or a standby
+  server</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_subscriber</application> takes the publisher and subscriber
+   connection strings, a base backup directory and a list of database names and
+   it sets up a new logical replica using the physical recovery process. A
+   standby server can also be used.
+  </para>
+
+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+
+  <para>
+   The transformation proceeds in eight steps. First,
+   <application>pg_subscriber</application> checks if the given target data
+   directory has the same system identifier than the source data directory.
+   Since it uses the recovery process as one of the steps, it starts the target
+   server as a replica from the source server. If the system identifier is not
+   the same, <application>pg_subscriber</application> will terminate with an
+   error.
+  </para>
+
+  <para>
+   Second, <application>pg_subscriber</application> checks if the target data
+   directory is used by a standby server. Stop the standby server if it is
+   running. One of the next steps is to add some recovery parameters that
+   requires a server start. This step avoids an error.
+  </para>
+
+  <para>
+   Next, <application>pg_subscriber</application> creates one replication slot
+   for each specified database on the source server. The replication slot name
+   contains a <literal>pg_subscriber</literal> prefix. These replication slots
+   will be used by the subscriptions in a future step.  Another replication
+   slot is used to get a consistent start location. This consistent LSN will be
+   used (a) as a stopping point in the <xref
+   linkend="guc-recovery-target-lsn"/> parameter and (b) by the subscriptions
+   as a replication starting point. It guarantees that no transaction will be
+   lost.
+  </para>
+
+  <para>
+   Next, write recovery parameters into the target data directory and start the
+   target server. It specifies a LSN (consistent LSN that was obtained in the
+   previous step) of write-ahead log location up to which recovery will
+   proceed. It also specifies <literal>promote</literal> as the action that the
+   server should take once the recovery target is reached. This step finishes
+   once the server ends standby mode and is accepting read-write operations.
+  </para>
+
+  <para>
+   Next, <application>pg_subscriber</application> creates one publication for
+   each specified database on the source server. Each publication replicates
+   changes for all tables in the database. The publication name contains a
+   <literal>pg_subscriber</literal> prefix. These publication will be used by a
+   corresponding subscription in a next step.
+  </para>
+
+  <para>
+   Next, <application>pg_subscriber</application> creates one subscription for
+   each specified database on the target server. Each subscription name
+   contains a <literal>pg_subscriber</literal> prefix. The replication slot
+   name is identical to the subscription name. It also does not copy existing
+   data from the source server. It does not create a replication slot. Instead,
+   it uses the replication slot that was created in a previous step. The
+   subscription is created but it is not enabled yet. The reason is the
+   replication progress must be set to the consistent LSN but replication
+   origin name contains the subscription oid in its name. Hence, the
+   subscription will be enabled in a separate step.
+  </para>
+
+  <para>
+   Next, <application>pg_subscriber</application> sets the replication progress
+   to the consistent LSN that was obtained in a previous step. When the target
+   server started the recovery process, it caught up to the consistent LSN.
+   This is the exact LSN to be used as a initial location for the logical
+   replication.
+  </para>
+
+  <para>
+   Finally, <application>pg_subscriber</application> enables the subscription
+   for each specified database on the target server. The subscription starts
+   streaming from the consistent LSN.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_subscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a base backup. It can also be a
+        cluster directory from a standby server.
+       </para>
+      </listitem>
+     </varlistentry>
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--publisher-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_subscriber</application> to output progress messages
+        and detailed information about each step.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_subscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_subscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for database <literal>bar</literal> from a base
+   backup of the server at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_basebackup -h foo -D /usr/local/pgsql/data</userinput>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d bar</userinput>
+</screen>
+  </para>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a standby server at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index da421ff24e..3566c6050c 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -256,6 +256,7 @@
    &pgReceivewal;
    &pgRecvlogical;
    &pgRestore;
+   &pgSubscriber;
    &pgVerifyBackup;
    &psqlRef;
    &reindexdb;
diff --git a/src/bin/Makefile b/src/bin/Makefile
index 7f9dde924e..6c4d3c1ffe 100644
--- a/src/bin/Makefile
+++ b/src/bin/Makefile
@@ -25,6 +25,7 @@ SUBDIRS = \
 	pg_dump \
 	pg_resetwal \
 	pg_rewind \
+	pg_subscriber \
 	pg_test_fsync \
 	pg_test_timing \
 	pg_upgrade \
diff --git a/src/bin/pg_subscriber/Makefile b/src/bin/pg_subscriber/Makefile
new file mode 100644
index 0000000000..c48dca6e49
--- /dev/null
+++ b/src/bin/pg_subscriber/Makefile
@@ -0,0 +1,39 @@
+# src/bin/pg_subscriber/Makefile
+
+PGFILEDESC = "pg_subscriber - create a new logical replica from a base backup or a standby server"
+PGAPPICON=win32
+
+subdir = src/bin/pg_subscriber
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+OBJS = \
+	$(WIN32RES) \
+	pg_subscriber.o
+
+all: pg_subscriber
+
+pg_subscriber: $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
+install: all installdirs
+	$(INSTALL_PROGRAM) pg_subscriber$(X) '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
+
+installdirs:
+	$(MKDIR_P) '$(DESTDIR)$(bindir)'
+
+uninstall:
+	rm -f '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
+
+clean distclean maintainer-clean:
+	rm -f pg_subscriber$(X) $(OBJS)
+	rm -rf tmp_check
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
diff --git a/src/bin/pg_subscriber/pg_subscriber.c b/src/bin/pg_subscriber/pg_subscriber.c
new file mode 100644
index 0000000000..7565950a08
--- /dev/null
+++ b/src/bin/pg_subscriber/pg_subscriber.c
@@ -0,0 +1,1463 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *	  Create a new logical replica from a base backup or a standby server
+ *
+ * Copyright (C) 2022, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_subscriber/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;		/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+}			LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static PGconn *connect_database(const char *conninfo, bool secure_search_path);
+static void disconnect_database(PGconn *conn);
+static char *get_sysid_from_conn(const char *conninfo);
+static char *get_control_from_datadir(const char *datadir);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 const char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static bool postmaster_is_alive(pid_t pid);
+static void wait_postmaster_connection(const char *conninfo);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+const char *progname;
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static int	verbose = 0;
+static bool	success = false;
+
+static LogicalRepInfo  *dbinfo;
+
+static int		num_dbs = 0;
+
+static char		temp_replslot[NAMEDATALEN];
+static bool		made_temp_replslot = false;
+
+char		pidfile[MAXPGPATH]; /* subscriber PID file */
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_subscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Dependind on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	*conn;
+	int		i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo, true);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo, true);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+
+	if (made_temp_replslot)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo, true);
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		disconnect_database(conn);
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a base backup or a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-conninfo=CONNINFO   publisher connection string\n"));
+	printf(_(" -S, --subscriber-conninfo=CONNINFO  subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name plus a fallback application name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	if (verbose)
+		pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	if (i > 0)
+		appendPQExpBufferChar(buf, ' ');
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	if (verbose)
+		pg_log_info("checking if directory \"%s\" is a cluster data directory",
+					datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+	appendPQExpBufferStr(buf, " replication=database");
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+static PGconn *
+connect_database(const char *conninfo, bool secure_search_path)
+{
+	PGconn	   *conn;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	if (secure_search_path)
+	{
+		PGresult   *res;
+
+		res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+			return NULL;
+		}
+		PQclear(res);
+	}
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static char *
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	char	   *repconninfo;
+	char	   *sysid = NULL;
+
+	if (verbose)
+		pg_log_info("getting system identifier from publisher");
+
+	repconninfo = psprintf("%s replication=database", conninfo);
+	conn = connect_database(repconninfo, false);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		return NULL;
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		return NULL;
+	}
+
+	sysid = pg_strdup(PQgetvalue(res, 0, 0));
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static char *
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	char	   *sysid = pg_malloc(32);
+
+	if (verbose)
+		pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	snprintf(sysid, 32, UINT64_FORMAT, cf->system_identifier);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * XXX CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	if (verbose)
+		pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+					 PQresultErrorMessage(res));
+		return lsn;
+	}
+
+	/* for cleanup purposes */
+	if (slot_name == NULL)
+		dbinfo->made_replslot = true;
+	else
+		made_temp_replslot = true;
+
+	lsn = pg_strdup(PQgetvalue(res, 0, 1));
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	if (verbose)
+		pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+					 PQerrorMessage(conn));
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			fprintf(stderr,
+					"See C include file \"ntstatus.h\" for a description of the hexadecimal value.\n");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		fprintf(stderr, "The failed command was: %s\n", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (verbose)
+	{
+		if (action)
+			pg_log_info("postmaster was started");
+		else
+			pg_log_info("postmaster was stopped");
+	}
+}
+
+/*
+ * XXX This function was copied from pg_ctl.c.
+ *
+ * We should probably move it to a common place.
+ */
+static bool
+postmaster_is_alive(pid_t pid)
+{
+	/*
+	 * Test to see if the process is still there.  Note that we do not
+	 * consider an EPERM failure to mean that the process is still there;
+	 * EPERM must mean that the given PID belongs to some other userid, and
+	 * considering the permissions on $PGDATA, that means it's not the
+	 * postmaster we are after.
+	 *
+	 * Don't believe that our own PID or parent shell's PID is the postmaster,
+	 * either.  (Windows hasn't got getppid(), though.)
+	 */
+	if (pid == getpid())
+		return false;
+#ifndef WIN32
+	if (pid == getppid())
+		return false;
+#endif
+	if (kill(pid, 0) == 0)
+		return true;
+	return false;
+}
+
+/*
+ * Returns after postmaster is accepting connections.
+ */
+static void
+wait_postmaster_connection(const char *conninfo)
+{
+	PGPing		ret;
+	long		pmpid;
+	int			status = POSTMASTER_STILL_STARTING;
+
+	if (verbose)
+		pg_log_info("waiting for the postmaster to allow connections ...");
+
+	/*
+	 * Wait postmaster to come up. XXX this code path is a modified version of
+	 * wait_for_postmaster().
+	 */
+	for (;;)
+	{
+		char	  **optlines;
+		int			numlines;
+
+		if ((optlines = readfile(pidfile, &numlines)) != NULL &&
+			numlines >= LOCK_FILE_LINE_PM_STATUS)
+		{
+			/*
+			 * Check the status line (this assumes a v10 or later server).
+			 */
+			char	   *pmstatus = optlines[LOCK_FILE_LINE_PM_STATUS - 1];
+
+			pmpid = atol(optlines[LOCK_FILE_LINE_PID - 1]);
+
+			if (strcmp(pmstatus, PM_STATUS_READY) == 0)
+			{
+				free_readfile(optlines);
+				status = POSTMASTER_READY;
+				break;
+			}
+			else if (strcmp(pmstatus, PM_STATUS_STANDBY) == 0)
+			{
+				free_readfile(optlines);
+				status = POSTMASTER_STANDBY;
+				break;
+			}
+		}
+
+		free_readfile(optlines);
+
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+	}
+
+	if (verbose)
+		pg_log_info("postmaster.pid is available");
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not start in time");
+		exit(1);
+	}
+	else if (status == POSTMASTER_STANDBY)
+	{
+		pg_log_error("server is running but hot standby mode is not enabled");
+		exit(1);
+	}
+	else if (status == POSTMASTER_FAILED)
+	{
+		pg_log_error("could not start server");
+		fprintf(stderr, "Examine the log output.\n");
+		exit(1);
+	}
+
+	if (verbose)
+	{
+		pg_log_info("postmaster is up and running");
+		pg_log_info("waiting until the postmaster accepts connections ...");
+	}
+
+	/* Postmaster is up. Let's wait for it to accept connections. */
+	for (;;)
+	{
+		ret = PQping(conninfo);
+		if (ret == PQPING_OK)
+			break;
+		else if (ret == PQPING_NO_ATTEMPT)
+			break;
+
+		/*
+		 * Postmaster started but for some reason it crashed leaving a
+		 * postmaster.pid.
+		 */
+		if (!postmaster_is_alive((pid_t) pmpid))
+		{
+			pg_log_error("could not start server");
+			fprintf(stderr, "Examine the log output.\n");
+			exit(1);
+		}
+
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+	}
+
+	if (verbose)
+		pg_log_info("postmaster is accepting connections");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+
+	if (verbose)
+		pg_log_info("waiting the postmaster to reach the consistent state ...");
+
+	conn = connect_database(conninfo, true);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/* Does the recovery process finish? */
+		if (!in_recovery)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	if (verbose)
+		pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_subscriber must have created this
+		 * publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			if (verbose)
+				pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * XXX Unfortunately, if it reaches this code path, pg_subscriber
+			 * will always fail here. That's bad but it is not expected that
+			 * the user choose a name with pg_subscriber_ prefix followed by
+			 * the exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	if (verbose)
+		pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		PQfinish(conn);
+		exit(1);
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	if (verbose)
+		pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	if (verbose)
+		pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		PQfinish(conn);
+		exit(1);
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	if (verbose)
+		pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	if (verbose)
+		pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+					originname, lsn, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsn);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		PQfinish(conn);
+		exit(1);
+	}
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	if (verbose)
+		pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+					 PQerrorMessage(conn));
+		PQfinish(conn);
+		exit(1);
+	}
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-conninfo", required_argument, NULL, 'P'},
+		{"subscriber-conninfo", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"stop-subscriber", no_argument, NULL, 1},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_ctl_path;
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	SimpleStringListCell *cell;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo;
+
+	char	   *pub_sysid;
+	char	   *sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	int			i;
+
+	pg_logging_init(argv[0]);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_subscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		fprintf(stderr, _("You must run %s as the PostgreSQL superuser.\n"),
+				progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:t:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				simple_string_list_append(&database_names, optarg);
+				num_dbs++;
+				break;
+			case 'v':
+				verbose++;
+				break;
+			default:
+
+				/*
+				 * getopt_long already emitted a complaint
+				 */
+				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
+						progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
+				progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
+				progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * FIXME use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
+				progname);
+		exit(1);
+	}
+	dbname_conninfo = pg_malloc(NAMEDATALEN);
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
+				progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		if (verbose)
+			pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			if (verbose)
+				pg_log_info("database \"%s\" was extracted from the publisher connection string",
+							dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
+					progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute pg_ctl path on the subscriber.
+	 */
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(argv[0], "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv[0], full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		exit(1);
+	}
+
+	if (verbose)
+		pg_log_info("pg_ctl path is: %s", pg_ctl_path);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+	i = 0;
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = pg_malloc(32);
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = pg_malloc(32);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (strcmp(pub_sysid, sub_sysid) != 0)
+	{
+		pg_log_error("subscriber data directory is not a base backup from the publisher");
+		exit(1);
+	}
+
+	/*
+	 * Stop the subscriber if it is a standby server. Before executing the
+	 * transformation steps, make sure the subscriber is not running because
+	 * one of the steps is to modify some recovery parameters that require a
+	 * restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		if (verbose)
+		{
+			pg_log_info("subscriber is up and running");
+			pg_log_info("stopping the server to start the transformation steps");
+		}
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+	}
+
+	/*
+	 * Create a replication slot for each database on the publisher.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		PGresult   *res;
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
+		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
+		 * to reduce the probability of collision. By default, subscription
+		 * name is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_subscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. We could probably use the last created replication
+	 * slot, however, if this tool decides to support cloning the publisher
+	 * (via pg_basebackup -- after creating the replication slots), the
+	 * consistent point should be after the pg_basebackup finishes.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn == NULL)
+		exit(1);
+	snprintf(temp_replslot, sizeof(temp_replslot), "pg_subscriber_%d_tmp",
+			 (int) getpid());
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+					  consistent_lsn);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	disconnect_database(conn);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	if (verbose)
+		pg_log_info("starting the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+	wait_postmaster_connection(dbinfo[0].subconninfo);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create a publication for each database. This step should be executed
+	 * after promoting the subscriber to avoid replicating unnecessary
+	 * objects.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+
+		/* Connect to publisher. */
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		create_publication(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Create a subscription for each database.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+		if (conn == NULL)
+			exit(1);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * The temporary replication slot is no longer required. Drop it.
+	 * XXX we might not fail here. Instead, provide a warning so the user
+	 * XXX eventually drops the replication slot later.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+	if (conn == NULL)
+		exit(1);
+	drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+	disconnect_database(conn);
+
+	/*
+	 * Stop the subscriber.
+	 */
+	if (verbose)
+		pg_log_info("stopping the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+	success = true;
+
+	return 0;
+}
diff --git a/src/bin/pg_subscriber/t/001_basic.pl b/src/bin/pg_subscriber/t/001_basic.pl
new file mode 100644
index 0000000000..824e7d7906
--- /dev/null
+++ b/src/bin/pg_subscriber/t/001_basic.pl
@@ -0,0 +1,41 @@
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use Cwd;
+use Config;
+use File::Basename qw(basename dirname);
+use File::Path qw(rmtree);
+use Fcntl qw(:seek);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_subscriber');
+program_version_ok('pg_subscriber');
+program_options_handling_ok('pg_subscriber');
+
+my $tempdir = PostgreSQL::Test::Utils::tempdir;
+
+my $node = PostgreSQL::Test::Cluster->new('publisher');
+$node->init(allows_streaming => 'logical');
+$node->start;
+
+$node->command_fails_like(
+	['pg_subscriber'],
+	qr/no subscriber data directory specified/,
+	'target directory must be specified');
+$node->command_fails_like(
+	['pg_subscriber', '-D', $tempdir],
+	qr/no publisher connection string specified/,
+	'publisher connection string must be specified');
+$node->command_fails_like(
+	['pg_subscriber', '-D', $tempdir, '-P', 'dbname=postgres'],
+	qr/no subscriber connection string specified/,
+	'subscriber connection string must be specified');
+$node->command_fails_like(
+	['pg_subscriber', '-D', $tempdir, '-P', 'dbname=postgres', '-S', 'dbname=postgres'],
+	qr/is not a database cluster directory/,
+	'directory must be a real database cluster directory');
+
+done_testing();
diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm
index 105f5c72a2..330d312ee6 100644
--- a/src/tools/msvc/Mkvcbuild.pm
+++ b/src/tools/msvc/Mkvcbuild.pm
@@ -55,7 +55,7 @@ my @contrib_excludes = (
 # Set of variables for frontend modules
 my $frontend_defines = { 'initdb' => 'FRONTEND' };
 my @frontend_uselibpq =
-  ('pg_amcheck', 'pg_ctl', 'pg_upgrade', 'pgbench', 'psql', 'initdb');
+  ('pg_amcheck', 'pg_ctl', 'pg_upgrade', 'pgbench', 'psql', 'initdb', 'pg_subscriber');
 my @frontend_uselibpgport = (
 	'pg_amcheck',    'pg_archivecleanup',
 	'pg_test_fsync', 'pg_test_timing',
-- 
2.30.2

#2Andres Freund
andres@anarazel.de
In reply to: Euler Taveira (#1)
Re: speed up a logical replica setup

Hi,

On 2022-02-21 09:09:12 -0300, Euler Taveira wrote:

Logical replication has been used to migration with minimal downtime. However,
if you are dealing with a big database, the amount of required resources (disk
-- due to WAL retention) increases as the backlog (WAL) increases. Unless you
have a generous amount of resources and can wait for long period of time until
the new replica catches up, creating a logical replica is impracticable on
large databases.

Indeed.

DESIGN

The conversion requires 8 steps.

1. Check if the target data directory has the same system identifier than the
source data directory.
2. Stop the target server if it is running as a standby server. (Modify
recovery parameters requires a restart.)
3. Create one replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent LSN
(This consistent LSN will be used as (a) a stopping point for the recovery
process and (b) a starting point for the subscriptions).
4. Write recovery parameters into the target data directory and start the
target server (Wait until the target server is promoted).
5. Create one publication (FOR ALL TABLES) per specified database on the source
server.
6. Create one subscription per specified database on the target server (Use
replication slot and publication created in a previous step. Don't enable the
subscriptions yet).
7. Sets the replication progress to the consistent LSN that was got in a
previous step.
8. Enable the subscription for each specified database on the target server.

I think the system identifier should also be changed, otherwise you can way
too easily get into situations trying to apply WAL from different systems to
each other. Not going to end well, obviously.

This tool does not take a base backup. It can certainly be included later.
There is already a tool do it: pg_basebackup.

It would make sense to allow to call pg_basebackup from the new tool. Perhaps
with a --pg-basebackup-parameters or such.

Greetings,

Andres Freund

#3Euler Taveira
euler@eulerto.com
In reply to: Andres Freund (#2)
Re: speed up a logical replica setup

On Mon, Feb 21, 2022, at 8:28 PM, Andres Freund wrote:

I think the system identifier should also be changed, otherwise you can way
too easily get into situations trying to apply WAL from different systems to
each other. Not going to end well, obviously.

Good point.

This tool does not take a base backup. It can certainly be included later.
There is already a tool do it: pg_basebackup.

It would make sense to allow to call pg_basebackup from the new tool. Perhaps
with a --pg-basebackup-parameters or such.

Yeah. I'm planning to do that in a near future. There are a few questions in my
mind. Should we call the pg_basebackup directly (like
pglogical_create_subscriber does) or use a base backup machinery to obtain the
backup? If we choose the former, it should probably sanitize the
--pg-basebackup-parameters to allow only a subset of the command-line options
(?). AFAICS the latter requires some refactors in the pg_basebackup code --
e.g. expose at least one function (BaseBackup?) that accepts a struct of
command-line options as a parameter and returns success/failure. Another
possibility is to implement a simple BASE_BACKUP command via replication
protocol. The disadvantages are: (a) it could duplicate code and (b) it might
require maintenance if new options are added to the BASE_BACKUP command.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#4Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#1)
Re: speed up a logical replica setup

On Mon, Feb 21, 2022 at 5:41 PM Euler Taveira <euler@eulerto.com> wrote:

Logical replication has been used to migration with minimal downtime. However,
if you are dealing with a big database, the amount of required resources (disk
-- due to WAL retention) increases as the backlog (WAL) increases. Unless you
have a generous amount of resources and can wait for long period of time until
the new replica catches up, creating a logical replica is impracticable on
large databases.

The general idea is to create and convert a physical replica or a base backup
(archived WAL files available) into a logical replica. The initial data copy
and catchup tends to be faster on a physical replica. This technique has been
successfully used in pglogical_create_subscriber [1].

Sounds like a promising idea.

A new tool called pg_subscriber does this conversion and is tightly integrated
with Postgres.

DESIGN

The conversion requires 8 steps.

1. Check if the target data directory has the same system identifier than the
source data directory.
2. Stop the target server if it is running as a standby server. (Modify
recovery parameters requires a restart.)
3. Create one replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent LSN
(This consistent LSN will be used as (a) a stopping point for the recovery
process and (b) a starting point for the subscriptions).

What is the need to create an extra slot other than the slot for each
database? Can't we use the largest LSN returned by slots as the
recovery-target-lsn and starting point for subscriptions?

How, these additional slots will get freed or reused when say the
server has crashed/stopped after creating the slots but before
creating the subscriptions? Users won't even know the names of such
slots as they are internally created.

4. Write recovery parameters into the target data directory and start the
target server (Wait until the target server is promoted).
5. Create one publication (FOR ALL TABLES) per specified database on the source
server.
6. Create one subscription per specified database on the target server (Use
replication slot and publication created in a previous step. Don't enable the
subscriptions yet).
7. Sets the replication progress to the consistent LSN that was got in a
previous step.
8. Enable the subscription for each specified database on the target server.

This tool does not take a base backup. It can certainly be included later.
There is already a tool do it: pg_basebackup.

The backup will take the backup of all the databases present on the
source server. Do we need to provide the way/recommendation to remove
the databases that are not required?

Can we see some numbers with various sizes of databases (cluster) to
see how it impacts the time for small to large size databases as
compared to the traditional method? This might help giving users
advice on when to use this tool?

--
With Regards,
Amit Kapila.

#5Andreas Karlsson
andreas@proxel.se
In reply to: Euler Taveira (#1)
Re: speed up a logical replica setup

On 2/21/22 13:09, Euler Taveira wrote:

DESIGN

The conversion requires 8 steps.

1. Check if the target data directory has the same system identifier
than the
source data directory.
2. Stop the target server if it is running as a standby server. (Modify
recovery parameters requires a restart.)
3. Create one replication slot per specified database on the source
server. One
additional replication slot is created at the end to get the consistent LSN
(This consistent LSN will be used as (a) a stopping point for the recovery
process and (b) a starting point for the subscriptions).
4. Write recovery parameters into the target data directory and start the
target server (Wait until the target server is promoted).
5. Create one publication (FOR ALL TABLES) per specified database on the
source
server.
6. Create one subscription per specified database on the target server (Use
replication slot and publication created in a previous step. Don't
enable the
subscriptions yet).
7. Sets the replication progress to the consistent LSN that was got in a
previous step.
8. Enable the subscription for each specified database on the target server.

Very interesting!

I actually just a couple of weeks ago proposed a similar design for
upgrading a database of a customer of mine. We have not tried it yet so
it is not decided if we should go ahead with it.

In our case the goal is a bit different so my idea is that we will use
pg_dump/pg_restore (or pg_upgrade and then some manual cleanup if
pg_dump/pg_restore is too slow) on the target server. The goal of this
design is to get a nice clean logical replica at the new version of
PostgreSQL with indexes with the correct collations, all old invalid
constraints validated, minimal bloat, etc. And all of this without
creating bloat or putting too much load on the old master during the
process. We have plenty of disk space and plenty of time so those are
not limitations in our case. I can go into more detail if there is interest.

It is nice to see that our approach is not entirely unique. :) And I
will take a look at this patch when I find the time.

Andreas

#6Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Euler Taveira (#1)
1 attachment(s)
Re: speed up a logical replica setup

On 21.02.22 13:09, Euler Taveira wrote:

A new tool called pg_subscriber does this conversion and is tightly
integrated
with Postgres.

Are we comfortable with the name pg_subscriber? It seems too general.
Are we planning other subscriber-related operations in the future? If
so, we should at least make this one use a --create option or
something like that.

doc/src/sgml/ref/pg_subscriber.sgml

Attached is a patch that reorganizes the man page a bit. I moved the
description of the steps to the Notes section and formatted it
differently. I think the steps are interesting but not essential for
the using of the program, so I wanted to get them out of the main
description.

src/bin/pg_subscriber/pg_subscriber.c

+   if (made_temp_replslot)
+   {
+       conn = connect_database(dbinfo[0].pubconninfo, true);
+       drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+       disconnect_database(conn);
+   }

Temp slots don't need to be cleaned up.

+/*
+ * Obtain the system identifier from control file. It will be used to 
compare
+ * if a data directory is a clone of another one. This routine is used 
locally
+ * and avoids a replication connection.
+ */
+static char *
+get_control_from_datadir(const char *datadir)

This could return uint64 directly, without string conversion.
get_sysid_from_conn() could then convert to uint64 internally.

+ {"verbose", no_argument, NULL, 'v'},

I'm not sure if the --verbose option is all that useful.

+ {"stop-subscriber", no_argument, NULL, 1},

This option doesn't seem to be otherwise supported or documented.

+   pub_sysid = pg_malloc(32);
+   pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+   sub_sysid = pg_malloc(32);
+   sub_sysid = get_control_from_datadir(subscriber_dir);

These mallocs don't appears to be of any use.

+ dbname_conninfo = pg_malloc(NAMEDATALEN);

This seems wrong.

Overall, this code could use a little bit more structuring. There are
a lot of helper functions that don't seem to do a lot and are mostly
duplicate runs-this-SQL-command calls. But the main() function is
still huge. There is room for refinement.

src/bin/pg_subscriber/t/001_basic.pl

Good start, but obviously, we'll need some real test cases here also.

src/bin/initdb/initdb.c
src/bin/pg_ctl/pg_ctl.c
src/common/file_utils.c
src/include/common/file_utils.h

I recommend skipping this refactoring. The readfile() function from
pg_ctl is not general enough to warrant the pride of place of a
globally available function. Note that it is specifically geared
toward some of pg_ctl's requirements, for example that the underlying
file can change while it is being read.

The requirements of pg_subscriber can be satisfied more easily: Just
call pg_ctl to start the server. You are already using that in
pg_subscriber. Is there a reason it can't be used here as well?

Attachments:

0001-fixup-Create-a-new-logical-replica-from-a-base-backu.patchtext/plain; charset=UTF-8; name=0001-fixup-Create-a-new-logical-replica-from-a-base-backu.patchDownload
From a0ad5fdaddc17ef74594bfd3c65c777649d1544b Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Tue, 15 Mar 2022 14:39:19 +0100
Subject: [PATCH] fixup! Create a new logical replica from a base backup or
 standby server.

---
 doc/src/sgml/ref/pg_subscriber.sgml | 172 ++++++++++++++++------------
 1 file changed, 99 insertions(+), 73 deletions(-)

diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
index e68a19092e..df63c6a993 100644
--- a/doc/src/sgml/ref/pg_subscriber.sgml
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -43,79 +43,6 @@ <title>Description</title>
    replication connections from the target server (known as subscriber server).
    The target server should accept local logical replication connection.
   </para>
-
-  <para>
-   The transformation proceeds in eight steps. First,
-   <application>pg_subscriber</application> checks if the given target data
-   directory has the same system identifier than the source data directory.
-   Since it uses the recovery process as one of the steps, it starts the target
-   server as a replica from the source server. If the system identifier is not
-   the same, <application>pg_subscriber</application> will terminate with an
-   error.
-  </para>
-
-  <para>
-   Second, <application>pg_subscriber</application> checks if the target data
-   directory is used by a standby server. Stop the standby server if it is
-   running. One of the next steps is to add some recovery parameters that
-   requires a server start. This step avoids an error.
-  </para>
-
-  <para>
-   Next, <application>pg_subscriber</application> creates one replication slot
-   for each specified database on the source server. The replication slot name
-   contains a <literal>pg_subscriber</literal> prefix. These replication slots
-   will be used by the subscriptions in a future step.  Another replication
-   slot is used to get a consistent start location. This consistent LSN will be
-   used (a) as a stopping point in the <xref
-   linkend="guc-recovery-target-lsn"/> parameter and (b) by the subscriptions
-   as a replication starting point. It guarantees that no transaction will be
-   lost.
-  </para>
-
-  <para>
-   Next, write recovery parameters into the target data directory and start the
-   target server. It specifies a LSN (consistent LSN that was obtained in the
-   previous step) of write-ahead log location up to which recovery will
-   proceed. It also specifies <literal>promote</literal> as the action that the
-   server should take once the recovery target is reached. This step finishes
-   once the server ends standby mode and is accepting read-write operations.
-  </para>
-
-  <para>
-   Next, <application>pg_subscriber</application> creates one publication for
-   each specified database on the source server. Each publication replicates
-   changes for all tables in the database. The publication name contains a
-   <literal>pg_subscriber</literal> prefix. These publication will be used by a
-   corresponding subscription in a next step.
-  </para>
-
-  <para>
-   Next, <application>pg_subscriber</application> creates one subscription for
-   each specified database on the target server. Each subscription name
-   contains a <literal>pg_subscriber</literal> prefix. The replication slot
-   name is identical to the subscription name. It also does not copy existing
-   data from the source server. It does not create a replication slot. Instead,
-   it uses the replication slot that was created in a previous step. The
-   subscription is created but it is not enabled yet. The reason is the
-   replication progress must be set to the consistent LSN but replication
-   origin name contains the subscription oid in its name. Hence, the
-   subscription will be enabled in a separate step.
-  </para>
-
-  <para>
-   Next, <application>pg_subscriber</application> sets the replication progress
-   to the consistent LSN that was obtained in a previous step. When the target
-   server started the recovery process, it caught up to the consistent LSN.
-   This is the exact LSN to be used as a initial location for the logical
-   replication.
-  </para>
-
-  <para>
-   Finally, <application>pg_subscriber</application> enables the subscription
-   for each specified database on the target server. The subscription starts
-   streaming from the consistent LSN.
-  </para>
  </refsect1>
 
  <refsect1>
@@ -209,6 +136,105 @@ <title>Options</title>
 
  </refsect1>
 
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_subscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the target data
+     directory is used by a standby server. Stop the standby server if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_subscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  Another
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used (a) as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and (b) by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_subscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_subscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_subscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It also does not copy
+     existing data from the source server. It does not create a replication
+     slot. Instead, it uses the replication slot that was created in a
+     previous step. The subscription is created but it is not enabled yet. The
+     reason is the replication progress must be set to the consistent LSN but
+     replication origin name contains the subscription oid in its name. Hence,
+     the subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for the logical
+     replication.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_subscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
  <refsect1>
   <title>Examples</title>
 
-- 
2.35.1

#7Andrew Dunstan
andrew@dunslane.net
In reply to: Peter Eisentraut (#6)
Re: speed up a logical replica setup

On 3/15/22 09:51, Peter Eisentraut wrote:

On 21.02.22 13:09, Euler Taveira wrote:

A new tool called pg_subscriber does this conversion and is tightly
integrated
with Postgres.

Are we comfortable with the name pg_subscriber?  It seems too general.
Are we planning other subscriber-related operations in the future?  If
so, we should at least make this one use a --create option or
something like that.

Not really sold on the name (and I didn't much like the name
pglogical_create_subscriber either, although it's a cool facility and
I'm happy to see us adopting something like it).

ISTM we should have a name that conveys that we are *converting* a
replica or equivalent to a subscriber.

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#8Andres Freund
andres@anarazel.de
In reply to: Euler Taveira (#1)
Re: speed up a logical replica setup

Hi,

On 2022-02-21 09:09:12 -0300, Euler Taveira wrote:

A new tool called pg_subscriber does this conversion and is tightly integrated
with Postgres.

Given that this has been submitted just before the last CF and is a patch of
nontrivial size, has't made significant progress ISTM it should be moved to
the next CF?

It currently fails in cfbot, but that's likely just due to Peter's incremental
patch. Perhaps you could make sure the patch still applies and repost?

Greetings,

Andres Freund

#9Fabrízio de Royes Mello
fabriziomello@gmail.com
In reply to: Andrew Dunstan (#7)
Re: speed up a logical replica setup

On Fri, 18 Mar 2022 at 19:34 Andrew Dunstan <andrew@dunslane.net> wrote:

On 3/15/22 09:51, Peter Eisentraut wrote:

On 21.02.22 13:09, Euler Taveira wrote:

A new tool called pg_subscriber does this conversion and is tightly
integrated
with Postgres.

Are we comfortable with the name pg_subscriber? It seems too general.
Are we planning other subscriber-related operations in the future? If
so, we should at least make this one use a --create option or
something like that.

Not really sold on the name (and I didn't much like the name
pglogical_create_subscriber either, although it's a cool facility and
I'm happy to see us adopting something like it).

ISTM we should have a name that conveys that we are *converting* a
replica or equivalent to a subscriber.

Some time ago I did a POC on it [1]https://github.com/fabriziomello/pg_create_subscriber -- Fabrízio de Royes Mello and I used the name pg_create_subscriber

[1]: https://github.com/fabriziomello/pg_create_subscriber -- Fabrízio de Royes Mello
https://github.com/fabriziomello/pg_create_subscriber
--
Fabrízio de Royes Mello

#10Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andrew Dunstan (#7)
Re: speed up a logical replica setup

On 18.03.22 23:34, Andrew Dunstan wrote:

On 3/15/22 09:51, Peter Eisentraut wrote:

On 21.02.22 13:09, Euler Taveira wrote:

A new tool called pg_subscriber does this conversion and is tightly
integrated
with Postgres.

Are we comfortable with the name pg_subscriber?  It seems too general.
Are we planning other subscriber-related operations in the future?  If
so, we should at least make this one use a --create option or
something like that.

Not really sold on the name (and I didn't much like the name
pglogical_create_subscriber either, although it's a cool facility and
I'm happy to see us adopting something like it).

ISTM we should have a name that conveys that we are *converting* a
replica or equivalent to a subscriber.

The pglogical tool includes the pg_basebackup run, so it actually
"creates" the subscriber from scratch. Whether this tool is also doing
that is still being discussed.

#11Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andres Freund (#8)
Re: speed up a logical replica setup

On 22.03.22 02:25, Andres Freund wrote:

On 2022-02-21 09:09:12 -0300, Euler Taveira wrote:

A new tool called pg_subscriber does this conversion and is tightly integrated
with Postgres.

Given that this has been submitted just before the last CF and is a patch of
nontrivial size, has't made significant progress ISTM it should be moved to
the next CF?

done

#12Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#11)
Re: speed up a logical replica setup

This entry has been waiting on author input for a while (our current
threshold is roughly two weeks), so I've marked it Returned with
Feedback.

Once you think the patchset is ready for review again, you (or any
interested party) can resurrect the patch entry by visiting

https://commitfest.postgresql.org/38/3556/

and changing the status to "Needs Review", and then changing the
status again to "Move to next CF". (Don't forget the second step;
hopefully we will have streamlined this in the near future!)

Thanks,
--Jacob

#13Euler Taveira
euler@eulerto.com
In reply to: Euler Taveira (#1)
1 attachment(s)
Re: speed up a logical replica setup

On Mon, Feb 21, 2022, at 9:09 AM, Euler Taveira wrote:

A new tool called pg_subscriber does this conversion and is tightly integrated
with Postgres.

After a long period of inactivity, I'm back to this client tool. As suggested
by Andres, I added a new helper function to change the system identifier as the
last step. I also thought about including the pg_basebackup support but decided
to keep it simple (at least for this current version). The user can always
execute pg_basebackup as a preliminary step to create a standby replica and it
will work. (I will post a separate patch that includes the pg_basebackup
support on the top of this one.)

Amit asked if an extra replication slot is required. It is not. The reason I
keep it is to remember that at least the latest replication slot needs to be
created after the pg_basebackup finishes (pg_backup_stop() call). Regarding the
atexit() routine, it tries to do the best to remove all the objects it created,
however, there is no guarantee it can remove them because it depends on
external resources such as connectivity and authorization. I added a new
warning message if it cannot drop the transient replication slot. It is
probably a good idea to add such warning message into the cleanup routine too.
More to this point, another feature that checks and remove all left objects.
The transient replication slot is ok because it should always be removed at the
end. However, the big question is how to detect that you are not removing
objects (publications, subscriptions, replication slots) from a successful
conversion.

Amit also asked about setup a logical replica with m databases where m is less
than the total number of databases. One option is to remove the "extra"
databases in the target server after promoting the physical replica or in one
of the latest steps. Maybe it is time to propose partial physical replica that
contains only a subset of databases on primary. (I'm not volunteering to it.)
Hence, pg_basebackup has an option to remove these "extra" databases so this
tool can take advantage of it.

Let's continue with the bike shedding... I agree with Peter E that this name
does not express what this tool is. At the moment, it only have one action:
create. If I have to suggest other actions I would say that it could support
switchover option too (that removes the infrastructure created by this tool).
If we decide to keep this name, it should be a good idea to add an option to
indicate what action it is executing (similar to pg_recvlogical) as suggested
by Peter.

I included the documentation cleanups that Peter E shared. I also did small
adjustments into the documentation. It probably deserves a warning section that
advertises about the cleanup.

I refactored the transient replication slot code and decided to use a permanent
(instead of temporary) slot to avoid keeping a replication connection open for
a long time until the target server catches up.

The system identifier functions (get_control_from_datadir() and
get_sysid_from_conn()) now returns uint64 as suggested by Peter.

After reflection, the --verbose option should be renamed to --progress. There
are also some messages that should be converted to debug messages.

I fixed the useless malloc. I rearrange the code a bit but the main still has ~
370 lines (without options/validation ~ 255 lines. I'm trying to rearrange the
code to make the code easier to read and at the same time reduce the main size.
I already have a few candidates in mind such as the code that stops the standby
and the part that includes the recovery parameters. I removed the refactor I
proposed in the previous patch and the current code is relying on pg_ctl --wait
behavior. Are there issues with this choice? Well, one annoying situation is
that pg_ctl does not have a "wait forever" option. If one of the pg_ctl calls
fails, you could probably have to start again (unless you understand the
pg_subscriber internals and fix the setup by yourself). You have to choose an
arbitrary timeout value and expect that pg_ctl *does* perform the action less
than the timeout.

Real tests are included. The cleanup code does not have coverage because a
simple reproducible case isn't easy. I'm also not sure if it is worth it. We
can explain it in the warning section that was proposed.

It is still a WIP but I would like to share it and get some feedback.

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v2-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchtext/x-patch; name="=?UTF-8?Q?v2-0001-Creates-a-new-logical-replica-from-a-standby-serv.patc?= =?UTF-8?Q?h?="Download
From 0aca46ed57c15bce1972c9db6459c04bcc5cf73d Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v2] Creates a new logical replica from a standby server

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server. Remove
the additional replication slot that was used to get the consistent LSN.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml         |    1 +
 doc/src/sgml/ref/pg_subscriber.sgml    |  271 +++++
 doc/src/sgml/reference.sgml            |    1 +
 src/bin/Makefile                       |    1 +
 src/bin/meson.build                    |    1 +
 src/bin/pg_basebackup/streamutil.c     |   55 +
 src/bin/pg_basebackup/streamutil.h     |    5 +
 src/bin/pg_subscriber/Makefile         |   39 +
 src/bin/pg_subscriber/meson.build      |   31 +
 src/bin/pg_subscriber/pg_subscriber.c  | 1470 ++++++++++++++++++++++++
 src/bin/pg_subscriber/po/meson.build   |    3 +
 src/bin/pg_subscriber/t/001_basic.pl   |   42 +
 src/bin/pg_subscriber/t/002_standby.pl |  114 ++
 src/tools/msvc/Mkvcbuild.pm            |    2 +-
 src/tools/pgindent/typedefs.list       |    7 +-
 15 files changed, 2038 insertions(+), 5 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_subscriber.sgml
 create mode 100644 src/bin/pg_subscriber/Makefile
 create mode 100644 src/bin/pg_subscriber/meson.build
 create mode 100644 src/bin/pg_subscriber/pg_subscriber.c
 create mode 100644 src/bin/pg_subscriber/po/meson.build
 create mode 100644 src/bin/pg_subscriber/t/001_basic.pl
 create mode 100644 src/bin/pg_subscriber/t/002_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 54b5f22d6e..e2ecb4f944 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -213,6 +213,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgSubscriber       SYSTEM "pg_subscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
new file mode 100644
index 0000000000..8480a3a281
--- /dev/null
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -0,0 +1,271 @@
+<!--
+doc/src/sgml/ref/pg_subscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgsubscriber">
+ <indexterm zone="app-pgsubscriber">
+  <primary>pg_subscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_subscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>create a new logical replica from a standby server</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_subscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a standby server and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_subscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a standby
+        server.
+       </para>
+      </listitem>
+     </varlistentry>
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--publisher-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_subscriber</application> to output progress messages
+        and detailed information about each step.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_subscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_subscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_subscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the target data
+     directory is used by a standby server. Stop the standby server if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_subscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  Another
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_subscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_subscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_subscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_subscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> removes the additional replication
+     slot that was used to get the consistent LSN on the source server.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a standby server at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..67e257436b 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -257,6 +257,7 @@
    &pgReceivewal;
    &pgRecvlogical;
    &pgRestore;
+   &pgSubscriber;
    &pgVerifyBackup;
    &psqlRef;
    &reindexdb;
diff --git a/src/bin/Makefile b/src/bin/Makefile
index 373077bf52..9abd5b9711 100644
--- a/src/bin/Makefile
+++ b/src/bin/Makefile
@@ -25,6 +25,7 @@ SUBDIRS = \
 	pg_dump \
 	pg_resetwal \
 	pg_rewind \
+	pg_subscriber \
 	pg_test_fsync \
 	pg_test_timing \
 	pg_upgrade \
diff --git a/src/bin/meson.build b/src/bin/meson.build
index 67cb50630c..c7a6881e5f 100644
--- a/src/bin/meson.build
+++ b/src/bin/meson.build
@@ -11,6 +11,7 @@ subdir('pg_ctl')
 subdir('pg_dump')
 subdir('pg_resetwal')
 subdir('pg_rewind')
+subdir('pg_subscriber')
 subdir('pg_test_fsync')
 subdir('pg_test_timing')
 subdir('pg_upgrade')
diff --git a/src/bin/pg_basebackup/streamutil.c b/src/bin/pg_basebackup/streamutil.c
index dbd08ab172..d8e438f114 100644
--- a/src/bin/pg_basebackup/streamutil.c
+++ b/src/bin/pg_basebackup/streamutil.c
@@ -33,6 +33,9 @@
 
 int			WalSegSz;
 
+static bool CreateReplicationSlot_internal(PGconn *conn, const char *slot_name, const char *plugin,
+					  bool is_temporary, bool is_physical, bool reserve_wal,
+					  bool slot_exists_ok, bool two_phase, char *lsn);
 static bool RetrieveDataDirCreatePerm(PGconn *conn);
 
 /* SHOW command for replication connection was introduced in version 10 */
@@ -583,6 +586,26 @@ bool
 CreateReplicationSlot(PGconn *conn, const char *slot_name, const char *plugin,
 					  bool is_temporary, bool is_physical, bool reserve_wal,
 					  bool slot_exists_ok, bool two_phase)
+{
+	return CreateReplicationSlot_internal(conn, slot_name, plugin,
+					  is_temporary, is_physical, reserve_wal,
+					  slot_exists_ok, two_phase, NULL);
+}
+
+bool
+CreateReplicationSlotLSN(PGconn *conn, const char *slot_name, const char *plugin,
+					  bool is_temporary, bool is_physical, bool reserve_wal,
+					  bool slot_exists_ok, bool two_phase, char *lsn)
+{
+	return CreateReplicationSlot_internal(conn, slot_name, plugin,
+					  is_temporary, is_physical, reserve_wal,
+					  slot_exists_ok, two_phase, lsn);
+}
+
+static bool
+CreateReplicationSlot_internal(PGconn *conn, const char *slot_name, const char *plugin,
+					  bool is_temporary, bool is_physical, bool reserve_wal,
+					  bool slot_exists_ok, bool two_phase, char *lsn)
 {
 	PQExpBuffer query;
 	PGresult   *res;
@@ -654,6 +677,30 @@ CreateReplicationSlot(PGconn *conn, const char *slot_name, const char *plugin,
 		{
 			destroyPQExpBuffer(query);
 			PQclear(res);
+
+			/* Duplicate replication slot. Obtain the current LSN. */
+			if (lsn)
+			{
+				query = createPQExpBuffer();
+				appendPQExpBuffer(query, "SELECT restart_lsn FROM pg_catalog.pg_replication_slots WHERE slot_name = '%s'", slot_name);
+				res = PQexec(conn, query->data);
+				if (PQresultStatus(res) != PGRES_TUPLES_OK)
+				{
+					pg_log_error("could not read replication slot \"%s\": got %d rows, expected %d rows", slot_name, PQntuples(res), 1);
+					return false;	/* FIXME can't happen */
+				}
+				else if (PQgetisnull(res, 0, 0))
+				{
+					lsn = NULL;
+				}
+				else
+				{
+					lsn = pg_strdup(PQgetvalue(res, 0, 0));
+				}
+				destroyPQExpBuffer(query);
+				PQclear(res);
+			}
+
 			return true;
 		}
 		else
@@ -678,6 +725,14 @@ CreateReplicationSlot(PGconn *conn, const char *slot_name, const char *plugin,
 		return false;
 	}
 
+	if (lsn)
+	{
+		if (PQgetisnull(res, 0, 1))
+			lsn = NULL;
+		else
+			lsn = pg_strdup(PQgetvalue(res, 0, 1));
+	}
+
 	destroyPQExpBuffer(query);
 	PQclear(res);
 	return true;
diff --git a/src/bin/pg_basebackup/streamutil.h b/src/bin/pg_basebackup/streamutil.h
index 268c163213..bbd0789d2b 100644
--- a/src/bin/pg_basebackup/streamutil.h
+++ b/src/bin/pg_basebackup/streamutil.h
@@ -36,6 +36,11 @@ extern bool CreateReplicationSlot(PGconn *conn, const char *slot_name,
 								  const char *plugin, bool is_temporary,
 								  bool is_physical, bool reserve_wal,
 								  bool slot_exists_ok, bool two_phase);
+extern bool CreateReplicationSlotLSN(PGconn *conn, const char *slot_name,
+								  const char *plugin, bool is_temporary,
+								  bool is_physical, bool reserve_wal,
+								  bool slot_exists_ok, bool two_phase,
+								  char *lsn);
 extern bool DropReplicationSlot(PGconn *conn, const char *slot_name);
 extern bool RunIdentifySystem(PGconn *conn, char **sysid,
 							  TimeLineID *starttli,
diff --git a/src/bin/pg_subscriber/Makefile b/src/bin/pg_subscriber/Makefile
new file mode 100644
index 0000000000..b580e57382
--- /dev/null
+++ b/src/bin/pg_subscriber/Makefile
@@ -0,0 +1,39 @@
+# src/bin/pg_subscriber/Makefile
+
+PGFILEDESC = "pg_subscriber - create a new logical replica from a standby server"
+PGAPPICON=win32
+
+subdir = src/bin/pg_subscriber
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+OBJS = \
+	$(WIN32RES) \
+	pg_subscriber.o
+
+all: pg_subscriber
+
+pg_subscriber: $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
+install: all installdirs
+	$(INSTALL_PROGRAM) pg_subscriber$(X) '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
+
+installdirs:
+	$(MKDIR_P) '$(DESTDIR)$(bindir)'
+
+uninstall:
+	rm -f '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
+
+clean distclean maintainer-clean:
+	rm -f pg_subscriber$(X) $(OBJS)
+	rm -rf tmp_check
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
diff --git a/src/bin/pg_subscriber/meson.build b/src/bin/pg_subscriber/meson.build
new file mode 100644
index 0000000000..868c81dc62
--- /dev/null
+++ b/src/bin/pg_subscriber/meson.build
@@ -0,0 +1,31 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+pg_subscriber_sources = files(
+  'pg_subscriber.c'
+)
+
+if host_system == 'windows'
+  pg_subscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_subscriber',
+	'--FILEDESC', 'pg_subscriber - create a new logical replica from a standby server',])
+endif
+
+pg_subscriber = executable('pg_subscriber',
+  pg_subscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_subscriber
+
+tests += {
+  'name': 'pg_subscriber',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_basic.pl',
+    ],
+  },
+}
+
+subdir('po', if_found: libintl)
diff --git a/src/bin/pg_subscriber/pg_subscriber.c b/src/bin/pg_subscriber/pg_subscriber.c
new file mode 100644
index 0000000000..b2ff6d7c15
--- /dev/null
+++ b/src/bin/pg_subscriber/pg_subscriber.c
@@ -0,0 +1,1470 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_subscriber/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool secure_search_path);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static bool create_all_logical_replication_slots(LogicalRepInfo *dbinfo);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static int	verbose = 0;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static char temp_replslot[NAMEDATALEN] = {0};
+static bool made_transient_replslot = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_subscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo, true);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo, true);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+
+	if (made_transient_replslot)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo, true);
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		disconnect_database(conn);
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-conninfo=CONNINFO   publisher connection string\n"));
+	printf(_(" -S, --subscriber-conninfo=CONNINFO  subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name plus a fallback application name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	if (verbose)
+		pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	if (i > 0)
+		appendPQExpBufferChar(buf, ' ');
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	if (verbose)
+		pg_log_info("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	if (verbose)
+		pg_log_info("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	if (verbose)
+		pg_log_info("checking if directory \"%s\" is a cluster data directory",
+					datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+	appendPQExpBufferStr(buf, " replication=database");
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo, bool secure_search_path)
+{
+	PGconn	   *conn;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	if (secure_search_path)
+	{
+		PGresult   *res;
+
+		res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+			return NULL;
+		}
+		PQclear(res);
+	}
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	char	   *repconninfo;
+	uint64		sysid;
+
+	if (verbose)
+		pg_log_info("getting system identifier from publisher");
+
+	repconninfo = psprintf("%s replication=database", conninfo);
+	conn = connect_database(repconninfo, false);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	if (verbose)
+		pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	if (verbose)
+		pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	update_controlfile(datadir, cf, true);
+
+	if (verbose)
+		pg_log_info("running pg_resetwal in the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+	rc = system(cmd_str);
+	if (rc == 0)
+		pg_log_info("subscriber successfully changed the system identifier");
+	else
+		pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+
+	pfree(cf);
+}
+
+static bool
+create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+{
+	int			i;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
+		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
+		 * to reduce the probability of collision. By default, subscription
+		 * name is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_subscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, sizeof(slot_name), "pg_subscriber_%d_startpoint",
+			 (int) getpid());
+		transient_replslot = true;
+	}
+
+	if (verbose)
+		pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+					 PQresultErrorMessage(res));
+		return lsn;
+	}
+
+	/* for cleanup purposes */
+	if (transient_replslot)
+		made_transient_replslot = true;
+	else
+		dbinfo->made_replslot = true;
+
+	lsn = pg_strdup(PQgetvalue(res, 0, 1));
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	if (verbose)
+		pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+					 PQerrorMessage(conn));
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (verbose)
+	{
+		if (action)
+			pg_log_info("postmaster was started");
+		else
+			pg_log_info("postmaster was stopped");
+	}
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+
+	if (verbose)
+		pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/* Does the recovery process finish? */
+		if (!in_recovery)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	if (verbose)
+		pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_subscriber must have created this
+		 * publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			if (verbose)
+				pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always fail
+			 * (unless you decide to change the existing publication name).
+			 * That's bad but it is very unlikely that the user will choose a
+			 * name with pg_subscriber_ prefix followed by the exact database
+			 * oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	if (verbose)
+		pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		PQfinish(conn);
+		exit(1);
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	if (verbose)
+		pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	if (verbose)
+		pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		PQfinish(conn);
+		exit(1);
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	if (verbose)
+		pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	if (verbose)
+		pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+					originname, lsn, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsn);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		PQfinish(conn);
+		exit(1);
+	}
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	if (verbose)
+		pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	if (verbose)
+		pg_log_info("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+					 PQerrorMessage(conn));
+		PQfinish(conn);
+		exit(1);
+	}
+
+	PQclear(res);
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-conninfo", required_argument, NULL, 'P'},
+		{"subscriber-conninfo", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+	int			rc;
+
+	char	   *pg_ctl_cmd;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	int			i;
+
+	pg_logging_init(argv[0]);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_subscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:t:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				simple_string_list_append(&database_names, optarg);
+				num_dbs++;
+				break;
+			case 'v':
+				verbose++;
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		if (verbose)
+			pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			if (verbose)
+				pg_log_info("database \"%s\" was extracted from the publisher connection string",
+							dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * Stop the subscriber if it is a standby server. Before executing the
+	 * transformation steps, make sure the subscriber is not running because
+	 * one of the steps is to modify some recovery parameters that require a
+	 * restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		if (verbose)
+		{
+			pg_log_info("subscriber is up and running");
+			pg_log_info("stopping the server to start the transformation steps");
+		}
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+	}
+
+	/*
+	 * Create a replication slot for each database on the publisher.
+	 */
+	if (!create_all_logical_replication_slots(dbinfo))
+		exit(1);
+
+	/*
+	 * Create a logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. We cannot use the last created replication slot
+	 * because the consistent LSN should be obtained *after* the base backup
+	 * finishes (and the base backup should include the logical replication
+	 * slots).
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 *
+	 * A temporary replication slot is not used here to avoid keeping a
+	 * replication connection open (depending when base backup was taken, the
+	 * connection should be open for a few hours).
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+					  consistent_lsn);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	disconnect_database(conn);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	if (verbose)
+		pg_log_info("starting the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create a publication for each database. This step should be executed
+	 * after promoting the subscriber to avoid replicating unnecessary
+	 * objects.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+
+		/* Connect to publisher. */
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		create_publication(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Create a subscription for each database.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+		if (conn == NULL)
+			exit(1);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * The transient replication slot is no longer required. Drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops the replication slot later.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+	if (conn == NULL)
+	{
+		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+	else
+	{
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	if (verbose)
+		pg_log_info("stopping the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	success = true;
+
+	if (verbose)
+		pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_subscriber/po/meson.build b/src/bin/pg_subscriber/po/meson.build
new file mode 100644
index 0000000000..f287b5e974
--- /dev/null
+++ b/src/bin/pg_subscriber/po/meson.build
@@ -0,0 +1,3 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+nls_targets += [i18n.gettext('pg_subscriber-' + pg_version_major.to_string())]
diff --git a/src/bin/pg_subscriber/t/001_basic.pl b/src/bin/pg_subscriber/t/001_basic.pl
new file mode 100644
index 0000000000..505a060101
--- /dev/null
+++ b/src/bin/pg_subscriber/t/001_basic.pl
@@ -0,0 +1,42 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_subscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_subscriber');
+program_version_ok('pg_subscriber');
+program_options_handling_ok('pg_subscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_subscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+		'--subscriber-conninfo', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_subscriber/t/002_standby.pl b/src/bin/pg_subscriber/t/002_standby.pl
new file mode 100644
index 0000000000..40bc3bf13c
--- /dev/null
+++ b/src/bin/pg_subscriber/t/002_standby.pl
@@ -0,0 +1,114 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical');
+$node_f->start;
+
+# Create databases
+# Create a test table and insert a row in primary server
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', "CREATE TABLE tbl1 (a text)");
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', "CREATE TABLE tbl2 (a text)");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on P and wait standby S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_subscriber on about-to-fail node (F)
+command_fails(
+	[
+		'pg_subscriber', "--verbose",
+		"--pgdata", $node_f->data_dir,
+		"--publisher-conninfo", $node_p->connstr('pg1'),
+		"--subscriber-conninfo", $node_f->connstr('pg1'),
+		"--database", 'pg1',
+		"--database", 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Run pg_subscriber on node S
+command_ok(
+	[
+		'pg_subscriber', "--verbose",
+		"--pgdata", $node_s->data_dir,
+		"--publisher-conninfo", $node_p->connstr('pg1'),
+		"--subscriber-conninfo", $node_s->connstr('pg1'),
+		"--database", 'pg1',
+		"--database", 'pg2'
+	],
+	'run pg_subscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_subscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', "SELECT * FROM tbl1");
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', "SELECT * FROM tbl2");
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', "SELECT system_identifier FROM pg_control_system()");
+my $sysid_s = $node_s->safe_psql('postgres', "SELECT system_identifier FROM pg_control_system()");
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm
index db242c9205..b35a5ec693 100644
--- a/src/tools/msvc/Mkvcbuild.pm
+++ b/src/tools/msvc/Mkvcbuild.pm
@@ -55,7 +55,7 @@ my @contrib_excludes = (
 # Set of variables for frontend modules
 my $frontend_defines = { 'pgbench' => 'FD_SETSIZE=1024' };
 my @frontend_uselibpq =
-  ('pg_amcheck', 'pg_ctl', 'pg_upgrade', 'pgbench', 'psql', 'initdb');
+  ('pg_amcheck', 'pg_ctl', 'pg_upgrade', 'pgbench', 'psql', 'initdb', 'pg_subscriber');
 my @frontend_uselibpgport = (
 	'pg_amcheck', 'pg_archivecleanup',
 	'pg_test_fsync', 'pg_test_timing',
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 06b25617bc..2d5c08d06a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1275,9 +1275,9 @@ JsonManifestWALRangeField
 JsonObjectAgg
 JsonObjectConstructor
 JsonOutput
-JsonParseExpr
 JsonParseContext
 JsonParseErrorType
+JsonParseExpr
 JsonPath
 JsonPathBool
 JsonPathExecContext
@@ -1340,6 +1340,7 @@ LINE
 LLVMAttributeRef
 LLVMBasicBlockRef
 LLVMBuilderRef
+LLVMContextRef
 LLVMErrorRef
 LLVMIntPredicate
 LLVMJITEventListenerRef
@@ -1913,7 +1914,6 @@ ParallelHashJoinBatch
 ParallelHashJoinBatchAccessor
 ParallelHashJoinState
 ParallelIndexScanDesc
-ParallelReadyList
 ParallelSlot
 ParallelSlotArray
 ParallelSlotResultHandler
@@ -2993,7 +2993,6 @@ WaitEvent
 WaitEventActivity
 WaitEventBufferPin
 WaitEventClient
-WaitEventExtension
 WaitEventExtensionCounterData
 WaitEventExtensionEntryById
 WaitEventExtensionEntryByName
@@ -3403,6 +3402,7 @@ indexed_tlist
 inet
 inetKEY
 inet_struct
+initRowMethod
 init_function
 inline_cte_walker_context
 inline_error_callback_arg
@@ -3870,7 +3870,6 @@ wchar2mb_with_len_converter
 wchar_t
 win32_deadchild_waitinfo
 wint_t
-worker_spi_state
 worker_state
 worktable
 wrap
-- 
2.30.2

#14Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Euler Taveira (#13)
Re: speed up a logical replica setup

On Mon, Oct 23, 2023 at 9:34 AM Euler Taveira <euler@eulerto.com> wrote:

It is still a WIP but I would like to share it and get some feedback.

I have started reviewing the patch. I have just read through all the
code. It's well documented and clear. Next I will review the design in
detail. Here are a couple of minor comments
1.
+tests += {
+ 'name': 'pg_subscriber',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'tests': [
+ 't/001_basic.pl',

COMMENT
Shouldn't we include 002_standby.pl?

2. CreateReplicationSlotLSN, is not used anywhere. Instead I see
create_logical_replication_slot() in pg_subscriber.c. Which of these
two you intend to use finally?

--
Best Wishes,
Ashutosh Bapat

#15shihao zhong
zhong950419@gmail.com
In reply to: Ashutosh Bapat (#14)
Re: speed up a logical replica setup

I think this is duplicate with https://commitfest.postgresql.org/45/4637/

The new status of this patch is: Waiting on Author

#16Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Ashutosh Bapat (#14)
Re: speed up a logical replica setup

On Thu, Oct 26, 2023 at 5:17 PM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:

On Mon, Oct 23, 2023 at 9:34 AM Euler Taveira <euler@eulerto.com> wrote:

It is still a WIP but I would like to share it and get some feedback.

I have started reviewing the patch. I have just read through all the
code. It's well documented and clear. Next I will review the design in
detail.

Here are some comments about functionality and design.

+ <step>
+ <para>
+ <application>pg_subscriber</application> creates one replication slot for
+ each specified database on the source server. The replication slot name
+ contains a <literal>pg_subscriber</literal> prefix. These replication
+ slots will be used by the subscriptions in a future step. Another
+ replication slot is used to get a consistent start location. This
+ consistent LSN will be used as a stopping point in the <xref
+ linkend="guc-recovery-target-lsn"/> parameter and by the
+ subscriptions as a replication starting point. It guarantees that no
+ transaction will be lost.
+ </para>
+ </step>

CREATE_REPLICATION_SLOT would wait for any incomplete transaction to
complete. So it may not be possible to have an incomplete transaction
on standby when it comes out of recovery. Am I correct? Can we please
have a testcase where we test this scenario? What about a prepared
transactions?

+
+ <step>
+ <para>
+ <application>pg_subscriber</application> writes recovery parameters into
+ the target data directory and start the target server. It specifies a LSN
+ (consistent LSN that was obtained in the previous step) of write-ahead
+ log location up to which recovery will proceed. It also specifies
+ <literal>promote</literal> as the action that the server should take once
+ the recovery target is reached. This step finishes once the server ends
+ standby mode and is accepting read-write operations.
+ </para>
+ </step>

At this stage the standby would have various replication objects like
publications, subscriptions, origins inherited from the upstream
server and possibly very much active. With failover slots, it might
inherit replication slots. Is it intended that the new subscriber also
acts as publisher for source's subscribers OR that the new subscriber
should subscribe to the upstreams of the source? Some use cases like
logical standby might require that but a multi-master multi-node setup
may not. The behaviour should be user configurable.

There may be other objects in this category which need special consideration on
the subscriber. I haven't fully thought through the list of such objects.

+ uses the replication slot that was created in a previous step. The
+ subscription is created but it is not enabled yet. The reason is the
+ replication progress must be set to the consistent LSN but replication
+ origin name contains the subscription oid in its name. Hence, the

Not able to understand the sentence "The reason is ... in its name".
Why is subscription OID in origin name matters?

+ <para>
+ <application>pg_subscriber</application> stops the target server to change
+ its system identifier.
+ </para>

I expected the subscriber to be started after this step.

Why do we need pg_resetwal?

+ appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+ appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");

Hardcoding output plugin name would limit this utility only to
built-in plugin. Any reason for that limitation?

In its current form the utility creates a logical subscriber which
subscribes to all the tables (and sequences when we have sequence
replication). But it will be useful even in case of selective
replication from a very large database. In such a case the new
subscriber will need to a. remove the unwanted objects b. subscriber
will need to subscribe to publications publishing the "interesting"
objects. We don't need to support this case, but the current
functionality (including the interface) and design shouldn't limit us
from doing so. Have you thought about this case?

I noticed some differences between this and a similar utility
https://github.com/2ndQuadrant/pglogical/blob/REL2_x_STABLE/pglogical_create_subscriber.c.
I will be reviewing these differences next to see if we are missing
anything here.

--
Best Wishes,
Ashutosh Bapat

#17Euler Taveira
euler@eulerto.com
In reply to: shihao zhong (#15)
Re: speed up a logical replica setup

On Tue, Oct 31, 2023, at 11:46 PM, shihao zhong wrote:

I think this is duplicate with https://commitfest.postgresql.org/45/4637/

The new status of this patch is: Waiting on Author

I withdrew the other entry.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#18Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Ashutosh Bapat (#16)
Re: speed up a logical replica setup

On Wed, Nov 1, 2023 at 7:10 PM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:

I noticed some differences between this and a similar utility
https://github.com/2ndQuadrant/pglogical/blob/REL2_x_STABLE/pglogical_create_subscriber.c.
I will be reviewing these differences next to see if we are missing
anything here.

Some more missing things to discuss

Handling signals - The utility cleans up left over objects on exit.
But default signal handlers will make the utility exit without a
proper cleanup [1]NOTEs section in man atexit().. The signal handlers may clean up the objects
themselves or at least report the objects that need tobe cleaned up.

Idempotent behaviour - Given that the utility will be used when very
large amount of data is involved, redoing everything after a network
glitch or a temporary failure should be avoided. This is true when the
users start with base backup. Again, I don't think we should try to be
idempotent in v1 but current design shouldn't stop us from doing so. I
didn't find anything like that in my review. But something to keep in
mind.

That finishes my first round of review. I will wait for your updated
patches before the next round.

[1]: NOTEs section in man atexit().

--
Best Wishes,
Ashutosh Bapat

#19Peter Eisentraut
peter@eisentraut.org
In reply to: Euler Taveira (#13)
Re: speed up a logical replica setup

On 23.10.23 05:53, Euler Taveira wrote:

Let's continue with the bike shedding... I agree with Peter E that this name
does not express what this tool is. At the moment, it only have one action:
create. If I have to suggest other actions I would say that it could support
switchover option too (that removes the infrastructure created by this
tool).
If we decide to keep this name, it should be a good idea to add an option to
indicate what action it is executing (similar to pg_recvlogical) as
suggested
by Peter.

Speaking of which, would it make sense to put this tool (whatever the
name) into the pg_basebackup directory? It's sort of related, and it
also shares some code.

#20Michael Paquier
michael@paquier.xyz
In reply to: Peter Eisentraut (#19)
Re: speed up a logical replica setup

On Tue, Nov 07, 2023 at 10:00:39PM +0100, Peter Eisentraut wrote:

Speaking of which, would it make sense to put this tool (whatever the name)
into the pg_basebackup directory? It's sort of related, and it also shares
some code.

I've read the patch, and the additions to streamutil.h and
streamutil.c make it kind of natural to have it sit in pg_basebackup/.
There's pg_recvlogical already there. I am wondering about two
things, though:
- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?
- And if it would be better to refactor some of the code generic to
all these streaming tools to fe_utils. What makes streamutil.h a bit
less pluggable are all its extern variables to control the connection,
but perhaps that can be an advantage, as well, in some cases.
--
Michael

#21Euler Taveira
euler@eulerto.com
In reply to: Michael Paquier (#20)
Re: speed up a logical replica setup

On Tue, Nov 7, 2023, at 8:12 PM, Michael Paquier wrote:

On Tue, Nov 07, 2023 at 10:00:39PM +0100, Peter Eisentraut wrote:

Speaking of which, would it make sense to put this tool (whatever the name)
into the pg_basebackup directory? It's sort of related, and it also shares
some code.

I used the CreateReplicationSlot() from streamutil.h but decided to use the
CREATE_REPLICATION_SLOT command directly because it needs the LSN as output. As
you noticed at that time I wouldn't like a dependency in the pg_basebackup
header files; if we move this binary to base backup directory, it seems natural
to refactor the referred function and use it.

I've read the patch, and the additions to streamutil.h and
streamutil.c make it kind of natural to have it sit in pg_basebackup/.
There's pg_recvlogical already there. I am wondering about two
things, though:
- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?

It is a good fit for this tool since it is another replication tool. I also
agree with the directory renaming; it seems confusing that the directory has
the same name as one binary but also contains other related binaries in it.

- And if it would be better to refactor some of the code generic to
all these streaming tools to fe_utils. What makes streamutil.h a bit
less pluggable are all its extern variables to control the connection,
but perhaps that can be an advantage, as well, in some cases.

I like it. There are common functions such as GetConnection(),
CreateReplicationSlot(), DropReplicationSlot() and RunIdentifySystem() that is
used by all of these replication tools. We can move the extern variables into
parameters to have a pluggable streamutil.h.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#22Michael Paquier
michael@paquier.xyz
In reply to: Euler Taveira (#21)
Re: speed up a logical replica setup

On Wed, Nov 08, 2023 at 09:50:47AM -0300, Euler Taveira wrote:

On Tue, Nov 7, 2023, at 8:12 PM, Michael Paquier wrote:
I used the CreateReplicationSlot() from streamutil.h but decided to use the
CREATE_REPLICATION_SLOT command directly because it needs the LSN as output. As
you noticed at that time I wouldn't like a dependency in the pg_basebackup
header files; if we move this binary to base backup directory, it seems natural
to refactor the referred function and use it.

Right. That should be OK to store that in an optional XLogRecPtr
pointer, aka by letting the option to pass NULL as argument of the
function if the caller needs nothing.

I've read the patch, and the additions to streamutil.h and
streamutil.c make it kind of natural to have it sit in pg_basebackup/.
There's pg_recvlogical already there. I am wondering about two
things, though:
- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?

It is a good fit for this tool since it is another replication tool. I also
agree with the directory renaming; it seems confusing that the directory has
the same name as one binary but also contains other related binaries in it.

Or cluster_tools? Or stream_tools? replication_tools may be OK, but
I have a bad sense in naming new things around here. So if anybody
has a better idea, feel free..

- And if it would be better to refactor some of the code generic to
all these streaming tools to fe_utils. What makes streamutil.h a bit
less pluggable are all its extern variables to control the connection,
but perhaps that can be an advantage, as well, in some cases.

I like it. There are common functions such as GetConnection(),
CreateReplicationSlot(), DropReplicationSlot() and RunIdentifySystem() that is
used by all of these replication tools. We can move the extern variables into
parameters to have a pluggable streamutil.h.

And perhaps RetrieveWalSegSize() as well as GetSlotInformation().
These kick replication commands.
--
Michael

#23Peter Eisentraut
peter@eisentraut.org
In reply to: Michael Paquier (#20)
Re: speed up a logical replica setup

On 08.11.23 00:12, Michael Paquier wrote:

- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?

Seems like unnecessary churn. Nobody has complained about any of the
other tools in there.

- And if it would be better to refactor some of the code generic to
all these streaming tools to fe_utils. What makes streamutil.h a bit
less pluggable are all its extern variables to control the connection,
but perhaps that can be an advantage, as well, in some cases.

Does anyone outside of pg_basebackup + existing friends + new friend
need that? Seems like extra complications.

#24Michael Paquier
michael@paquier.xyz
In reply to: Peter Eisentraut (#23)
Re: speed up a logical replica setup

On Thu, Nov 09, 2023 at 03:41:53PM +0100, Peter Eisentraut wrote:

On 08.11.23 00:12, Michael Paquier wrote:

- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?

Seems like unnecessary churn. Nobody has complained about any of the other
tools in there.

Not sure. We rename things across releases in the tree from time to
time, and here that's straight-forward.

- And if it would be better to refactor some of the code generic to
all these streaming tools to fe_utils. What makes streamutil.h a bit
less pluggable are all its extern variables to control the connection,
but perhaps that can be an advantage, as well, in some cases.

Does anyone outside of pg_basebackup + existing friends + new friend need
that? Seems like extra complications.

Actually, yes, I've used these utility routines in some past work, and
having the wrapper routines able to run the replication commands in
fe_utils would have been nicer than having to link to a source tree.
--
Michael

#25Euler Taveira
euler@eulerto.com
In reply to: Michael Paquier (#24)
1 attachment(s)
Re: speed up a logical replica setup

On Thu, Nov 9, 2023, at 8:12 PM, Michael Paquier wrote:

On Thu, Nov 09, 2023 at 03:41:53PM +0100, Peter Eisentraut wrote:

On 08.11.23 00:12, Michael Paquier wrote:

- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?

Seems like unnecessary churn. Nobody has complained about any of the other
tools in there.

Not sure. We rename things across releases in the tree from time to
time, and here that's straight-forward.

Based on this discussion it seems we have a consensus that this tool should be
in the pg_basebackup directory. (If/when we agree with the directory renaming,
it could be done in a separate patch.) Besides this move, the v3 provides a dry
run mode. It basically executes every routine but skip when should do
modifications. It is an useful option to check if you will be able to run it
without having issues with connectivity, permission, and existing objects
(replication slots, publications, subscriptions). Tests were slightly improved.
Messages were changed to *not* provide INFO messages by default and --verbose
provides INFO messages and --verbose --verbose also provides DEBUG messages. I
also refactored the connect_database() function into which the connection will
always use the logical replication mode. A bug was fixed in the transient
replication slot name. Ashutosh review [1]/messages/by-id/CAExHW5sCAU3NvPKd7msScQKvrBN-x_AdDQD-ZYAwOxuWG=oz1w@mail.gmail.com was included. The code was also indented.

There are a few suggestions from Ashutosh [2]/messages/by-id/CAExHW5vHFemFvTUHe+7XWphVZJxrEXz5H3dD4UQi7CwmdMJQYg@mail.gmail.com that I will reply in another
email.

I'm still planning to work on the following points:

1. improve the cleanup routine to point out leftover objects if there is any
connection issue.
2. remove the physical replication slot if the standby is using one
(primary_slot_name).
3. provide instructions to promote the logical replica into primary, I mean,
stop the replication between the nodes and remove the replication setup
(publications, subscriptions, replication slots). Or even include another
action to do it. We could add both too.

Point 1 should be done. Points 2 and 3 aren't essential but will provide a nice
UI for users that would like to use it.

[1]: /messages/by-id/CAExHW5sCAU3NvPKd7msScQKvrBN-x_AdDQD-ZYAwOxuWG=oz1w@mail.gmail.com
[2]: /messages/by-id/CAExHW5vHFemFvTUHe+7XWphVZJxrEXz5H3dD4UQi7CwmdMJQYg@mail.gmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v3-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchtext/x-patch; name="=?UTF-8?Q?v3-0001-Creates-a-new-logical-replica-from-a-standby-serv.patc?= =?UTF-8?Q?h?="Download
From 003255b64910ce73f15931a43def25c37be96b81 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v3] Creates a new logical replica from a standby server

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server. Remove
the additional replication slot that was used to get the consistent LSN.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_subscriber.sgml           |  284 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_subscriber.c         | 1517 +++++++++++++++++
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   44 +
 .../t/041_pg_subscriber_standby.pl            |  139 ++
 src/tools/msvc/Mkvcbuild.pm                   |    2 +-
 9 files changed, 2013 insertions(+), 2 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_subscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_subscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_subscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 54b5f22d6e..e2ecb4f944 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -213,6 +213,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgSubscriber       SYSTEM "pg_subscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
new file mode 100644
index 0000000000..553185c35f
--- /dev/null
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -0,0 +1,284 @@
+<!--
+doc/src/sgml/ref/pg_subscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgsubscriber">
+ <indexterm zone="app-pgsubscriber">
+  <primary>pg_subscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_subscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>create a new logical replica from a standby server</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_subscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a standby server and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_subscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a standby
+        server.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--publisher-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_subscriber</application> to output progress messages
+        and detailed information about each step.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_subscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_subscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_subscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the target data
+     directory is used by a standby server. Stop the standby server if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_subscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  Another
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_subscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_subscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_subscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_subscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> removes the additional replication
+     slot that was used to get the consistent LSN on the source server.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a standby server at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..67e257436b 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -257,6 +257,7 @@
    &pgReceivewal;
    &pgRecvlogical;
    &pgRestore;
+   &pgSubscriber;
    &pgVerifyBackup;
    &psqlRef;
    &reindexdb;
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index 74dc1ddd6d..bc011565bb 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -47,7 +47,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_subscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -58,10 +58,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_subscriber: $(WIN32RES) pg_subscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_subscriber$(X) '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -70,10 +74,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_subscriber$(X) pg_subscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index c426173db3..601517c096 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_subscriber_sources = files(
+  'pg_subscriber.c'
+)
+
+if host_system == 'windows'
+  pg_subscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_subscriber',
+	'--FILEDESC', 'pg_subscriber - create a new logical replica from a standby server',])
+endif
+
+pg_subscriber = executable('pg_subscriber',
+  pg_subscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_subscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_subscriber.pl',
+      't/041_pg_subscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
new file mode 100644
index 0000000000..b96ce26ed7
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -0,0 +1,1517 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_subscriber/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static bool create_all_logical_replication_slots(LogicalRepInfo *dbinfo);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static bool dry_run = false;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static char temp_replslot[NAMEDATALEN] = {0};
+static bool made_transient_replslot = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_subscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+
+	if (made_transient_replslot)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		disconnect_database(conn);
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-conninfo=CONNINFO   publisher connection string\n"));
+	printf(_(" -S, --subscriber-conninfo=CONNINFO  subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name plus a fallback application name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	if (i > 0)
+		appendPQExpBufferChar(buf, ' ');
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %ld on publisher", sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %ld on subscriber", sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %ld on subscriber", cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+static bool
+create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+{
+	int			i;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
+		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
+		 * to reduce the probability of collision. By default, subscription
+		 * name is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_subscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (transient_replslot)
+		made_transient_replslot = true;
+	else
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_subscriber must have created this
+		 * publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_subscriber_ prefix followed by the exact
+			 * database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-conninfo", required_argument, NULL, 'P'},
+		{"subscriber-conninfo", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+	int			rc;
+
+	char	   *pg_ctl_cmd;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	int			i;
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_subscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:t:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				simple_string_list_append(&database_names, optarg);
+				num_dbs++;
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * Stop the subscriber if it is a standby server. Before executing the
+	 * transformation steps, make sure the subscriber is not running because
+	 * one of the steps is to modify some recovery parameters that require a
+	 * restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		pg_log_info("subscriber is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+	}
+
+	/*
+	 * Create a replication slot for each database on the publisher.
+	 */
+	if (!create_all_logical_replication_slots(dbinfo))
+		exit(1);
+
+	/*
+	 * Create a logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. We cannot use the last created replication slot
+	 * because the consistent LSN should be obtained *after* the base backup
+	 * finishes (and the base backup should include the logical replication
+	 * slots).
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 *
+	 * A temporary replication slot is not used here to avoid keeping a
+	 * replication connection open (depending when base backup was taken, the
+	 * connection should be open for a few hours).
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create a publication for each database. This step should be executed
+	 * after promoting the subscriber to avoid replicating unnecessary
+	 * objects.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+
+		/* Connect to publisher. */
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		create_publication(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Create a subscription for each database.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * The transient replication slot is no longer required. Drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops the replication slot later.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+	{
+		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+	else
+	{
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
new file mode 100644
index 0000000000..9d20847dc2
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_subscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_subscriber');
+program_version_ok('pg_subscriber');
+program_options_handling_ok('pg_subscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_subscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+		'--subscriber-conninfo', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
new file mode 100644
index 0000000000..ce25608c68
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_subscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_subscriber on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_subscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm
index 46df01cc8d..68af923a29 100644
--- a/src/tools/msvc/Mkvcbuild.pm
+++ b/src/tools/msvc/Mkvcbuild.pm
@@ -55,7 +55,7 @@ my @contrib_excludes = (
 # Set of variables for frontend modules
 my $frontend_defines = { 'pgbench' => 'FD_SETSIZE=1024' };
 my @frontend_uselibpq =
-  ('pg_amcheck', 'pg_ctl', 'pg_upgrade', 'pgbench', 'psql', 'initdb');
+  ('pg_amcheck', 'pg_ctl', 'pg_upgrade', 'pgbench', 'psql', 'initdb', 'pg_subscriber');
 my @frontend_uselibpgport = (
 	'pg_amcheck', 'pg_archivecleanup',
 	'pg_test_fsync', 'pg_test_timing',
-- 
2.30.2

#26Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Euler Taveira (#25)
Re: speed up a logical replica setup

Hi,

On Wed, 6 Dec 2023 at 12:53, Euler Taveira <euler@eulerto.com> wrote:

On Thu, Nov 9, 2023, at 8:12 PM, Michael Paquier wrote:

On Thu, Nov 09, 2023 at 03:41:53PM +0100, Peter Eisentraut wrote:

On 08.11.23 00:12, Michael Paquier wrote:

- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?

Seems like unnecessary churn. Nobody has complained about any of the other
tools in there.

Not sure. We rename things across releases in the tree from time to
time, and here that's straight-forward.

Based on this discussion it seems we have a consensus that this tool should be
in the pg_basebackup directory. (If/when we agree with the directory renaming,
it could be done in a separate patch.) Besides this move, the v3 provides a dry
run mode. It basically executes every routine but skip when should do
modifications. It is an useful option to check if you will be able to run it
without having issues with connectivity, permission, and existing objects
(replication slots, publications, subscriptions). Tests were slightly improved.
Messages were changed to *not* provide INFO messages by default and --verbose
provides INFO messages and --verbose --verbose also provides DEBUG messages. I
also refactored the connect_database() function into which the connection will
always use the logical replication mode. A bug was fixed in the transient
replication slot name. Ashutosh review [1] was included. The code was also indented.

There are a few suggestions from Ashutosh [2] that I will reply in another
email.

I'm still planning to work on the following points:

1. improve the cleanup routine to point out leftover objects if there is any
connection issue.
2. remove the physical replication slot if the standby is using one
(primary_slot_name).
3. provide instructions to promote the logical replica into primary, I mean,
stop the replication between the nodes and remove the replication setup
(publications, subscriptions, replication slots). Or even include another
action to do it. We could add both too.

Point 1 should be done. Points 2 and 3 aren't essential but will provide a nice
UI for users that would like to use it.

[1] /messages/by-id/CAExHW5sCAU3NvPKd7msScQKvrBN-x_AdDQD-ZYAwOxuWG=oz1w@mail.gmail.com
[2] /messages/by-id/CAExHW5vHFemFvTUHe+7XWphVZJxrEXz5H3dD4UQi7CwmdMJQYg@mail.gmail.com

The changes in the file 'src/tools/msvc/Mkvcbuild.pm' seems
unnecessary as the folder 'msvc' is removed due to the commit [1]https://github.com/postgres/postgres/commit/1301c80b2167feb658a738fa4ceb1c23d0991e23.

To review the changes, I did 'git reset --hard' to the commit previous
to commit [1]https://github.com/postgres/postgres/commit/1301c80b2167feb658a738fa4ceb1c23d0991e23.
I tried to build the postgres on my Windows machine using two methods:
i. building using Visual Studio
ii. building using Meson

When I built the code using Visual Studio, on installing postgres,
pg_subscriber binary was not created.
But when I built the code using Meson, on installing postgres,
pg_subscriber binary was created.
Is this behaviour intentional?

[1]: https://github.com/postgres/postgres/commit/1301c80b2167feb658a738fa4ceb1c23d0991e23

Thanks and Regards,
Shlok Kyal

#27Euler Taveira
euler@eulerto.com
In reply to: Shlok Kyal (#26)
Re: speed up a logical replica setup

On Wed, Dec 20, 2023, at 9:22 AM, Shlok Kyal wrote:

When I built the code using Visual Studio, on installing postgres,
pg_subscriber binary was not created.
But when I built the code using Meson, on installing postgres,
pg_subscriber binary was created.
Is this behaviour intentional?

No. I will update the patch accordingly. I suspect that a fair amount of patches
broke due to MSVC change.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#28Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#25)
Re: speed up a logical replica setup

On Wed, Dec 6, 2023 at 12:53 PM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Nov 9, 2023, at 8:12 PM, Michael Paquier wrote:

On Thu, Nov 09, 2023 at 03:41:53PM +0100, Peter Eisentraut wrote:

On 08.11.23 00:12, Michael Paquier wrote:

- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?

Seems like unnecessary churn. Nobody has complained about any of the other
tools in there.

Not sure. We rename things across releases in the tree from time to
time, and here that's straight-forward.

Based on this discussion it seems we have a consensus that this tool should be
in the pg_basebackup directory. (If/when we agree with the directory renaming,
it could be done in a separate patch.) Besides this move, the v3 provides a dry
run mode. It basically executes every routine but skip when should do
modifications. It is an useful option to check if you will be able to run it
without having issues with connectivity, permission, and existing objects
(replication slots, publications, subscriptions). Tests were slightly improved.
Messages were changed to *not* provide INFO messages by default and --verbose
provides INFO messages and --verbose --verbose also provides DEBUG messages. I
also refactored the connect_database() function into which the connection will
always use the logical replication mode. A bug was fixed in the transient
replication slot name. Ashutosh review [1] was included. The code was also indented.

There are a few suggestions from Ashutosh [2] that I will reply in another
email.

I'm still planning to work on the following points:

1. improve the cleanup routine to point out leftover objects if there is any
connection issue.

I think this is an important part. Shall we try to write to some file
the pending objects to be cleaned up? We do something like that during
the upgrade.

2. remove the physical replication slot if the standby is using one
(primary_slot_name).
3. provide instructions to promote the logical replica into primary, I mean,
stop the replication between the nodes and remove the replication setup
(publications, subscriptions, replication slots). Or even include another
action to do it. We could add both too.

Point 1 should be done. Points 2 and 3 aren't essential but will provide a nice
UI for users that would like to use it.

Isn't point 2 also essential because how would otherwise such a slot
be advanced or removed?

A few other points:
==============
1. Previously, I asked whether we need an additional replication slot
patch created to get consistent LSN and I see the following comment in
the patch:

+ *
+ * XXX we should probably use the last created replication slot to get a
+ * consistent LSN but it should be changed after adding pg_basebackup
+ * support.

Yeah, sure, we may want to do that after backup support and we can
keep a comment for the same but I feel as the patch stands today,
there is no good reason to keep it. Also, is there a reason that we
can't create the slots after backup is complete and before we write
recovery parameters

2.
+ appendPQExpBuffer(str,
+   "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+   "WITH (create_slot = false, copy_data = false, enabled = false)",
+   dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);

Shouldn't we enable two_phase by default for newly created
subscriptions? Is there a reason for not doing so?

3. How about sync slots on the physical standby if present? Do we want
to retain those as it is or do we need to remove those? We are
actively working on the patch [1]/messages/by-id/OS0PR01MB5716DAF72265388A2AD424119495A@OS0PR01MB5716.jpnprd01.prod.outlook.com for the same.

4. Can we see some numbers with various sizes of databases (cluster)
to see how it impacts the time for small to large-size databases as
compared to the traditional method? This might help us with giving
users advice on when to use this tool. We can do this bit later as
well when the patch is closer to being ready for commit.

[1]: /messages/by-id/OS0PR01MB5716DAF72265388A2AD424119495A@OS0PR01MB5716.jpnprd01.prod.outlook.com

--
With Regards,
Amit Kapila.

#29Amit Kapila
amit.kapila16@gmail.com
In reply to: Ashutosh Bapat (#16)
Re: speed up a logical replica setup

On Wed, Nov 1, 2023 at 7:10 PM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:

Here are some comments about functionality and design.

+ <step>
+ <para>
+ <application>pg_subscriber</application> creates one replication slot for
+ each specified database on the source server. The replication slot name
+ contains a <literal>pg_subscriber</literal> prefix. These replication
+ slots will be used by the subscriptions in a future step. Another
+ replication slot is used to get a consistent start location. This
+ consistent LSN will be used as a stopping point in the <xref
+ linkend="guc-recovery-target-lsn"/> parameter and by the
+ subscriptions as a replication starting point. It guarantees that no
+ transaction will be lost.
+ </para>
+ </step>

CREATE_REPLICATION_SLOT would wait for any incomplete transaction to
complete. So it may not be possible to have an incomplete transaction
on standby when it comes out of recovery. Am I correct? Can we please
have a testcase where we test this scenario? What about a prepared
transactions?

It will wait even for prepared transactions to commit. So, there
shouldn't be any behavior difference for prepared and non-prepared
transactions.

+
+ <step>
+ <para>
+ <application>pg_subscriber</application> writes recovery parameters into
+ the target data directory and start the target server. It specifies a LSN
+ (consistent LSN that was obtained in the previous step) of write-ahead
+ log location up to which recovery will proceed. It also specifies
+ <literal>promote</literal> as the action that the server should take once
+ the recovery target is reached. This step finishes once the server ends
+ standby mode and is accepting read-write operations.
+ </para>
+ </step>

At this stage the standby would have various replication objects like
publications, subscriptions, origins inherited from the upstream
server and possibly very much active. With failover slots, it might
inherit replication slots. Is it intended that the new subscriber also
acts as publisher for source's subscribers OR that the new subscriber
should subscribe to the upstreams of the source? Some use cases like
logical standby might require that but a multi-master multi-node setup
may not. The behaviour should be user configurable.

Good points but even if we make it user configurable how to exclude
such replication objects? And if we don't exclude then what will be
their use because if one wants to use it as a logical standby then we
only need publications and failover/sync slots in it and also there
won't be a need to create new slots, publications on the primary to
make the current physical standby as logical subscriber.

There may be other objects in this category which need special consideration on
the subscriber. I haven't fully thought through the list of such objects.

+ uses the replication slot that was created in a previous step. The
+ subscription is created but it is not enabled yet. The reason is the
+ replication progress must be set to the consistent LSN but replication
+ origin name contains the subscription oid in its name. Hence, the

Not able to understand the sentence "The reason is ... in its name".
Why is subscription OID in origin name matters?

Using subscription OID in origin is probably to uniquely identify the
origin corresponding to the subscription, we do that while creating a
subscription as well.

--
With Regards,
Amit Kapila.

#30vignesh C
vignesh21@gmail.com
In reply to: Euler Taveira (#25)
Re: speed up a logical replica setup

On Wed, 6 Dec 2023 at 12:53, Euler Taveira <euler@eulerto.com> wrote:

On Thu, Nov 9, 2023, at 8:12 PM, Michael Paquier wrote:

On Thu, Nov 09, 2023 at 03:41:53PM +0100, Peter Eisentraut wrote:

On 08.11.23 00:12, Michael Paquier wrote:

- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?

Seems like unnecessary churn. Nobody has complained about any of the other
tools in there.

Not sure. We rename things across releases in the tree from time to
time, and here that's straight-forward.

Based on this discussion it seems we have a consensus that this tool should be
in the pg_basebackup directory. (If/when we agree with the directory renaming,
it could be done in a separate patch.) Besides this move, the v3 provides a dry
run mode. It basically executes every routine but skip when should do
modifications. It is an useful option to check if you will be able to run it
without having issues with connectivity, permission, and existing objects
(replication slots, publications, subscriptions). Tests were slightly improved.
Messages were changed to *not* provide INFO messages by default and --verbose
provides INFO messages and --verbose --verbose also provides DEBUG messages. I
also refactored the connect_database() function into which the connection will
always use the logical replication mode. A bug was fixed in the transient
replication slot name. Ashutosh review [1] was included. The code was also indented.

There are a few suggestions from Ashutosh [2] that I will reply in another
email.

I'm still planning to work on the following points:

1. improve the cleanup routine to point out leftover objects if there is any
connection issue.
2. remove the physical replication slot if the standby is using one
(primary_slot_name).
3. provide instructions to promote the logical replica into primary, I mean,
stop the replication between the nodes and remove the replication setup
(publications, subscriptions, replication slots). Or even include another
action to do it. We could add both too.

Point 1 should be done. Points 2 and 3 aren't essential but will provide a nice
UI for users that would like to use it.

1) This Assert can fail if source is shutdown:
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const
char *slot_name)
+{
+       PQExpBuffer str = createPQExpBuffer();
+       PGresult   *res;
+
+       Assert(conn != NULL);

I could simulate it by shutting the primary while trying to reach the
consistent state:
pg_subscriber: postmaster reached the consistent state
pg_subscriber: error: connection to database failed: connection to
server at "localhost" (127.0.0.1), port 5432 failed: Connection
refused
Is the server running on that host and accepting TCP/IP connections?
pg_subscriber: error: connection to database failed: connection to
server at "localhost" (127.0.0.1), port 5432 failed: Connection
refused
Is the server running on that host and accepting TCP/IP connections?
pg_subscriber: error: connection to database failed: connection to
server at "localhost" (127.0.0.1), port 5432 failed: Connection
refused
Is the server running on that host and accepting TCP/IP connections?
pg_subscriber: pg_subscriber.c:692: drop_replication_slot: Assertion
`conn != ((void *)0)' failed.
Aborted

2) Should we have some checks to see if the max replication slot
configuration is ok based on the number of slots that will be created,
we have similar checks in upgrade replication slots in
check_new_cluster_logical_replication_slots

3) Should we check if wal_level is set to logical, we have similar
checks in upgrade replication slots in
check_new_cluster_logical_replication_slots

4) The physical replication slot that was created will still be
present in the primary node, I felt this should be removed.

5) I felt the target server should be started before completion of
pg_subscriber:
+       /*
+        * Stop the subscriber.
+        */
+       pg_log_info("stopping the subscriber");
+
+       pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
subscriber_dir);
+       rc = system(pg_ctl_cmd);
+       pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+       /*
+        * Change system identifier.
+        */
+       modify_sysid(pg_resetwal_path, subscriber_dir);
+
+       success = true;
+
+       pg_log_info("Done!");
+
+       return 0;

Regards,
Vignesh

#31vignesh C
vignesh21@gmail.com
In reply to: Ashutosh Bapat (#16)
Re: speed up a logical replica setup

On Wed, 1 Nov 2023 at 19:28, Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:

At this stage the standby would have various replication objects like
publications, subscriptions, origins inherited from the upstream
server and possibly very much active. With failover slots, it might
inherit replication slots. Is it intended that the new subscriber also
acts as publisher for source's subscribers OR that the new subscriber
should subscribe to the upstreams of the source? Some use cases like
logical standby might require that but a multi-master multi-node setup
may not. The behaviour should be user configurable.

How about we do like this:
a) Starting the server in binary upgrade mode(so that the existing
subscriptions will not try to connect to the publishers) b) Disable
the subscriptions c) Drop the replication slots d) Drop the
publications e) Then restart the server in normal(non-upgrade) mode.
f) The rest of pg_subscriber work like
create_all_logical_replication_slots, create_subscription,
set_replication_progress, enable_subscription, etc
This will be done by default. There will be an option
--clean-logical-replication-info provided to allow DBA not to clean
the objects if DBA does not want to remove these objects.
I felt cleaning the replication information is better as a) Node-1
will replicate all the data to Node-2 (that Node-1 is subscribing to
from other nodes) after pg_subscriber setup is done. b) all the data
that Node-1 is publishing need not be published again by Node-2. There
is an option to override if the user does not want to remove the
logical replication objects.

Regards,
Vignesh

#32Amit Kapila
amit.kapila16@gmail.com
In reply to: vignesh C (#31)
Re: speed up a logical replica setup

On Wed, Jan 3, 2024 at 12:09 PM vignesh C <vignesh21@gmail.com> wrote:

On Wed, 1 Nov 2023 at 19:28, Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:

At this stage the standby would have various replication objects like
publications, subscriptions, origins inherited from the upstream
server and possibly very much active. With failover slots, it might
inherit replication slots. Is it intended that the new subscriber also
acts as publisher for source's subscribers OR that the new subscriber
should subscribe to the upstreams of the source? Some use cases like
logical standby might require that but a multi-master multi-node setup
may not. The behaviour should be user configurable.

How about we do like this:
a) Starting the server in binary upgrade mode(so that the existing
subscriptions will not try to connect to the publishers)

Can't we simply do it by starting the server with
max_logical_replication_workers = 0 or is there some other need to
start in binary upgrade mode?

b) Disable

the subscriptions

Why not simply drop the subscriptions?

c) Drop the replication slots d) Drop the

publications

I am not so sure about dropping publications because, unlike
subscriptions which can start to pull the data, there is no harm with
publications. Similar to publications there could be some user-defined
functions or other other objects which may not be required once the
standby replica is converted to subscriber. I guess we need to leave
those to the user.

e) Then restart the server in normal(non-upgrade) mode.

f) The rest of pg_subscriber work like
create_all_logical_replication_slots, create_subscription,
set_replication_progress, enable_subscription, etc
This will be done by default. There will be an option
--clean-logical-replication-info provided to allow DBA not to clean
the objects if DBA does not want to remove these objects.

I agree that some kind of switch to control this action would be useful.

--
With Regards,
Amit Kapila.

#33vignesh C
vignesh21@gmail.com
In reply to: Amit Kapila (#32)
Re: speed up a logical replica setup

On Wed, 3 Jan 2024 at 14:49, Amit Kapila <amit.kapila16@gmail.com> wrote:

On Wed, Jan 3, 2024 at 12:09 PM vignesh C <vignesh21@gmail.com> wrote:

On Wed, 1 Nov 2023 at 19:28, Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:

At this stage the standby would have various replication objects like
publications, subscriptions, origins inherited from the upstream
server and possibly very much active. With failover slots, it might
inherit replication slots. Is it intended that the new subscriber also
acts as publisher for source's subscribers OR that the new subscriber
should subscribe to the upstreams of the source? Some use cases like
logical standby might require that but a multi-master multi-node setup
may not. The behaviour should be user configurable.

How about we do like this:
a) Starting the server in binary upgrade mode(so that the existing
subscriptions will not try to connect to the publishers)

Can't we simply do it by starting the server with
max_logical_replication_workers = 0 or is there some other need to
start in binary upgrade mode?

I agree, max_logical_replication_workers = 0 is enough for our case.

b) Disable

the subscriptions

Why not simply drop the subscriptions?

Dropping subscriptions is ok as these subscriptions will not be required.

c) Drop the replication slots d) Drop the

publications

I am not so sure about dropping publications because, unlike
subscriptions which can start to pull the data, there is no harm with
publications. Similar to publications there could be some user-defined
functions or other other objects which may not be required once the
standby replica is converted to subscriber. I guess we need to leave
those to the user.

Yes, that makes sense.

Regards,
Vignesh

#34Euler Taveira
euler@eulerto.com
In reply to: Amit Kapila (#28)
Re: speed up a logical replica setup

On Thu, Dec 21, 2023, at 3:16 AM, Amit Kapila wrote:

I think this is an important part. Shall we try to write to some file
the pending objects to be cleaned up? We do something like that during
the upgrade.

That's a good idea.

2. remove the physical replication slot if the standby is using one
(primary_slot_name).
3. provide instructions to promote the logical replica into primary, I mean,
stop the replication between the nodes and remove the replication setup
(publications, subscriptions, replication slots). Or even include another
action to do it. We could add both too.

Point 1 should be done. Points 2 and 3 aren't essential but will provide a nice
UI for users that would like to use it.

Isn't point 2 also essential because how would otherwise such a slot
be advanced or removed?

I'm worried about a scenario that you will still use the primary. (Let's say
the logical replica will be promoted to a staging or dev server.) No connection
between primary and this new server so the primary slot is useless after the
promotion.

A few other points:
==============
1. Previously, I asked whether we need an additional replication slot
patch created to get consistent LSN and I see the following comment in
the patch:

+ *
+ * XXX we should probably use the last created replication slot to get a
+ * consistent LSN but it should be changed after adding pg_basebackup
+ * support.

Yeah, sure, we may want to do that after backup support and we can
keep a comment for the same but I feel as the patch stands today,
there is no good reason to keep it.

I'll remove the comment to avoid confusing.

Also, is there a reason that we
can't create the slots after backup is complete and before we write
recovery parameters

No.

2.
+ appendPQExpBuffer(str,
+   "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+   "WITH (create_slot = false, copy_data = false, enabled = false)",
+   dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);

Shouldn't we enable two_phase by default for newly created
subscriptions? Is there a reason for not doing so?

Why? I decided to keep the default for some settings (streaming,
synchronous_commit, two_phase, disable_on_error). Unless there is a compelling
reason to enable it, I think we should use the default. Either way, data will
arrive on subscriber as soon as the prepared transaction is committed.

3. How about sync slots on the physical standby if present? Do we want
to retain those as it is or do we need to remove those? We are
actively working on the patch [1] for the same.

I didn't read the current version of the referred patch but if the proposal is
to synchronize logical replication slots iif you are using a physical
replication, as soon as pg_subscriber finishes the execution, there won't be
synchronization on these logical replication slots because there isn't a
physical replication anymore. If the goal is a promotion, the current behavior
is correct because the logical replica will retain WAL since it was converted.
However, if you are creating a logical replica, this WAL retention is not good
and the customer should eventually remove these logical replication slots on
the logical replica.

4. Can we see some numbers with various sizes of databases (cluster)
to see how it impacts the time for small to large-size databases as
compared to the traditional method? This might help us with giving
users advice on when to use this tool. We can do this bit later as
well when the patch is closer to being ready for commit.

I'll share it.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#35Euler Taveira
euler@eulerto.com
In reply to: vignesh C (#30)
Re: speed up a logical replica setup

On Mon, Jan 1, 2024, at 7:14 AM, vignesh C wrote:

1) This Assert can fail if source is shutdown:
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const
char *slot_name)
+{
+       PQExpBuffer str = createPQExpBuffer();
+       PGresult   *res;
+
+       Assert(conn != NULL);

Oops. I'll remove it.

2) Should we have some checks to see if the max replication slot
configuration is ok based on the number of slots that will be created,
we have similar checks in upgrade replication slots in
check_new_cluster_logical_replication_slots

That's a good idea.

3) Should we check if wal_level is set to logical, we have similar
checks in upgrade replication slots in
check_new_cluster_logical_replication_slots

That's a good idea.

4) The physical replication slot that was created will still be
present in the primary node, I felt this should be removed.

My proposal is to remove it [1]/messages/by-id/e02a2c17-22e5-4ba6-b788-de696ab74f1e@app.fastmail.com. It'll be include in the next version.

5) I felt the target server should be started before completion of
pg_subscriber:

Why? The initial version had an option to stop the subscriber. I decided to
remove the option and stop the subscriber by default mainly because (1) it is
an extra step to start the server (another point is that the WAL retention
doesn't happen due to additional (synchronized?) replication slots on
subscriber -- point 2). It was a conservative choice. If point 2 isn't an
issue, imo point 1 is no big deal.

[1]: /messages/by-id/e02a2c17-22e5-4ba6-b788-de696ab74f1e@app.fastmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

#36Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#34)
Re: speed up a logical replica setup

On Thu, Jan 4, 2024 at 8:24 AM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Dec 21, 2023, at 3:16 AM, Amit Kapila wrote:

2. remove the physical replication slot if the standby is using one
(primary_slot_name).
3. provide instructions to promote the logical replica into primary, I mean,
stop the replication between the nodes and remove the replication setup
(publications, subscriptions, replication slots). Or even include another
action to do it. We could add both too.

Point 1 should be done. Points 2 and 3 aren't essential but will provide a nice
UI for users that would like to use it.

Isn't point 2 also essential because how would otherwise such a slot
be advanced or removed?

I'm worried about a scenario that you will still use the primary. (Let's say
the logical replica will be promoted to a staging or dev server.) No connection
between primary and this new server so the primary slot is useless after the
promotion.

So, you also seem to be saying that it is not required once
pg_subscriber has promoted it. So, why it should be optional to remove
physical_replication_slot? I think we must remove it from the primary
unless there is some other reason.

A few other points:
==============
1. Previously, I asked whether we need an additional replication slot
patch created to get consistent LSN and I see the following comment in
the patch:

+ *
+ * XXX we should probably use the last created replication slot to get a
+ * consistent LSN but it should be changed after adding pg_basebackup
+ * support.

Yeah, sure, we may want to do that after backup support and we can
keep a comment for the same but I feel as the patch stands today,
there is no good reason to keep it.

I'll remove the comment to avoid confusing.

My point is to not have an additional slot and keep a comment
indicating that we need an extra slot once we add pg_basebackup
support.

2.
+ appendPQExpBuffer(str,
+   "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+   "WITH (create_slot = false, copy_data = false, enabled = false)",
+   dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);

Shouldn't we enable two_phase by default for newly created
subscriptions? Is there a reason for not doing so?

Why? I decided to keep the default for some settings (streaming,
synchronous_commit, two_phase, disable_on_error). Unless there is a compelling
reason to enable it, I think we should use the default. Either way, data will
arrive on subscriber as soon as the prepared transaction is committed.

I thought we could provide a better experience for logical replicas
created by default but I see your point and probably keeping default
values for parameters you mentioned seems reasonable to me.

3. How about sync slots on the physical standby if present? Do we want
to retain those as it is or do we need to remove those? We are
actively working on the patch [1] for the same.

I didn't read the current version of the referred patch but if the proposal is
to synchronize logical replication slots iif you are using a physical
replication, as soon as pg_subscriber finishes the execution, there won't be
synchronization on these logical replication slots because there isn't a
physical replication anymore. If the goal is a promotion, the current behavior
is correct because the logical replica will retain WAL since it was converted.

I don't understand what you mean by promotion in this context. If
users want to simply promote the standby then there is no need to do
additional things that this tool is doing.

However, if you are creating a logical replica, this WAL retention is not good
and the customer should eventually remove these logical replication slots on
the logical replica.

I think asking users to manually remove such slots won't be a good
idea. We might want to either remove them by default or provide an
option to the user.

--
With Regards,
Amit Kapila.

#37Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#35)
Re: speed up a logical replica setup

On Thu, Jan 4, 2024 at 8:52 AM Euler Taveira <euler@eulerto.com> wrote:

On Mon, Jan 1, 2024, at 7:14 AM, vignesh C wrote:

5) I felt the target server should be started before completion of
pg_subscriber:

Why?

Won't it be a better user experience that after setting up the target
server as a logical replica (subscriber), it started to work
seamlessly without user intervention?

The initial version had an option to stop the subscriber. I decided to
remove the option and stop the subscriber by default mainly because (1) it is
an extra step to start the server (another point is that the WAL retention
doesn't happen due to additional (synchronized?) replication slots on
subscriber -- point 2). It was a conservative choice. If point 2 isn't an
issue, imo point 1 is no big deal.

By point 2, do you mean to have a check for "max replication slots"?
It so, the one possibility is to even increase that config, if the
required max_replication_slots is low.

--
With Regards,
Amit Kapila.

#38Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Amit Kapila (#37)
Re: speed up a logical replica setup

Hi,
I was testing the patch with following test cases:

Test 1 :
- Create a 'primary' node
- Setup physical replica using pg_basebackup "./pg_basebackup –h
localhost –X stream –v –R –W –D ../standby "
- Insert data before and after pg_basebackup
- Run pg_subscriber and then insert some data to check logical
replication "./pg_subscriber –D ../standby -S “host=localhost
port=9000 dbname=postgres” -P “host=localhost port=9000
dbname=postgres” -d postgres"
- Also check pg_publication, pg_subscriber and pg_replication_slots tables.

Observation:
Data is not lost. Replication is happening correctly. Pg_subscriber is
working as expected.

Test 2:
- Create a 'primary' node
- Use normal pg_basebackup but don’t set up Physical replication
"./pg_basebackup –h localhost –v –W –D ../standby"
- Insert data before and after pg_basebackup
- Run pg_subscriber

Observation:
Pg_subscriber command is not completing and is stuck with following
log repeating:
LOG: waiting for WAL to become available at 0/3000168
LOG: invalid record length at 0/3000150: expected at least 24, got 0

Test 3:
- Create a 'primary' node
- Use normal pg_basebackup but don’t set up Physical replication
"./pg_basebackup –h localhost –v –W –D ../standby"
-Insert data before pg_basebackup but not after pg_basebackup
-Run pg_subscriber

Observation:
Pg_subscriber command is not completing and is stuck with following
log repeating:
LOG: waiting for WAL to become available at 0/3000168
LOG: invalid record length at 0/3000150: expected at least 24, got 0

I was not clear about how to use pg_basebackup in this case, can you
let me know if any changes need to be made for test2 and test3.

Thanks and regards
Shlok Kyal

#39Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Amit Kapila (#32)
Re: speed up a logical replica setup

On Wed, Jan 3, 2024 at 2:49 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

c) Drop the replication slots d) Drop the

publications

I am not so sure about dropping publications because, unlike
subscriptions which can start to pull the data, there is no harm with
publications. Similar to publications there could be some user-defined
functions or other other objects which may not be required once the
standby replica is converted to subscriber. I guess we need to leave
those to the user.

IIUC, primary use of pg_subscriber utility is to start a logical
subscription from a physical base backup (to reduce initial sync time)
as against logical backup taken while creating a subscription. Hence I
am expecting that apart from this difference, the resultant logical
replica should look similar to the logical replica setup using a
logical subscription sync. Hence we should not leave any replication
objects around. UDFs (views, and other objects) may have some use on a
logical replica. We may replicate changes to UDF once DDL replication
is supported. But what good is having the same publications as primary
also on logical replica?

--
Best Wishes,
Ashutosh Bapat

#40Amit Kapila
amit.kapila16@gmail.com
In reply to: Ashutosh Bapat (#39)
Re: speed up a logical replica setup

On Thu, Jan 4, 2024 at 12:30 PM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:

On Wed, Jan 3, 2024 at 2:49 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

c) Drop the replication slots d) Drop the

publications

I am not so sure about dropping publications because, unlike
subscriptions which can start to pull the data, there is no harm with
publications. Similar to publications there could be some user-defined
functions or other other objects which may not be required once the
standby replica is converted to subscriber. I guess we need to leave
those to the user.

IIUC, primary use of pg_subscriber utility is to start a logical
subscription from a physical base backup (to reduce initial sync time)
as against logical backup taken while creating a subscription. Hence I
am expecting that apart from this difference, the resultant logical
replica should look similar to the logical replica setup using a
logical subscription sync. Hence we should not leave any replication
objects around. UDFs (views, and other objects) may have some use on a
logical replica. We may replicate changes to UDF once DDL replication
is supported. But what good is having the same publications as primary
also on logical replica?

The one use case that comes to my mind is to set up bi-directional
replication. The publishers want to subscribe to the new subscriber.

--
With Regards,
Amit Kapila.

#41Amit Kapila
amit.kapila16@gmail.com
In reply to: Shlok Kyal (#38)
Re: speed up a logical replica setup

On Thu, Jan 4, 2024 at 12:22 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

Hi,
I was testing the patch with following test cases:

Test 1 :
- Create a 'primary' node
- Setup physical replica using pg_basebackup "./pg_basebackup –h
localhost –X stream –v –R –W –D ../standby "
- Insert data before and after pg_basebackup
- Run pg_subscriber and then insert some data to check logical
replication "./pg_subscriber –D ../standby -S “host=localhost
port=9000 dbname=postgres” -P “host=localhost port=9000
dbname=postgres” -d postgres"
- Also check pg_publication, pg_subscriber and pg_replication_slots tables.

Observation:
Data is not lost. Replication is happening correctly. Pg_subscriber is
working as expected.

Test 2:
- Create a 'primary' node
- Use normal pg_basebackup but don’t set up Physical replication
"./pg_basebackup –h localhost –v –W –D ../standby"
- Insert data before and after pg_basebackup
- Run pg_subscriber

Observation:
Pg_subscriber command is not completing and is stuck with following
log repeating:
LOG: waiting for WAL to become available at 0/3000168
LOG: invalid record length at 0/3000150: expected at least 24, got 0

I think probably the required WAL is not copied. Can you use the -X
option to stream WAL as well and then test? But I feel in this case
also, we should wait for some threshold time and then exit with
failure, removing new objects created, if any.

Test 3:
- Create a 'primary' node
- Use normal pg_basebackup but don’t set up Physical replication
"./pg_basebackup –h localhost –v –W –D ../standby"
-Insert data before pg_basebackup but not after pg_basebackup
-Run pg_subscriber

Observation:
Pg_subscriber command is not completing and is stuck with following
log repeating:
LOG: waiting for WAL to become available at 0/3000168
LOG: invalid record length at 0/3000150: expected at least 24, got 0

This is similar to the previous test and you can try the same option
here as well.

--
With Regards,
Amit Kapila.

#42Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Amit Kapila (#40)
Re: speed up a logical replica setup

On Thu, Jan 4, 2024 at 4:34 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

But what good is having the same publications as primary
also on logical replica?

The one use case that comes to my mind is to set up bi-directional
replication. The publishers want to subscribe to the new subscriber.

Hmm. Looks like another user controlled cleanup.

--
Best Wishes,
Ashutosh Bapat

#43Euler Taveira
euler@eulerto.com
In reply to: Amit Kapila (#36)
Re: speed up a logical replica setup

On Thu, Jan 4, 2024, at 2:41 AM, Amit Kapila wrote:

So, you also seem to be saying that it is not required once
pg_subscriber has promoted it. So, why it should be optional to remove
physical_replication_slot? I think we must remove it from the primary
unless there is some other reason.

My point is to *always* remove the primary_slot_name on primary.

My point is to not have an additional slot and keep a comment
indicating that we need an extra slot once we add pg_basebackup
support.

Got it.

3. How about sync slots on the physical standby if present? Do we want
to retain those as it is or do we need to remove those? We are
actively working on the patch [1] for the same.

I didn't read the current version of the referred patch but if the proposal is
to synchronize logical replication slots iif you are using a physical
replication, as soon as pg_subscriber finishes the execution, there won't be
synchronization on these logical replication slots because there isn't a
physical replication anymore. If the goal is a promotion, the current behavior
is correct because the logical replica will retain WAL since it was converted.

I don't understand what you mean by promotion in this context. If
users want to simply promote the standby then there is no need to do
additional things that this tool is doing.

ENOCOFFEE. s/promotion/switchover/

However, if you are creating a logical replica, this WAL retention is not good
and the customer should eventually remove these logical replication slots on
the logical replica.

I think asking users to manually remove such slots won't be a good
idea. We might want to either remove them by default or provide an
option to the user.

Am I correct that the majority of the use cases these replication slots will be
useless? If so, let's remove them by default and add an option to control this
behavior (replication slot removal).

--
Euler Taveira
EDB https://www.enterprisedb.com/

#44Euler Taveira
euler@eulerto.com
In reply to: Amit Kapila (#37)
Re: speed up a logical replica setup

On Thu, Jan 4, 2024, at 3:05 AM, Amit Kapila wrote:

Won't it be a better user experience that after setting up the target
server as a logical replica (subscriber), it started to work
seamlessly without user intervention?

If we have an option to control the replication slot removal (default is on),
it seems a good UI. Even if the user decides to disable the replication slot
removal, it should print a message saying that these replication slots can
cause WAL retention.

The initial version had an option to stop the subscriber. I decided to
remove the option and stop the subscriber by default mainly because (1) it is
an extra step to start the server (another point is that the WAL retention
doesn't happen due to additional (synchronized?) replication slots on
subscriber -- point 2). It was a conservative choice. If point 2 isn't an
issue, imo point 1 is no big deal.

By point 2, do you mean to have a check for "max replication slots"?
It so, the one possibility is to even increase that config, if the
required max_replication_slots is low.

By point 2, I mean WAL retention (sentence inside parenthesis).

--
Euler Taveira
EDB https://www.enterprisedb.com/

#45Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#43)
Re: speed up a logical replica setup

On Thu, Jan 4, 2024 at 9:18 PM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Jan 4, 2024, at 2:41 AM, Amit Kapila wrote:

I think asking users to manually remove such slots won't be a good
idea. We might want to either remove them by default or provide an
option to the user.

Am I correct that the majority of the use cases these replication slots will be
useless?

I am not so sure about it. Say, if some sync slots are present this
means the user wants this replica to be used later as a publisher.
Now, if the existing primary/publisher node is still alive then we
don't have these slots but if the user wants to switch over to this
new node as well then they may be required.

Is there a possibility that a cascading standby also has a slot on the
current physical replica being converted to a new subscriber?

If so, let's remove them by default and add an option to control this
behavior (replication slot removal).

The presence of slots on the physical replica indicates that the other
nodes/clusters could be dependent on it, so, I feel by default we
should give an error and if the user uses some option to remove slots
then it is fine to remove them.

--
With Regards,
Amit Kapila.

#46Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#44)
Re: speed up a logical replica setup

On Thu, Jan 4, 2024 at 9:27 PM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Jan 4, 2024, at 3:05 AM, Amit Kapila wrote:

Won't it be a better user experience that after setting up the target
server as a logical replica (subscriber), it started to work
seamlessly without user intervention?

If we have an option to control the replication slot removal (default is on),
it seems a good UI. Even if the user decides to disable the replication slot
removal, it should print a message saying that these replication slots can
cause WAL retention.

As pointed out in the previous response, I think we should not proceed
with such a risk of WAL retention and other nodes dependency, we
should either give an ERROR (default) or remove slots, if the user
provides an option. If we do so, do you think by default we can keep
the server started or let the user start it later? I think one
advantage of letting the user start it later is that she gets a chance
to adjust config parameters in postgresql.conf and by default we won't
be using system resources.

--
With Regards,
Amit Kapila.

#47Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Amit Kapila (#41)
Re: speed up a logical replica setup

On Thu, 4 Jan 2024 at 16:46, Amit Kapila <amit.kapila16@gmail.com> wrote:

On Thu, Jan 4, 2024 at 12:22 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

Hi,
I was testing the patch with following test cases:

Test 1 :
- Create a 'primary' node
- Setup physical replica using pg_basebackup "./pg_basebackup –h
localhost –X stream –v –R –W –D ../standby "
- Insert data before and after pg_basebackup
- Run pg_subscriber and then insert some data to check logical
replication "./pg_subscriber –D ../standby -S “host=localhost
port=9000 dbname=postgres” -P “host=localhost port=9000
dbname=postgres” -d postgres"
- Also check pg_publication, pg_subscriber and pg_replication_slots tables.

Observation:
Data is not lost. Replication is happening correctly. Pg_subscriber is
working as expected.

Test 2:
- Create a 'primary' node
- Use normal pg_basebackup but don’t set up Physical replication
"./pg_basebackup –h localhost –v –W –D ../standby"
- Insert data before and after pg_basebackup
- Run pg_subscriber

Observation:
Pg_subscriber command is not completing and is stuck with following
log repeating:
LOG: waiting for WAL to become available at 0/3000168
LOG: invalid record length at 0/3000150: expected at least 24, got 0

I think probably the required WAL is not copied. Can you use the -X
option to stream WAL as well and then test? But I feel in this case
also, we should wait for some threshold time and then exit with
failure, removing new objects created, if any.

I have tested with -X stream option in pg_basebackup as well. In this
case also the pg_subscriber command is getting stuck.
logs:
2024-01-05 11:49:34.436 IST [61948] LOG: invalid resource manager ID
102 at 0/3000118
2024-01-05 11:49:34.436 IST [61948] LOG: waiting for WAL to become
available at 0/3000130

Test 3:
- Create a 'primary' node
- Use normal pg_basebackup but don’t set up Physical replication
"./pg_basebackup –h localhost –v –W –D ../standby"
-Insert data before pg_basebackup but not after pg_basebackup
-Run pg_subscriber

Observation:
Pg_subscriber command is not completing and is stuck with following
log repeating:
LOG: waiting for WAL to become available at 0/3000168
LOG: invalid record length at 0/3000150: expected at least 24, got 0

This is similar to the previous test and you can try the same option
here as well.

For this test as well tried with -X stream option in pg_basebackup.
It is getting stuck here as well with similar log.

Will investigate the issue further.

Thanks and regards
Shlok Kyal

#48Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#25)
RE: speed up a logical replica setup

Dear Euler,

I love your proposal, so I want to join the review. Here are my first comments.

01.
Should we restrict that `--subscriber-conninfo` must not have hostname or IP?
We want users to execute pg_subscriber on the target, right?

02.
When the application was executed, many outputs filled my screen. Some of them
were by pg_subscriber, and others were server log. Can we record them into
separated file? I imagined like pg_upgrade.

03.
A replication command is used when replication slots are created. Is there a
reason to use it? I think we do not have to use logical replication walsender mode,
we can use an SQL function instead. pg_create_logical_replication_slot() also outputs
LSN, isn't it sufficient?

04.
As you know, there are several options for publications/subscriptions/replication
slots. Do you have a good way to specify them in your mind?

05.
I found that the connection string for each subscriptions have a setting
"fallback_application_name=pg_subscriber". Can we remove it?

```
postgres=# SELECT subconninfo FROM pg_subscription;
subconninfo
---------------------------------------------------------------------------------
user=postgres port=5431 fallback_application_name=pg_subscriber dbname=postgres
(1 row)
```

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

#49Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#48)
Re: speed up a logical replica setup

On Fri, Jan 5, 2024 at 3:36 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

I love your proposal, so I want to join the review. Here are my first comments.

01.
Should we restrict that `--subscriber-conninfo` must not have hostname or IP?
We want users to execute pg_subscriber on the target, right?

I don't see any harm in users giving those information but we should
have some checks to ensure that the server is in standby mode and is
running locally. The other related point is do we need to take input
for the target cluster directory from the user? Can't we fetch that
information once we are connected to standby?

05.
I found that the connection string for each subscriptions have a setting
"fallback_application_name=pg_subscriber". Can we remove it?

```
postgres=# SELECT subconninfo FROM pg_subscription;
subconninfo
---------------------------------------------------------------------------------
user=postgres port=5431 fallback_application_name=pg_subscriber dbname=postgres
(1 row)
```

Can that help distinguish the pg_subscriber connection on the publisher?

--
With Regards,
Amit Kapila.

#50Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Amit Kapila (#49)
RE: speed up a logical replica setup

Dear Amit,

On Fri, Jan 5, 2024 at 3:36 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

I love your proposal, so I want to join the review. Here are my first comments.

01.
Should we restrict that `--subscriber-conninfo` must not have hostname or IP?
We want users to execute pg_subscriber on the target, right?

I don't see any harm in users giving those information but we should
have some checks to ensure that the server is in standby mode and is
running locally. The other related point is do we need to take input
for the target cluster directory from the user? Can't we fetch that
information once we are connected to standby?

I think that functions like inet_client_addr() may be able to use, but it returns
NULL only when the connection is via a Unix-domain socket. Can we restrict
pg_subscriber to use such a socket?

05.
I found that the connection string for each subscriptions have a setting
"fallback_application_name=pg_subscriber". Can we remove it?

```
postgres=# SELECT subconninfo FROM pg_subscription;
subconninfo

---------------------------------------------------------------------------------

user=postgres port=5431 fallback_application_name=pg_subscriber

dbname=postgres

(1 row)
```

Can that help distinguish the pg_subscriber connection on the publisher?

Note that this connection string is used between the publisher instance and the
subscriber instance (not pg_subscriber client application). Also, the
fallback_application_name would be replaced to the name of subscriber in
run_apply_worker()->walrcv_connect(). Actually the value would not be used.
See below output on publisher.

```
publisher=# SELECT application_name, backend_type FROM pg_stat_activity where backend_type = 'walsender';
application_name | backend_type
----------------------+--------------
pg_subscriber_5_9411 | walsender
(1 row)
```

Or, if you mean to say that this can distinguish whether the subscription is used
by pg_subscriber or not. I think it is sufficient the current format of name.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

#51Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#50)
Re: speed up a logical replica setup

On Mon, Jan 8, 2024 at 12:35 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

On Fri, Jan 5, 2024 at 3:36 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

I love your proposal, so I want to join the review. Here are my first comments.

01.
Should we restrict that `--subscriber-conninfo` must not have hostname or IP?
We want users to execute pg_subscriber on the target, right?

I don't see any harm in users giving those information but we should
have some checks to ensure that the server is in standby mode and is
running locally. The other related point is do we need to take input
for the target cluster directory from the user? Can't we fetch that
information once we are connected to standby?

I think that functions like inet_client_addr() may be able to use, but it returns
NULL only when the connection is via a Unix-domain socket. Can we restrict
pg_subscriber to use such a socket?

Good question. So, IIUC, this tool has a requirement to run locally
where standby is present because we want to write reconvery.conf file.
I am not sure if it is a good idea to have a restriction to use only
the unix domain socket as users need to set up the standby for that by
configuring unix_socket_directories. It is fine if we can't ensure
that it is running locally but we should at least ensure that the
server is a physical standby node to avoid the problems as Shlok has
reported.

On a related point, I see that the patch stops the standby server (if
it is running) before starting with subscriber-side steps. I was
wondering if users can object to it that there was some important data
replication in progress which this tool has stopped. Now, OTOH,
anyway, once the user uses pg_subscriber, the standby server will be
converted to a subscriber, so it may not be useful as a physical
replica. Do you or others have any thoughts on this matter?

05.
I found that the connection string for each subscriptions have a setting
"fallback_application_name=pg_subscriber". Can we remove it?

```
postgres=# SELECT subconninfo FROM pg_subscription;
subconninfo

---------------------------------------------------------------------------------

user=postgres port=5431 fallback_application_name=pg_subscriber

dbname=postgres

(1 row)
```

Can that help distinguish the pg_subscriber connection on the publisher?

Note that this connection string is used between the publisher instance and the
subscriber instance (not pg_subscriber client application). Also, the
fallback_application_name would be replaced to the name of subscriber in
run_apply_worker()->walrcv_connect(). Actually the value would not be used.

Fair point. It is not clear what other purpose this can achieve,
probably Euler has something in mind for this.

--
With Regards,
Amit Kapila.

#52Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Amit Kapila (#51)
RE: speed up a logical replica setup

Dear Amit,

I don't see any harm in users giving those information but we should
have some checks to ensure that the server is in standby mode and is
running locally. The other related point is do we need to take input
for the target cluster directory from the user? Can't we fetch that
information once we are connected to standby?

I think that functions like inet_client_addr() may be able to use, but it returns
NULL only when the connection is via a Unix-domain socket. Can we restrict
pg_subscriber to use such a socket?

Good question. So, IIUC, this tool has a requirement to run locally
where standby is present because we want to write reconvery.conf file.
I am not sure if it is a good idea to have a restriction to use only
the unix domain socket as users need to set up the standby for that by
configuring unix_socket_directories. It is fine if we can't ensure
that it is running locally but we should at least ensure that the
server is a physical standby node to avoid the problems as Shlok has
reported.

While thinking more about it, I found that we did not define the policy
whether user must not connect to the target while running pg_subscriber. What
should be? If it should be avoided, some parameters like listen_addresses and
unix_socket_permissions should be restricted like start_postmaster() in
pg_upgrade/server.c. Also, the port number should be changed to another value
as well.

Personally, I vote to reject connections during the pg_subscriber.

On a related point, I see that the patch stops the standby server (if
it is running) before starting with subscriber-side steps. I was
wondering if users can object to it that there was some important data
replication in progress which this tool has stopped. Now, OTOH,
anyway, once the user uses pg_subscriber, the standby server will be
converted to a subscriber, so it may not be useful as a physical
replica. Do you or others have any thoughts on this matter?

I assumed that connections should be closed before running pg_subscriber. If so,
it may be better to just fail the command when the physical standby has already
been started. There is no answer whether data replication and user queries
should stop. Users should choose the stop option based on their policy and then
pg_subscriebr can start postmaster.
pg_upgrade does the same thing in setup().

====

Further comment:
According to the doc, currently pg_subscriber is listed in the client application.
But based on the definition, I felt it should be at "PostgreSQL Server Applications"
page. How do you think? The definition is:

This part contains reference information for PostgreSQL server applications and
support utilities. These commands can only be run usefully on the host where the
database server resides. Other utility programs are listed in PostgreSQL Client
Applications.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

#53Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#52)
Re: speed up a logical replica setup

On Tue, Jan 9, 2024 at 12:31 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

I don't see any harm in users giving those information but we should
have some checks to ensure that the server is in standby mode and is
running locally. The other related point is do we need to take input
for the target cluster directory from the user? Can't we fetch that
information once we are connected to standby?

I think that functions like inet_client_addr() may be able to use, but it returns
NULL only when the connection is via a Unix-domain socket. Can we restrict
pg_subscriber to use such a socket?

Good question. So, IIUC, this tool has a requirement to run locally
where standby is present because we want to write reconvery.conf file.
I am not sure if it is a good idea to have a restriction to use only
the unix domain socket as users need to set up the standby for that by
configuring unix_socket_directories. It is fine if we can't ensure
that it is running locally but we should at least ensure that the
server is a physical standby node to avoid the problems as Shlok has
reported.

While thinking more about it, I found that we did not define the policy
whether user must not connect to the target while running pg_subscriber. What
should be? If it should be avoided, some parameters like listen_addresses and
unix_socket_permissions should be restricted like start_postmaster() in
pg_upgrade/server.c.

Yeah, this makes sense to me.

Also, the port number should be changed to another value
as well.

Fair point, but I think in that case we should take this as one of the
parameters.

Personally, I vote to reject connections during the pg_subscriber.

On a related point, I see that the patch stops the standby server (if
it is running) before starting with subscriber-side steps. I was
wondering if users can object to it that there was some important data
replication in progress which this tool has stopped. Now, OTOH,
anyway, once the user uses pg_subscriber, the standby server will be
converted to a subscriber, so it may not be useful as a physical
replica. Do you or others have any thoughts on this matter?

I assumed that connections should be closed before running pg_subscriber. If so,
it may be better to just fail the command when the physical standby has already
been started. There is no answer whether data replication and user queries
should stop. Users should choose the stop option based on their policy and then
pg_subscriebr can start postmaster.
pg_upgrade does the same thing in setup().

Agreed.

====

Further comment:
According to the doc, currently pg_subscriber is listed in the client application.
But based on the definition, I felt it should be at "PostgreSQL Server Applications"
page. How do you think?

I also think it should be a server application.

--
With Regards,
Amit Kapila.

#54Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Shlok Kyal (#47)
1 attachment(s)
Re: speed up a logical replica setup

On Fri, 5 Jan 2024 at 12:19, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

On Thu, 4 Jan 2024 at 16:46, Amit Kapila <amit.kapila16@gmail.com> wrote:

On Thu, Jan 4, 2024 at 12:22 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

Hi,
I was testing the patch with following test cases:

Test 1 :
- Create a 'primary' node
- Setup physical replica using pg_basebackup "./pg_basebackup –h
localhost –X stream –v –R –W –D ../standby "
- Insert data before and after pg_basebackup
- Run pg_subscriber and then insert some data to check logical
replication "./pg_subscriber –D ../standby -S “host=localhost
port=9000 dbname=postgres” -P “host=localhost port=9000
dbname=postgres” -d postgres"
- Also check pg_publication, pg_subscriber and pg_replication_slots tables.

Observation:
Data is not lost. Replication is happening correctly. Pg_subscriber is
working as expected.

Test 2:
- Create a 'primary' node
- Use normal pg_basebackup but don’t set up Physical replication
"./pg_basebackup –h localhost –v –W –D ../standby"
- Insert data before and after pg_basebackup
- Run pg_subscriber

Observation:
Pg_subscriber command is not completing and is stuck with following
log repeating:
LOG: waiting for WAL to become available at 0/3000168
LOG: invalid record length at 0/3000150: expected at least 24, got 0

I think probably the required WAL is not copied. Can you use the -X
option to stream WAL as well and then test? But I feel in this case
also, we should wait for some threshold time and then exit with
failure, removing new objects created, if any.

I have tested with -X stream option in pg_basebackup as well. In this
case also the pg_subscriber command is getting stuck.
logs:
2024-01-05 11:49:34.436 IST [61948] LOG: invalid resource manager ID
102 at 0/3000118
2024-01-05 11:49:34.436 IST [61948] LOG: waiting for WAL to become
available at 0/3000130

Test 3:
- Create a 'primary' node
- Use normal pg_basebackup but don’t set up Physical replication
"./pg_basebackup –h localhost –v –W –D ../standby"
-Insert data before pg_basebackup but not after pg_basebackup
-Run pg_subscriber

Observation:
Pg_subscriber command is not completing and is stuck with following
log repeating:
LOG: waiting for WAL to become available at 0/3000168
LOG: invalid record length at 0/3000150: expected at least 24, got 0

This is similar to the previous test and you can try the same option
here as well.

For this test as well tried with -X stream option in pg_basebackup.
It is getting stuck here as well with similar log.

Will investigate the issue further.

I noticed that the pg_subscriber get stuck when we run it on node
which is not a standby. It is because the of the code:
+   conn = connect_database(dbinfo[0].pubconninfo);
+   if (conn == NULL)
+       exit(1);
+   consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+                                                    temp_replslot);
+
.....
+else
+   {
+       appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+                         consistent_lsn);
+       WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+   }

Here the standby node would be waiting for the 'consistent_lsn' wal
during recovery but this wal will not be present on standby if no
physical replication is setup. Hence the command will be waiting
infinitely for the wal.
To solve this added a timeout of 60s for the recovery process and also
added a check so that pg_subscriber would give a error when it called
for node which is not in physical replication.
Have attached the patch for the same. It is a top-up patch of the
patch shared by Euler at [1]/messages/by-id/e02a2c17-22e5-4ba6-b788-de696ab74f1e@app.fastmail.com.

Please review the changes and merge the changes if it looks ok.

[1]: /messages/by-id/e02a2c17-22e5-4ba6-b788-de696ab74f1e@app.fastmail.com

Thanks and regards
Shlok Kyal

Attachments:

v1-0001-Restrict-pg_subscriber-to-standby-node.patchapplication/octet-stream; name=v1-0001-Restrict-pg_subscriber-to-standby-node.patchDownload
From 90c03545c09d29e9daf64e9151047bdd2a93348e Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 9 Jan 2024 20:53:47 +0530
Subject: [PATCH v1] Restrict pg_subscriber to standby node

Earlier pg_subscriber can run on normal backup cluster and the command gets
stuck. With this patch we are restricting pg_subscriber to run only for
standby server. Also added a timeout of 60 seconds so that the process ends
if it get stuck.
---
 src/bin/pg_basebackup/pg_subscriber.c | 59 ++++++++++++++++++++++++++-
 1 file changed, 57 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index b96ce26ed7..25ef10b0e7 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -72,9 +72,13 @@ static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
 static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 
+#define DEFAULT_WAIT	60
 #define	USEC_PER_SEC	1000000
+#define WAITS_PER_SEC	10		/* should divide USEC_PER_SEC evenly */
 #define	WAIT_INTERVAL	1		/* 1 second */
 
+static int	wait_seconds = DEFAULT_WAIT;
+
 /* Options */
 static const char *progname;
 
@@ -756,6 +760,9 @@ wait_for_end_recovery(const char *conninfo)
 	PGconn	   *conn;
 	PGresult   *res;
 	int			status = POSTMASTER_STILL_STARTING;
+	int			cnt;
+	int			rc;
+	char	   *pg_ctl_cmd;
 
 	pg_log_info("waiting the postmaster to reach the consistent state");
 
@@ -763,7 +770,7 @@ wait_for_end_recovery(const char *conninfo)
 	if (conn == NULL)
 		exit(1);
 
-	for (;;)
+	for (cnt = 0; cnt < wait_seconds * WAITS_PER_SEC; cnt++)
 	{
 		bool		in_recovery;
 
@@ -796,11 +803,25 @@ wait_for_end_recovery(const char *conninfo)
 		}
 
 		/* Keep waiting. */
-		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+		pg_usleep(USEC_PER_SEC / WAITS_PER_SEC);
 	}
 
 	disconnect_database(conn);
 
+	/*
+	 * if timeout is reached exit the pg_subscriber and stop the standby node
+	 */
+	if (cnt >= wait_seconds * WAITS_PER_SEC)
+	{
+		pg_log_error("recovery timed out");
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+		exit(1);
+	}
+
 	if (status == POSTMASTER_STILL_STARTING)
 	{
 		pg_log_error("server did not end recovery");
@@ -1160,6 +1181,7 @@ main(int argc, char **argv)
 	struct stat statbuf;
 
 	PGconn	   *conn;
+	PGresult   *res;
 	char	   *consistent_lsn;
 
 	PQExpBuffer recoveryconfcontents = NULL;
@@ -1167,6 +1189,7 @@ main(int argc, char **argv)
 	char		pidfile[MAXPGPATH];
 
 	int			i;
+	bool		in_recovery;
 
 	pg_logging_init(argv[0]);
 	pg_logging_set_level(PG_LOG_WARNING);
@@ -1340,6 +1363,38 @@ main(int argc, char **argv)
 	/* subscriber PID file. */
 	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
 
+	/*
+	 * Exit the pg_subscriber if the node is not a standby server.
+	 */
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("unexpected result from pg_is_in_recovery function");
+		exit(1);
+	}
+
+	in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+	if (!in_recovery)
+	{
+		pg_log_error("pg_subscriber is supported only on standby server");
+		exit(1);
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
 	/*
 	 * Stop the subscriber if it is a standby server. Before executing the
 	 * transformation steps, make sure the subscriber is not running because
-- 
2.34.1

#55vignesh C
vignesh21@gmail.com
In reply to: Euler Taveira (#25)
Re: speed up a logical replica setup

On Wed, 6 Dec 2023 at 12:53, Euler Taveira <euler@eulerto.com> wrote:

On Thu, Nov 9, 2023, at 8:12 PM, Michael Paquier wrote:

On Thu, Nov 09, 2023 at 03:41:53PM +0100, Peter Eisentraut wrote:

On 08.11.23 00:12, Michael Paquier wrote:

- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?

Seems like unnecessary churn. Nobody has complained about any of the other
tools in there.

Not sure. We rename things across releases in the tree from time to
time, and here that's straight-forward.

Based on this discussion it seems we have a consensus that this tool should be
in the pg_basebackup directory. (If/when we agree with the directory renaming,
it could be done in a separate patch.) Besides this move, the v3 provides a dry
run mode. It basically executes every routine but skip when should do
modifications. It is an useful option to check if you will be able to run it
without having issues with connectivity, permission, and existing objects
(replication slots, publications, subscriptions). Tests were slightly improved.
Messages were changed to *not* provide INFO messages by default and --verbose
provides INFO messages and --verbose --verbose also provides DEBUG messages. I
also refactored the connect_database() function into which the connection will
always use the logical replication mode. A bug was fixed in the transient
replication slot name. Ashutosh review [1] was included. The code was also indented.

There are a few suggestions from Ashutosh [2] that I will reply in another
email.

I'm still planning to work on the following points:

1. improve the cleanup routine to point out leftover objects if there is any
connection issue.
2. remove the physical replication slot if the standby is using one
(primary_slot_name).
3. provide instructions to promote the logical replica into primary, I mean,
stop the replication between the nodes and remove the replication setup
(publications, subscriptions, replication slots). Or even include another
action to do it. We could add both too.

Point 1 should be done. Points 2 and 3 aren't essential but will provide a nice
UI for users that would like to use it.

Few comments:
1) We should not allow specifying the same database name twice as we
will try to create the slots multiple times in the publisher, this can
be detected while parsing the options and error can be thrown:
+                       case 'd':
+
simple_string_list_append(&database_names, optarg);
+                               num_dbs++;
+                               break;
+static bool
+create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+{
+       int                     i;
+
+       for (i = 0; i < num_dbs; i++)
+       {
+               PGconn     *conn;
+               PGresult   *res;
+               char            replslotname[NAMEDATALEN];
....
....
....
+               /* Create replication slot on publisher. */
+               if (create_logical_replication_slot(conn, &dbinfo[i],
replslotname) != NULL || dry_run)
+                       pg_log_info("create replication slot \"%s\" on
publisher", replslotname);
+               else
+                       return false;
+
+               disconnect_database(conn);
+       }

E.g.: pg_subscriber -d postgres -d postgres

2) 2023 should be changed to 2024
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *       Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *             src/bin/pg_subscriber/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
3) Similarly here too:
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl
b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
new file mode 100644
index 0000000000..9d20847dc2
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_subscriber.
+#
4) Similarly here too:
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
new file mode 100644
index 0000000000..ce25608c68
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.

Regards,
Vignesh

#56Euler Taveira
euler@eulerto.com
In reply to: Shlok Kyal (#54)
Re: speed up a logical replica setup

On Wed, Jan 10, 2024, at 1:33 AM, Shlok Kyal wrote:

Here the standby node would be waiting for the 'consistent_lsn' wal
during recovery but this wal will not be present on standby if no
physical replication is setup. Hence the command will be waiting
infinitely for the wal.

Hmm. Some validations are missing.

To solve this added a timeout of 60s for the recovery process and also
added a check so that pg_subscriber would give a error when it called
for node which is not in physical replication.
Have attached the patch for the same. It is a top-up patch of the
patch shared by Euler at [1].

If the user has a node that is not a standby and it does not set the GUCs to
start the recovery process from a backup, the initial setup is broken. (That's
the case you described.) A good UI is to detect this scenario earlier.
Unfortunately, there isn't a reliable and cheap way to do it. You need to start
the recovery and check if it is having some progress. (I don't have a strong
opinion about requiring a standby to use this tool. It would reduce the
complexity about checking if the setup has all requirements to run this tool.)

--
Euler Taveira
EDB https://www.enterprisedb.com/

#57Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#56)
Re: speed up a logical replica setup

On Thu, Jan 11, 2024 at 7:59 AM Euler Taveira <euler@eulerto.com> wrote:

On Wed, Jan 10, 2024, at 1:33 AM, Shlok Kyal wrote:

Here the standby node would be waiting for the 'consistent_lsn' wal
during recovery but this wal will not be present on standby if no
physical replication is setup. Hence the command will be waiting
infinitely for the wal.

Hmm. Some validations are missing.

To solve this added a timeout of 60s for the recovery process and also
added a check so that pg_subscriber would give a error when it called
for node which is not in physical replication.
Have attached the patch for the same. It is a top-up patch of the
patch shared by Euler at [1].

If the user has a node that is not a standby and it does not set the GUCs to
start the recovery process from a backup, the initial setup is broken. (That's
the case you described.) A good UI is to detect this scenario earlier.
Unfortunately, there isn't a reliable and cheap way to do it. You need to start
the recovery and check if it is having some progress. (I don't have a strong
opinion about requiring a standby to use this tool. It would reduce the
complexity about checking if the setup has all requirements to run this tool.)

Right, such a check will reduce some complexity. So, +1 for the check
as proposed by Shlok. Also, what are your thoughts on a timeout during
the wait? I think it is okay to wait for 60s by default but there
should be an option for users to wait for longer.

--
With Regards,
Amit Kapila.

#58Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Amit Kapila (#57)
3 attachment(s)
RE: speed up a logical replica setup

Dear hackers,

I have been concerned that the patch has not been tested by cfbot due to the
application error. Also, some comments were raised. Therefore, I created a patch
to move forward.
I also tried to address some comments which is not so claimed by others.
They were included in 0003 patch.

* 0001 patch
It is almost the same as v3-0001, which was posted by Euler.
An unnecessary change for Mkvcbuild.pm (this file was removed) was ignored.

* 0002 patch
This contains small fixes to keep complier quiet.

* 0003 patch
This addresses comments posted to -hackers. For now, this does not contain a doc.
Will add if everyone agrees these idea.

1.
An option --port was added to control the port number for physical standby.
Users can specify a port number via the option, or an environment variable PGSUBPORT.
If not specified, a fixed value (50111) would be used.

SOURCE: [1]/messages/by-id/TY3PR01MB988978C7362A101927070D29F56A2@TY3PR01MB9889.jpnprd01.prod.outlook.com

2.
A FATAL error would be raised if --subscriber-conninfo specifies non-local server.

SOURCE: [2]/messages/by-id/TY3PR01MB9889593399165B9A04106741F5662@TY3PR01MB9889.jpnprd01.prod.outlook.com

3.
Options -o/-O were added to specify options for publications/subscriptions.

SOURCE: [2]/messages/by-id/TY3PR01MB9889593399165B9A04106741F5662@TY3PR01MB9889.jpnprd01.prod.outlook.com

4.
Made standby to save their output to log file.

SOURCE: [2]/messages/by-id/TY3PR01MB9889593399165B9A04106741F5662@TY3PR01MB9889.jpnprd01.prod.outlook.com

5.
Unnecessary Assert in drop_replication_slot() was removed.

SOURCE: [3]/messages/by-id/CALDaNm098Jkbh+ye6zMj9Ro9j1bBe6FfPV80BFbs1=pUuTJ07g@mail.gmail.com

How do you think?
Thanks Shlok and Vignesh to work with me offline.

[1]: /messages/by-id/TY3PR01MB988978C7362A101927070D29F56A2@TY3PR01MB9889.jpnprd01.prod.outlook.com
[2]: /messages/by-id/TY3PR01MB9889593399165B9A04106741F5662@TY3PR01MB9889.jpnprd01.prod.outlook.com
[3]: /messages/by-id/CALDaNm098Jkbh+ye6zMj9Ro9j1bBe6FfPV80BFbs1=pUuTJ07g@mail.gmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

Attachments:

v4-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchapplication/octet-stream; name=v4-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchDownload
From c7e4005b38d61c6c51d4e2cef67c6218d087f502 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v4 1/3] Creates a new logical replica from a standby server

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server. Remove
the additional replication slot that was used to get the consistent LSN.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_subscriber.sgml           |  284 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_subscriber.c         | 1517 +++++++++++++++++
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   44 +
 .../t/041_pg_subscriber_standby.pl            |  139 ++
 8 files changed, 2012 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_subscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_subscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_subscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index fda4690eab..dbe5778711 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgSubscriber       SYSTEM "pg_subscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
new file mode 100644
index 0000000000..553185c35f
--- /dev/null
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -0,0 +1,284 @@
+<!--
+doc/src/sgml/ref/pg_subscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgsubscriber">
+ <indexterm zone="app-pgsubscriber">
+  <primary>pg_subscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_subscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>create a new logical replica from a standby server</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_subscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a standby server and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_subscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a standby
+        server.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--publisher-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_subscriber</application> to output progress messages
+        and detailed information about each step.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_subscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_subscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_subscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the target data
+     directory is used by a standby server. Stop the standby server if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_subscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  Another
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_subscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_subscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_subscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_subscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> removes the additional replication
+     slot that was used to get the consistent LSN on the source server.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a standby server at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index a07d2b5e01..6da45005db 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -258,6 +258,7 @@
    &pgReceivewal;
    &pgRecvlogical;
    &pgRestore;
+   &pgSubscriber;
    &pgVerifyBackup;
    &psqlRef;
    &reindexdb;
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..f6281b7676 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_subscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_subscriber: $(WIN32RES) pg_subscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_subscriber$(X) '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_subscriber$(X) pg_subscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..ccfd7bb7a5 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_subscriber_sources = files(
+  'pg_subscriber.c'
+)
+
+if host_system == 'windows'
+  pg_subscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_subscriber',
+	'--FILEDESC', 'pg_subscriber - create a new logical replica from a standby server',])
+endif
+
+pg_subscriber = executable('pg_subscriber',
+  pg_subscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_subscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_subscriber.pl',
+      't/041_pg_subscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
new file mode 100644
index 0000000000..b96ce26ed7
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -0,0 +1,1517 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_subscriber/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static bool create_all_logical_replication_slots(LogicalRepInfo *dbinfo);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static bool dry_run = false;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static char temp_replslot[NAMEDATALEN] = {0};
+static bool made_transient_replslot = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_subscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+
+	if (made_transient_replslot)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		disconnect_database(conn);
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-conninfo=CONNINFO   publisher connection string\n"));
+	printf(_(" -S, --subscriber-conninfo=CONNINFO  subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name plus a fallback application name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	if (i > 0)
+		appendPQExpBufferChar(buf, ' ');
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %ld on publisher", sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %ld on subscriber", sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %ld on subscriber", cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+static bool
+create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+{
+	int			i;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
+		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
+		 * to reduce the probability of collision. By default, subscription
+		 * name is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_subscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (transient_replslot)
+		made_transient_replslot = true;
+	else
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_subscriber must have created this
+		 * publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_subscriber_ prefix followed by the exact
+			 * database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-conninfo", required_argument, NULL, 'P'},
+		{"subscriber-conninfo", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+	int			rc;
+
+	char	   *pg_ctl_cmd;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	int			i;
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_subscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:t:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				simple_string_list_append(&database_names, optarg);
+				num_dbs++;
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * Stop the subscriber if it is a standby server. Before executing the
+	 * transformation steps, make sure the subscriber is not running because
+	 * one of the steps is to modify some recovery parameters that require a
+	 * restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		pg_log_info("subscriber is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+	}
+
+	/*
+	 * Create a replication slot for each database on the publisher.
+	 */
+	if (!create_all_logical_replication_slots(dbinfo))
+		exit(1);
+
+	/*
+	 * Create a logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. We cannot use the last created replication slot
+	 * because the consistent LSN should be obtained *after* the base backup
+	 * finishes (and the base backup should include the logical replication
+	 * slots).
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 *
+	 * A temporary replication slot is not used here to avoid keeping a
+	 * replication connection open (depending when base backup was taken, the
+	 * connection should be open for a few hours).
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create a publication for each database. This step should be executed
+	 * after promoting the subscriber to avoid replicating unnecessary
+	 * objects.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+
+		/* Connect to publisher. */
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		create_publication(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Create a subscription for each database.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * The transient replication slot is no longer required. Drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops the replication slot later.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+	{
+		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+	else
+	{
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
new file mode 100644
index 0000000000..9d20847dc2
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_subscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_subscriber');
+program_version_ok('pg_subscriber');
+program_options_handling_ok('pg_subscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_subscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+		'--subscriber-conninfo', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
new file mode 100644
index 0000000000..ce25608c68
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_subscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_subscriber on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_subscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
-- 
2.43.0

v4-0002-Fixed-small-bugs-to-keep-compiler-quiet.patchapplication/octet-stream; name=v4-0002-Fixed-small-bugs-to-keep-compiler-quiet.patchDownload
From 5a356aa7f273221b574c6f0b307cc86905d1cf45 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 10 Jan 2024 05:30:23 +0000
Subject: [PATCH v4 2/3] Fixed small bugs to keep compiler quiet

---
 src/bin/pg_basebackup/.gitignore      |  1 +
 src/bin/pg_basebackup/pg_subscriber.c | 10 +++++-----
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..0e5384a1d5 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_subscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index b96ce26ed7..c2d17dcda3 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -465,7 +465,7 @@ get_sysid_from_conn(const char *conninfo)
 
 	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
 
-	pg_log_info("system identifier is %ld on publisher", sysid);
+	pg_log_info("system identifier is " UINT64_FORMAT " on publisher", sysid);
 
 	disconnect_database(conn);
 
@@ -495,7 +495,7 @@ get_control_from_datadir(const char *datadir)
 
 	sysid = cf->system_identifier;
 
-	pg_log_info("system identifier is %ld on subscriber", sysid);
+	pg_log_info("system identifier is " UINT64_FORMAT " on subscriber", sysid);
 
 	pfree(cf);
 
@@ -539,7 +539,7 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
 	if (!dry_run)
 		update_controlfile(datadir, cf, true);
 
-	pg_log_info("system identifier is %ld on subscriber", cf->system_identifier);
+	pg_log_info("system identifier is " UINT64_FORMAT " on subscriber", cf->system_identifier);
 
 	pg_log_info("running pg_resetwal on the subscriber");
 
@@ -631,7 +631,7 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 								char *slot_name)
 {
 	PQExpBuffer str = createPQExpBuffer();
-	PGresult   *res;
+	PGresult   *res = NULL;
 	char	   *lsn = NULL;
 	bool		transient_replslot = false;
 
@@ -1204,7 +1204,7 @@ main(int argc, char **argv)
 	}
 #endif
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:t:v",
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
-- 
2.43.0

v4-0003-Address-some-comments-proposed-on-hackers.patchapplication/octet-stream; name=v4-0003-Address-some-comments-proposed-on-hackers.patchDownload
From efae01bbbf8f26ad09273562261d93bd57f485fe Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 10 Jan 2024 05:53:56 +0000
Subject: [PATCH v4 3/3] Address some comments proposed on -hackers

This patch contains below changes.

* Add --port option
* Restrict --subscriber-conninfo not to specify external server
* Allow to specify publication/subscription options
* Remove unecessary Assert
* Save logs in Standby logfile
---
 src/bin/pg_basebackup/pg_subscriber.c | 110 +++++++++++++++++++++++---
 1 file changed, 98 insertions(+), 12 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index c2d17dcda3..d502230b28 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -27,6 +27,7 @@
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
+#include "libpq/pqcomm.h"
 #include "utils/pidfile.h"
 
 typedef struct LogicalRepInfo
@@ -52,7 +53,7 @@ static char *get_base_conninfo(char *conninfo, char *dbname,
 							   const char *noderole);
 static bool get_exec_path(const char *path);
 static bool check_data_directory(const char *datadir);
-static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static char *concat_conninfo(const char *conninfo, const char *dbname, unsigned short port);
 static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
 static PGconn *connect_database(const char *conninfo);
 static void disconnect_database(PGconn *conn);
@@ -74,6 +75,7 @@ static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 
 #define	USEC_PER_SEC	1000000
 #define	WAIT_INTERVAL	1		/* 1 second */
+#define DEF_PGSPORT			50111
 
 /* Options */
 static const char *progname;
@@ -95,6 +97,11 @@ static int	num_dbs = 0;
 static char temp_replslot[NAMEDATALEN] = {0};
 static bool made_transient_replslot = false;
 
+static unsigned short subport;
+
+static char *pubopts;
+static char *subopts;
+
 enum WaitPMResult
 {
 	POSTMASTER_READY,
@@ -120,6 +127,9 @@ cleanup_objects_atexit(void)
 	if (success)
 		return;
 
+	if (!dbinfo)
+		return;
+
 	for (i = 0; i < num_dbs; i++)
 	{
 		if (dbinfo[i].made_subscription)
@@ -149,7 +159,8 @@ cleanup_objects_atexit(void)
 	if (made_transient_replslot)
 	{
 		conn = connect_database(dbinfo[0].pubconninfo);
-		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		if (conn != NULL)
+			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
 		disconnect_database(conn);
 	}
 }
@@ -214,6 +225,26 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 			continue;
 		}
 
+		/*
+		 * If the dbname is NULL (this means the conninfo is for the
+		 * subscriber), we also check that the connection string does not
+		 * specify the non-local server.
+		 */
+		if (!dbname &&
+			(strcmp(conn_opt->keyword, "host")  == 0 ||
+			 strcmp(conn_opt->keyword, "hostaddr")  == 0) &&
+			conn_opt->val != NULL)
+		{
+			const char *value = conn_opt->val;
+
+			if (value && strlen(value) > 0 &&
+			/* check for 'local' host values */
+				(strcmp(value, "localhost") != 0 && strcmp(value, "127.0.0.1") != 0 &&
+				 strcmp(value, "::1") != 0 && !is_unixsock_path(value)))
+				pg_fatal("--subscriber-conninfo must not be non-local connection: %s",
+						 value);
+		}
+
 		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
 		{
 			if (i > 0)
@@ -332,14 +363,14 @@ check_data_directory(const char *datadir)
 }
 
 /*
- * Append database name into a base connection string.
+ * Append database name and/or port number into a base connection string.
  *
  * dbname is the only parameter that changes so it is not included in the base
  * connection string. This function concatenates dbname to build a "real"
  * connection string.
  */
 static char *
-concat_conninfo_dbname(const char *conninfo, const char *dbname)
+concat_conninfo(const char *conninfo, const char *dbname, unsigned short port)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	char	   *ret;
@@ -349,6 +380,9 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
 	appendPQExpBufferStr(buf, conninfo);
 	appendPQExpBuffer(buf, " dbname=%s", dbname);
 
+	if (port)
+		appendPQExpBuffer(buf, " port=%d", port);
+
 	ret = pg_strdup(buf->data);
 	destroyPQExpBuffer(buf);
 
@@ -372,7 +406,7 @@ store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
 		char	   *conninfo;
 
 		/* Publisher. */
-		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		conninfo = concat_conninfo(pub_base_conninfo, cell->val, 0);
 		dbinfo[i].pubconninfo = conninfo;
 		dbinfo[i].dbname = cell->val;
 		dbinfo[i].made_replslot = false;
@@ -381,7 +415,7 @@ store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
 		/* other struct fields will be filled later. */
 
 		/* Subscriber. */
-		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		conninfo = concat_conninfo(sub_base_conninfo, cell->val, subport);
 		dbinfo[i].subconninfo = conninfo;
 
 		i++;
@@ -689,8 +723,6 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 
-	Assert(conn != NULL);
-
 	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
@@ -872,6 +904,9 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
 
+	if (pubopts)
+		appendPQExpBuffer(str, " WITH (%s)", pubopts);
+
 	pg_log_debug("command is: %s", str->data);
 
 	if (!dry_run)
@@ -948,9 +983,14 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
-					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  "WITH (create_slot = false, copy_data = false, enabled = false",
 					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
 
+	if (subopts)
+		appendPQExpBuffer(str, ", %s)", subopts);
+	else
+		appendPQExpBufferStr(str, ")");
+
 	pg_log_debug("command is: %s", str->data);
 
 	if (!dry_run)
@@ -1142,6 +1182,9 @@ main(int argc, char **argv)
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
 		{"verbose", no_argument, NULL, 'v'},
+		{"port", required_argument, NULL, 'p'},
+		{"pubopts", required_argument, NULL, 'o'},
+		{"subopts", required_argument, NULL, 'O'},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -1168,11 +1211,17 @@ main(int argc, char **argv)
 
 	int			i;
 
+	char		timebuf[128];
+	struct 		timeval time;
+	time_t		tt;
+
 	pg_logging_init(argv[0]);
 	pg_logging_set_level(PG_LOG_WARNING);
 	progname = get_progname(argv[0]);
 	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
 
+	subport = getenv("PGSUBPORT") ? atoi(getenv("PGSUBPORT")) : DEF_PGSPORT;
+
 	if (argc > 1)
 	{
 		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
@@ -1204,7 +1253,7 @@ main(int argc, char **argv)
 	}
 #endif
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nvp:o:O:",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1228,6 +1277,36 @@ main(int argc, char **argv)
 			case 'v':
 				pg_logging_increase_verbosity();
 				break;
+			case 'p':
+				if ((subport = atoi(optarg)) < 0)
+					pg_fatal("invalid port number");
+				break;
+			case 'o':
+				/* append option? */
+				if (!pubopts)
+					pubopts = pg_strdup(optarg);
+				else
+				{
+					char	   *old_pubopts = pubopts;
+
+					pubopts = psprintf("%s %s", old_pubopts, optarg);
+					free(old_pubopts);
+				}
+				break;
+
+			case 'O':
+				/* append option? */
+				if (!subopts)
+					subopts = pg_strdup(optarg);
+				else
+				{
+					char	   *old_subopts = subopts;
+
+					subopts = psprintf("%s %s", old_subopts, optarg);
+					free(old_subopts);
+				}
+				break;
+
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -1418,9 +1497,16 @@ main(int argc, char **argv)
 	/*
 	 * Start subscriber and wait until accepting connections.
 	 */
-	pg_log_info("starting the subscriber");
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	/* append milliseconds */
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
 
-	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	pg_log_info("starting the subscriber");
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -o \"-p %d\" -l \"%sstandby_%s.log\"",
+						  pg_ctl_path, subscriber_dir, subport, subscriber_dir, timebuf);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 1);
 
-- 
2.43.0

#59Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#58)
1 attachment(s)
Re: speed up a logical replica setup

On Thu, Jan 11, 2024, at 9:18 AM, Hayato Kuroda (Fujitsu) wrote:

I have been concerned that the patch has not been tested by cfbot due to the
application error. Also, some comments were raised. Therefore, I created a patch
to move forward.

Let me send an updated patch to hopefully keep the CF bot happy. The following
items are included in this patch:

* drop physical replication slot if standby is using one [1]/messages/by-id/e02a2c17-22e5-4ba6-b788-de696ab74f1e@app.fastmail.com.
* cleanup small changes (copyright, .gitignore) [2]/messages/by-id/CALDaNm1joke42n68LdegN5wCpaeoOMex2EHcdZrVZnGD3UhfNQ@mail.gmail.com[3]/messages/by-id/TY3PR01MB98895BA6C1D72CB8582CACC4F5682@TY3PR01MB9889.jpnprd01.prod.outlook.com
* fix getopt_long() options [2]/messages/by-id/CALDaNm1joke42n68LdegN5wCpaeoOMex2EHcdZrVZnGD3UhfNQ@mail.gmail.com
* fix format specifier for some messages
* move doc to Server Application section [4]/messages/by-id/TY3PR01MB988978C7362A101927070D29F56A2@TY3PR01MB9889.jpnprd01.prod.outlook.com
* fix assert failure
* ignore duplicate database names [2]/messages/by-id/CALDaNm1joke42n68LdegN5wCpaeoOMex2EHcdZrVZnGD3UhfNQ@mail.gmail.com
* store subscriber server log into a separate file
* remove MSVC support

I'm still addressing other reviews and I'll post another version that includes
it soon.

[1]: /messages/by-id/e02a2c17-22e5-4ba6-b788-de696ab74f1e@app.fastmail.com
[2]: /messages/by-id/CALDaNm1joke42n68LdegN5wCpaeoOMex2EHcdZrVZnGD3UhfNQ@mail.gmail.com
[3]: /messages/by-id/TY3PR01MB98895BA6C1D72CB8582CACC4F5682@TY3PR01MB9889.jpnprd01.prod.outlook.com
[4]: /messages/by-id/TY3PR01MB988978C7362A101927070D29F56A2@TY3PR01MB9889.jpnprd01.prod.outlook.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v5-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchtext/x-patch; name="=?UTF-8?Q?v5-0001-Creates-a-new-logical-replica-from-a-standby-serv.patc?= =?UTF-8?Q?h?="Download
From 2341ef3dc26d48b51b13ad01ac38c79d24b1e461 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v5] Creates a new logical replica from a standby server

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server. Remove
the additional replication slot that was used to get the consistent LSN.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_subscriber.sgml           |  284 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_subscriber.c         | 1657 +++++++++++++++++
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   44 +
 .../t/041_pg_subscriber_standby.pl            |  139 ++
 9 files changed, 2153 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_subscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_subscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_subscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..3862c976d7 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgSubscriber       SYSTEM "pg_subscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
new file mode 100644
index 0000000000..553185c35f
--- /dev/null
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -0,0 +1,284 @@
+<!--
+doc/src/sgml/ref/pg_subscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgsubscriber">
+ <indexterm zone="app-pgsubscriber">
+  <primary>pg_subscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_subscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>create a new logical replica from a standby server</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_subscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a standby server and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_subscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a standby
+        server.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--publisher-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_subscriber</application> to output progress messages
+        and detailed information about each step.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_subscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_subscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_subscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the target data
+     directory is used by a standby server. Stop the standby server if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_subscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  Another
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_subscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_subscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_subscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_subscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> removes the additional replication
+     slot that was used to get the consistent LSN on the source server.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a standby server at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..266f4e515a 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..0e5384a1d5 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_subscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..f6281b7676 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_subscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_subscriber: $(WIN32RES) pg_subscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_subscriber$(X) '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_subscriber$(X) pg_subscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..ccfd7bb7a5 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_subscriber_sources = files(
+  'pg_subscriber.c'
+)
+
+if host_system == 'windows'
+  pg_subscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_subscriber',
+	'--FILEDESC', 'pg_subscriber - create a new logical replica from a standby server',])
+endif
+
+pg_subscriber = executable('pg_subscriber',
+  pg_subscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_subscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_subscriber.pl',
+      't/041_pg_subscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
new file mode 100644
index 0000000000..e998c29f9e
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -0,0 +1,1657 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_subscriber/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static char *use_primary_slot_name(void);
+static bool create_all_logical_replication_slots(LogicalRepInfo *dbinfo);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static char temp_replslot[NAMEDATALEN] = {0};
+static bool made_transient_replslot = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_subscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+
+	if (made_transient_replslot)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+			disconnect_database(conn);
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-conninfo=CONNINFO   publisher connection string\n"));
+	printf(_(" -S, --subscriber-conninfo=CONNINFO  subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name plus a fallback application name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	if (i > 0)
+		appendPQExpBufferChar(buf, ' ');
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Return a palloc'd slot name if the replication is using one.
+ */
+static char *
+use_primary_slot_name(void)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+	char	   *slot_name;
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT setting FROM pg_settings WHERE name = 'primary_slot_name'");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain parameter information: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+
+	/*
+	 * If primary_slot_name is an empty string, the current replication
+	 * connection is not using a replication slot, bail out.
+	 */
+	if (strcmp(PQgetvalue(res, 0, 0), "") == 0)
+	{
+		PQclear(res);
+		return NULL;
+	}
+
+	slot_name = pg_strdup(PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_replication_slots r INNER JOIN pg_stat_activity a ON (r.active_pid = a.pid) WHERE slot_name = '%s'", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		return NULL;
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return slot_name;
+}
+
+static bool
+create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+{
+	int			i;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
+		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
+		 * to reduce the probability of collision. By default, subscription
+		 * name is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_subscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (transient_replslot)
+		made_transient_replslot = true;
+	else
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_subscriber must have created this
+		 * publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_subscriber_ prefix followed by the exact
+			 * database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-conninfo", required_argument, NULL, 'P'},
+		{"subscriber-conninfo", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+	int			rc;
+
+	char	   *pg_ctl_cmd;
+
+	char	   *base_dir;
+	char	   *server_start_log;
+
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	int			i;
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_subscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&database_names, optarg))
+				{
+					simple_string_list_append(&database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("directory path for subscriber is too long");
+		exit(1);
+	}
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+	{
+		pg_log_error("could not create directory \"%s\": %m", base_dir);
+		exit(1);
+	}
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * Stop the subscriber if it is a standby server. Before executing the
+	 * transformation steps, make sure the subscriber is not running because
+	 * one of the steps is to modify some recovery parameters that require a
+	 * restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Since the standby server is running, check if it is using an
+		 * existing replication slot for WAL retention purposes. This
+		 * replication slot has no use after the transformation, hence, it
+		 * will be removed at the end of this process.
+		 */
+		primary_slot_name = use_primary_slot_name();
+		if (primary_slot_name != NULL)
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+
+		pg_log_info("subscriber is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+	}
+
+	/*
+	 * Create a replication slot for each database on the publisher.
+	 */
+	if (!create_all_logical_replication_slots(dbinfo))
+		exit(1);
+
+	/*
+	 * Create a logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. We cannot use the last created replication slot
+	 * because the consistent LSN should be obtained *after* the base backup
+	 * finishes (and the base backup should include the logical replication
+	 * slots).
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 *
+	 * A temporary replication slot is not used here to avoid keeping a
+	 * replication connection open (depending when base backup was taken, the
+	 * connection should be open for a few hours).
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	server_start_log = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("log file path is too long");
+		exit(1);
+	}
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create a publication for each database. This step should be executed
+	 * after promoting the subscriber to avoid replicating unnecessary
+	 * objects.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+
+		/* Connect to publisher. */
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		create_publication(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Create a subscription for each database.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * The transient replication slot is no longer required. Drop it.
+	 *
+	 * If the physical replication slot exists, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops the replication slot later.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+	{
+		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		if (primary_slot_name != NULL)
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+	}
+	else
+	{
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		if (primary_slot_name != NULL)
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	/*
+	 * Remove log file generated by this tool, if it runs successfully.
+	 * Otherwise, file is kept that may provide useful debugging information.
+	 */
+	unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
new file mode 100644
index 0000000000..4ebff76b2d
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_subscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_subscriber');
+program_version_ok('pg_subscriber');
+program_options_handling_ok('pg_subscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_subscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+		'--subscriber-conninfo', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
new file mode 100644
index 0000000000..fbcd0fc82b
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_subscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_subscriber on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_subscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
-- 
2.30.2

#60Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#58)
Re: speed up a logical replica setup

On Thu, Jan 11, 2024, at 9:18 AM, Hayato Kuroda (Fujitsu) wrote:

I have been concerned that the patch has not been tested by cfbot due to the
application error. Also, some comments were raised. Therefore, I created a patch
to move forward.
I also tried to address some comments which is not so claimed by others.
They were included in 0003 patch.

[I removed the following part in the previous email and couldn't reply to it...]

* 0001 patch
It is almost the same as v3-0001, which was posted by Euler.
An unnecessary change for Mkvcbuild.pm (this file was removed) was ignored.

v5 removes the MSVC support.

* 0002 patch
This contains small fixes to keep complier quiet.

I applied it. Although, I used a different approach for format specifier.

* 0003 patch
This addresses comments posted to -hackers. For now, this does not contain a doc.
Will add if everyone agrees these idea.

I didn't review all items but ...

1.
An option --port was added to control the port number for physical standby.
Users can specify a port number via the option, or an environment variable PGSUBPORT.
If not specified, a fixed value (50111) would be used.

My first reaction as a new user would be: why do I need to specify a port if my
--subscriber-conninfo already contains a port? Ugh. I'm wondering if we can do
it behind the scenes. Try a range of ports.

2.
A FATAL error would be raised if --subscriber-conninfo specifies non-local server.

Extra protection is always good. However, let's make sure this code path is
really useful. I'll think a bit about it.

3.
Options -o/-O were added to specify options for publications/subscriptions.

Flexibility is cool. However, I think the cost benefit of it is not good. You
have to parse the options to catch preliminary errors. Things like publish only
delete and subscription options that conflicts with the embedded ones are
additional sources of failure.

4.
Made standby to save their output to log file.

It was already done in v5. I did in a different way.

5.
Unnecessary Assert in drop_replication_slot() was removed.

Instead, I fixed the code and keep the assert.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#61Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#60)
Re: speed up a logical replica setup

On Fri, Jan 12, 2024 at 4:30 AM Euler Taveira <euler@eulerto.com> wrote:

3.
Options -o/-O were added to specify options for publications/subscriptions.

Flexibility is cool. However, I think the cost benefit of it is not good. You
have to parse the options to catch preliminary errors. Things like publish only
delete and subscription options that conflicts with the embedded ones are
additional sources of failure.

Yeah, I am also not sure we need those. Did we discussed about that
previously? OTOH, we may consider to enhance this tool later if we
have user demand for such options.

BTW, I think we need some way to at least drop the existing
subscriptions otherwise the newly created subscriber will attempt to
fetch the data which may not be intended. Ashutosh made an argument
above thread that we need an option for publications as well.

--
With Regards,
Amit Kapila.

#62Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Amit Kapila (#61)
RE: speed up a logical replica setup

Dear Amit, Euler,

3.
Options -o/-O were added to specify options for publications/subscriptions.

Flexibility is cool. However, I think the cost benefit of it is not good. You
have to parse the options to catch preliminary errors. Things like publish only
delete and subscription options that conflicts with the embedded ones are
additional sources of failure.

Yeah, I am also not sure we need those. Did we discussed about that
previously? OTOH, we may consider to enhance this tool later if we
have user demand for such options.

OK, so let's drop it once and consider later as an enhancement.
As Euler said, it leads additional ERRORs.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

#63Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#60)
RE: speed up a logical replica setup

Dear Euler,

Sorry for disturbing your work and thanks for updates.
I will review your patch again.

* 0001 patch
It is almost the same as v3-0001, which was posted by Euler.
An unnecessary change for Mkvcbuild.pm (this file was removed) was ignored.

v5 removes the MSVC support.

Confirmed that the patch could be applied.

* 0002 patch
This contains small fixes to keep complier quiet.

I applied it. Although, I used a different approach for format specifier.

Good, all warnings were removed. However, the patch failed to pass tests on FreeBSD twice.
I'm quite not sure the ERROR, but is it related with us?

* 0003 patch
This addresses comments posted to -hackers. For now, this does not contain a doc.
Will add if everyone agrees these idea.

I didn't review all items but ...

1.
An option --port was added to control the port number for physical standby.
Users can specify a port number via the option, or an environment variable PGSUBPORT.
If not specified, a fixed value (50111) would be used.

My first reaction as a new user would be: why do I need to specify a port if my
--subscriber-conninfo already contains a port? Ugh. I'm wondering if we can do
it behind the scenes. Try a range of ports.

My initial motivation of the setting was to avoid establishing connections
during the pg_subscriber. While considering more, I started to think that
--subscriber-conninfo may not be needed. pg_upgrade does not requires the
string: it requries username, and optionally port number (which would be used
during the upgrade) instead. The advantage of this approach is that we do not
have to parse the connection string.
How do you think?

2.
A FATAL error would be raised if --subscriber-conninfo specifies non-local server.

Extra protection is always good. However, let's make sure this code path is
really useful. I'll think a bit about it.

OK, I can wait your consideration. Note that if we follow the pg_ugprade style,
we may able to reuse check_pghost_envvar().

3.
Options -o/-O were added to specify options for publications/subscriptions.

Flexibility is cool. However, I think the cost benefit of it is not good. You
have to parse the options to catch preliminary errors. Things like publish only
delete and subscription options that conflicts with the embedded ones are
additional sources of failure.

As I already replied, let's stop doing it once. We can resume based on the requirement.

4.
Made standby to save their output to log file.

It was already done in v5. I did in a different way.

Good. I felt that yours were better. BTW, can we record outputs by pg_subscriber to a file as well?
pg_upgrade did similar thing. Thought?

5.
Unnecessary Assert in drop_replication_slot() was removed.

Instead, I fixed the code and keep the assert.

Cool.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

#64Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#59)
RE: speed up a logical replica setup

Dear Euler,

Here are comments for your v5 patch.

01.
In the document, two words target/standby are used as almost the same meaning.
Can you unify them?

02.
```
<refsynopsisdiv>
<cmdsynopsis>
<command>pg_subscriber</command>
<arg rep="repeat"><replaceable>option</replaceable></arg>
</cmdsynopsis>
</refsynopsisdiv>
```

There are some mandatory options like -D/-S/-P. It must be listed in Synopsis chapter.

03.
```
<para>
<application>pg_subscriber</application> takes the publisher and subscriber
connection strings, a cluster directory from a standby server and a list of
database names and it sets up a new logical replica using the physical
recovery process.
</para>
```

I briefly checked other pages and they do not describe accepted options here.
A summary of the application should be mentioned. Based on that, how about:

```
pg_subscriber creates a new <link linkend="logical-replication-subscription">
subscriber</link> from a physical standby server. This allows users to quickly
set up logical replication system.
```

04.
```
<para>
The <application>pg_subscriber</application> should be run at the target
server. The source server (known as publisher server) should accept logical
replication connections from the target server (known as subscriber server).
The target server should accept local logical replication connection.
</para>
```

I'm not native speaker, but they are not just recommmendations - they are surely
required. So, should we replace s/should/has to/?

05.
```
<varlistentry>
<term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
<term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
<listitem>
<para>
The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
</para>
</listitem>
</varlistentry>
```

I became not sure whether it is "The connection string to the subscriber.".
The server is still physical standby at that time.

06.
```
* IDENTIFICATION
* src/bin/pg_subscriber/pg_subscriber.c
```

The identification is not correct.

07.
I felt that there were too many global variables and LogicalRepInfo should be
refactored. Because...

* Some info related with clusters(e.g., subscriber_dir, conninfo, ...) should be
gathered in one struct.
* pubconninfo/subsconninfo are stored per db, but it is not needed if we have
one base_conninfo.
* pubname/subname are not needed because we have fixed naming rule.
* pg_ctl_path and pg_resetwal_path can be conbimed into one bindir.
* num_dbs should not be alone.
...

Based on above, how about using structures like below?

```
typedef struct LogicalRepPerdbInfo
{
Oid oid;
char *dbname;
bool made_replslot; /* replication slot was created */
bool made_publication; /* publication was created */
bool made_subscription; /* subscription was created */
} LogicalRepPerdbInfo;

typedef struct PrimaryInfo
{
char *base_conninfo;
bool made_transient_replslot;
} PrimaryInfo;

typedef struct StandbyInfo
{
char *base_conninfo;
char *bindir;
char *pgdata;
char *primary_slot_name;
} StandbyInfo;

typedef struct LogicalRepInfo
{
int num_dbs;
LogicalRepPerdbInfo *perdb;
PrimaryInfo *primary;
StandbyInfo *standby;
} LogicalRepInfo;
```

08.
```
char *subconninfo; /* subscription connection string for logical
* replication */
```

Not sure how we should notate because the target has not been subscriber yet.

09.
```
enum WaitPMResult
{
POSTMASTER_READY,
POSTMASTER_STANDBY,
POSTMASTER_STILL_STARTING,
POSTMASTER_FAILED
};
```

This enum has been already defined in pg_ctl.c. Not sure we can use the same name.
Can we rename to PGSWaitPMResult. or export pre-existing one?

10.
```
/* Options */
static const char *progname;
```

I think it is not an option.

11.
```
/*
* Validate a connection string. Returns a base connection string that is a
* connection string without a database name plus a fallback application name.
* Since we might process multiple databases, each database name will be
* appended to this base connection string to provide a final connection string.
* If the second argument (dbname) is not null, returns dbname if the provided
* connection string contains it. If option --database is not provided, uses
* dbname as the only database to setup the logical replica.
* It is the caller's responsibility to free the returned connection string and
* dbname.
*/
static char *
get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
```
Just FYI - adding fallback_application_name may be too optimisitic. Currently
the output was used by both pg_subscriber and subscription connection.

12.
Can we add an option not to remove log files even operations were succeeded.

13.
```
/*
* Since the standby server is running, check if it is using an
* existing replication slot for WAL retention purposes. This
* replication slot has no use after the transformation, hence, it
* will be removed at the end of this process.
*/
primary_slot_name = use_primary_slot_name();
```

Now primary_slot_name is checked only when the server have been started, but
it should be checked in any cases.

14.
```
consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
temp_replslot);
```

Can we create a temporary slot here?

15.
I found that subscriptions cannot be started if tuples are inserted on publisher
after creating temp_replslot. After starting a subscriber, I got below output on the log.

```
ERROR: could not receive data from WAL stream: ERROR: publication "pg_subscriber_5" does not exist
CONTEXT: slot "pg_subscriber_5_3632", output plugin "pgoutput", in the change callback, associated LSN 0/30008A8
LOG: background worker "logical replication apply worker" (PID 3669) exited with exit code 1
```

But this is strange. I confirmed that the specified publication surely exists.
Do you know the reason?

```
publisher=# SELECT pubname FROM pg_publication;
pubname
-----------------
pg_subscriber_5
(1 row)
```

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

#65Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#63)
Re: speed up a logical replica setup

Hi,

On Fri, 12 Jan 2024 at 09:32, Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Good, all warnings were removed. However, the patch failed to pass tests on FreeBSD twice.
I'm quite not sure the ERROR, but is it related with us?

FreeBSD errors started after FreeBSD's CI image was updated [1]https://cirrus-ci.com/task/4700394639589376. I do
not think error is related to this.

[1]: https://cirrus-ci.com/task/4700394639589376

--
Regards,
Nazir Bilal Yavuz
Microsoft

#66Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#63)
Re: speed up a logical replica setup

On Fri, Jan 12, 2024 at 12:02 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

I didn't review all items but ...

1.
An option --port was added to control the port number for physical standby.
Users can specify a port number via the option, or an environment variable PGSUBPORT.
If not specified, a fixed value (50111) would be used.

My first reaction as a new user would be: why do I need to specify a port if my
--subscriber-conninfo already contains a port? Ugh. I'm wondering if we can do
it behind the scenes. Try a range of ports.

My initial motivation of the setting was to avoid establishing connections
during the pg_subscriber. While considering more, I started to think that
--subscriber-conninfo may not be needed. pg_upgrade does not requires the
string: it requries username, and optionally port number (which would be used
during the upgrade) instead. The advantage of this approach is that we do not
have to parse the connection string.
How do you think?

+1. This seems worth considering. I think unless we have a good reason
to have this parameter, we should try to avoid it.

--
With Regards,
Amit Kapila.

#67Junwang Zhao
zhjwpku@gmail.com
In reply to: Euler Taveira (#59)
Re: speed up a logical replica setup

Hi Euler,

On Fri, Jan 12, 2024 at 6:16 AM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Jan 11, 2024, at 9:18 AM, Hayato Kuroda (Fujitsu) wrote:

I have been concerned that the patch has not been tested by cfbot due to the
application error. Also, some comments were raised. Therefore, I created a patch
to move forward.

Let me send an updated patch to hopefully keep the CF bot happy. The following
items are included in this patch:

* drop physical replication slot if standby is using one [1].
* cleanup small changes (copyright, .gitignore) [2][3]
* fix getopt_long() options [2]
* fix format specifier for some messages
* move doc to Server Application section [4]
* fix assert failure
* ignore duplicate database names [2]
* store subscriber server log into a separate file
* remove MSVC support

I'm still addressing other reviews and I'll post another version that includes
it soon.

[1] /messages/by-id/e02a2c17-22e5-4ba6-b788-de696ab74f1e@app.fastmail.com
[2] /messages/by-id/CALDaNm1joke42n68LdegN5wCpaeoOMex2EHcdZrVZnGD3UhfNQ@mail.gmail.com
[3] /messages/by-id/TY3PR01MB98895BA6C1D72CB8582CACC4F5682@TY3PR01MB9889.jpnprd01.prod.outlook.com
[4] /messages/by-id/TY3PR01MB988978C7362A101927070D29F56A2@TY3PR01MB9889.jpnprd01.prod.outlook.com

+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>create a new logical replica from a standby server</refpurpose>
+ </refnamediv>
I'm a bit confused about this wording because we are converting a standby
to a logical replica but not creating a new logical replica and leaving the
standby as is. How about:

<refpurpose>convert a standby replica to a logical replica</refpurpose>

+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>

What is *local logical replication*? I can't find any clue in the patch, can you
give me some hint?

--
Euler Taveira
EDB https://www.enterprisedb.com/

--
Regards
Junwang Zhao

#68Shubham Khanna
khannashubham1197@gmail.com
In reply to: Amit Kapila (#28)
3 attachment(s)
Re: speed up a logical replica setup

On Thu, Dec 21, 2023 at 11:47 AM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Wed, Dec 6, 2023 at 12:53 PM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Nov 9, 2023, at 8:12 PM, Michael Paquier wrote:

On Thu, Nov 09, 2023 at 03:41:53PM +0100, Peter Eisentraut wrote:

On 08.11.23 00:12, Michael Paquier wrote:

- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?

Seems like unnecessary churn. Nobody has complained about any of the other
tools in there.

Not sure. We rename things across releases in the tree from time to
time, and here that's straight-forward.

Based on this discussion it seems we have a consensus that this tool should be
in the pg_basebackup directory. (If/when we agree with the directory renaming,
it could be done in a separate patch.) Besides this move, the v3 provides a dry
run mode. It basically executes every routine but skip when should do
modifications. It is an useful option to check if you will be able to run it
without having issues with connectivity, permission, and existing objects
(replication slots, publications, subscriptions). Tests were slightly improved.
Messages were changed to *not* provide INFO messages by default and --verbose
provides INFO messages and --verbose --verbose also provides DEBUG messages. I
also refactored the connect_database() function into which the connection will
always use the logical replication mode. A bug was fixed in the transient
replication slot name. Ashutosh review [1] was included. The code was also indented.

There are a few suggestions from Ashutosh [2] that I will reply in another
email.

I'm still planning to work on the following points:

1. improve the cleanup routine to point out leftover objects if there is any
connection issue.

I think this is an important part. Shall we try to write to some file
the pending objects to be cleaned up? We do something like that during
the upgrade.

2. remove the physical replication slot if the standby is using one
(primary_slot_name).
3. provide instructions to promote the logical replica into primary, I mean,
stop the replication between the nodes and remove the replication setup
(publications, subscriptions, replication slots). Or even include another
action to do it. We could add both too.

Point 1 should be done. Points 2 and 3 aren't essential but will provide a nice
UI for users that would like to use it.

Isn't point 2 also essential because how would otherwise such a slot
be advanced or removed?

A few other points:
==============
1. Previously, I asked whether we need an additional replication slot
patch created to get consistent LSN and I see the following comment in
the patch:

+ *
+ * XXX we should probably use the last created replication slot to get a
+ * consistent LSN but it should be changed after adding pg_basebackup
+ * support.

Yeah, sure, we may want to do that after backup support and we can
keep a comment for the same but I feel as the patch stands today,
there is no good reason to keep it. Also, is there a reason that we
can't create the slots after backup is complete and before we write
recovery parameters

2.
+ appendPQExpBuffer(str,
+   "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+   "WITH (create_slot = false, copy_data = false, enabled = false)",
+   dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);

Shouldn't we enable two_phase by default for newly created
subscriptions? Is there a reason for not doing so?

3. How about sync slots on the physical standby if present? Do we want
to retain those as it is or do we need to remove those? We are
actively working on the patch [1] for the same.

4. Can we see some numbers with various sizes of databases (cluster)
to see how it impacts the time for small to large-size databases as
compared to the traditional method? This might help us with giving
users advice on when to use this tool. We can do this bit later as
well when the patch is closer to being ready for commit.

I have done the Performance testing and attached the results to
compare the 'Execution Time' between 'logical replication' and
'pg_subscriber' for 100MB, 1GB and 5GB data:
| 100MB | 1GB | 5GB
Logical rep (2 w) | 1.815s | 14.895s | 75.541s
Logical rep (4 w) | 1.194s | 9.484s | 46.938s
Logical rep (8 w) | 0.828s | 6.422s | 31.704s
Logical rep(10 w)| 0.646s | 3.843s | 18.425s
pg_subscriber | 3.977s | 9.988s | 12.665s

Here, 'w' stands for 'workers'. I have included the tests to see the
test result variations with different values for
'max_sync_workers_per_subscription' ranging from 2 to 10. I ran the
tests for different data records; for 100MB I put 3,00,000 Records,
for 1GB I put 30,00,000 Records and for 5GB I put 1,50,00,000 Records.
It is observed that 'pg_subscriber' is better when the table size is
more.
Next I plan to run these tests for 10GB and 20GB to see if this trend
continues or not.
Attaching the script files which have the details of the test scripts
used and the excel file has the test run details. The
'pg_subscriber.pl' file is to test with 'pg_subscriber' and the
'logical_rep.pl' file is to test with 'Logical Replication'.

Thanks and Regards,
Shubham Khanna.

Attachments:

pg_subscriber.plapplication/octet-stream; name=pg_subscriber.plDownload
logical_replication.plapplication/octet-stream; name=logical_replication.plDownload
time_stamps(comparison).xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet; name="time_stamps(comparison).xlsx"Download
#69Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#59)
2 attachment(s)
RE: speed up a logical replica setup

Dear Euler, hackers,

I found that some bugs which have been reported by Shlok were not fixed, so
made a top-up patch. 0001 was not changed, and 0002 contains below:

* Add a timeout option for the recovery option, per [1]/messages/by-id/CANhcyEUCt-g4JLQU3Q3ofFk_Vt-Tqh3ZdXoLcpT8fjz9LY_-ww@mail.gmail.com. The code was basically ported from pg_ctl.c.
* Reject if the target server is not a standby, per [2]/messages/by-id/CANhcyEUCt-g4JLQU3Q3ofFk_Vt-Tqh3ZdXoLcpT8fjz9LY_-ww@mail.gmail.com
* Raise FATAL error if --subscriber-conninfo specifies non-local server, per [3]/messages/by-id/TY3PR01MB98895BA6C1D72CB8582CACC4F5682@TY3PR01MB9889.jpnprd01.prod.outlook.com
(not sure it is really needed, so feel free reject the part.)

Feel free to merge parts of 0002 if it looks good to you.
Thanks Shlok to make a part of patch.

[1]: /messages/by-id/CANhcyEUCt-g4JLQU3Q3ofFk_Vt-Tqh3ZdXoLcpT8fjz9LY_-ww@mail.gmail.com
[2]: /messages/by-id/CANhcyEUCt-g4JLQU3Q3ofFk_Vt-Tqh3ZdXoLcpT8fjz9LY_-ww@mail.gmail.com
[3]: /messages/by-id/TY3PR01MB98895BA6C1D72CB8582CACC4F5682@TY3PR01MB9889.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

Attachments:

v20240117-0001-Creates-a-new-logical-replica-from-a-stand.patchapplication/octet-stream; name=v20240117-0001-Creates-a-new-logical-replica-from-a-stand.patchDownload
From 6fef619cc529b056e85c512773f07fa53f494ca0 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v20240117 1/2] Creates a new logical replica from a standby
 server

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server. Remove
the additional replication slot that was used to get the consistent LSN.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_subscriber.sgml           |  284 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_subscriber.c         | 1657 +++++++++++++++++
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   44 +
 .../t/041_pg_subscriber_standby.pl            |  139 ++
 9 files changed, 2153 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_subscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_subscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_subscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..3862c976d7 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgSubscriber       SYSTEM "pg_subscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
new file mode 100644
index 0000000000..553185c35f
--- /dev/null
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -0,0 +1,284 @@
+<!--
+doc/src/sgml/ref/pg_subscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgsubscriber">
+ <indexterm zone="app-pgsubscriber">
+  <primary>pg_subscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_subscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>create a new logical replica from a standby server</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_subscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a standby server and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_subscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a standby
+        server.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--publisher-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_subscriber</application> to output progress messages
+        and detailed information about each step.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_subscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_subscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_subscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the target data
+     directory is used by a standby server. Stop the standby server if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_subscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  Another
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_subscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_subscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_subscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_subscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> removes the additional replication
+     slot that was used to get the consistent LSN on the source server.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a standby server at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..266f4e515a 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..0e5384a1d5 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_subscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..f6281b7676 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_subscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_subscriber: $(WIN32RES) pg_subscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_subscriber$(X) '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_subscriber$(X) pg_subscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..ccfd7bb7a5 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_subscriber_sources = files(
+  'pg_subscriber.c'
+)
+
+if host_system == 'windows'
+  pg_subscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_subscriber',
+	'--FILEDESC', 'pg_subscriber - create a new logical replica from a standby server',])
+endif
+
+pg_subscriber = executable('pg_subscriber',
+  pg_subscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_subscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_subscriber.pl',
+      't/041_pg_subscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
new file mode 100644
index 0000000000..e998c29f9e
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -0,0 +1,1657 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_subscriber/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static char *use_primary_slot_name(void);
+static bool create_all_logical_replication_slots(LogicalRepInfo *dbinfo);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static char temp_replslot[NAMEDATALEN] = {0};
+static bool made_transient_replslot = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_subscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+
+	if (made_transient_replslot)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+			disconnect_database(conn);
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-conninfo=CONNINFO   publisher connection string\n"));
+	printf(_(" -S, --subscriber-conninfo=CONNINFO  subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name plus a fallback application name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	if (i > 0)
+		appendPQExpBufferChar(buf, ' ');
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Return a palloc'd slot name if the replication is using one.
+ */
+static char *
+use_primary_slot_name(void)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+	char	   *slot_name;
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT setting FROM pg_settings WHERE name = 'primary_slot_name'");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain parameter information: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+
+	/*
+	 * If primary_slot_name is an empty string, the current replication
+	 * connection is not using a replication slot, bail out.
+	 */
+	if (strcmp(PQgetvalue(res, 0, 0), "") == 0)
+	{
+		PQclear(res);
+		return NULL;
+	}
+
+	slot_name = pg_strdup(PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_replication_slots r INNER JOIN pg_stat_activity a ON (r.active_pid = a.pid) WHERE slot_name = '%s'", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		return NULL;
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return slot_name;
+}
+
+static bool
+create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+{
+	int			i;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
+		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
+		 * to reduce the probability of collision. By default, subscription
+		 * name is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_subscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (transient_replslot)
+		made_transient_replslot = true;
+	else
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_subscriber must have created this
+		 * publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_subscriber_ prefix followed by the exact
+			 * database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-conninfo", required_argument, NULL, 'P'},
+		{"subscriber-conninfo", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+	int			rc;
+
+	char	   *pg_ctl_cmd;
+
+	char	   *base_dir;
+	char	   *server_start_log;
+
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	int			i;
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_subscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&database_names, optarg))
+				{
+					simple_string_list_append(&database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("directory path for subscriber is too long");
+		exit(1);
+	}
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+	{
+		pg_log_error("could not create directory \"%s\": %m", base_dir);
+		exit(1);
+	}
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * Stop the subscriber if it is a standby server. Before executing the
+	 * transformation steps, make sure the subscriber is not running because
+	 * one of the steps is to modify some recovery parameters that require a
+	 * restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Since the standby server is running, check if it is using an
+		 * existing replication slot for WAL retention purposes. This
+		 * replication slot has no use after the transformation, hence, it
+		 * will be removed at the end of this process.
+		 */
+		primary_slot_name = use_primary_slot_name();
+		if (primary_slot_name != NULL)
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+
+		pg_log_info("subscriber is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+	}
+
+	/*
+	 * Create a replication slot for each database on the publisher.
+	 */
+	if (!create_all_logical_replication_slots(dbinfo))
+		exit(1);
+
+	/*
+	 * Create a logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. We cannot use the last created replication slot
+	 * because the consistent LSN should be obtained *after* the base backup
+	 * finishes (and the base backup should include the logical replication
+	 * slots).
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 *
+	 * A temporary replication slot is not used here to avoid keeping a
+	 * replication connection open (depending when base backup was taken, the
+	 * connection should be open for a few hours).
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	server_start_log = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("log file path is too long");
+		exit(1);
+	}
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create a publication for each database. This step should be executed
+	 * after promoting the subscriber to avoid replicating unnecessary
+	 * objects.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+
+		/* Connect to publisher. */
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		create_publication(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Create a subscription for each database.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * The transient replication slot is no longer required. Drop it.
+	 *
+	 * If the physical replication slot exists, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops the replication slot later.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+	{
+		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		if (primary_slot_name != NULL)
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+	}
+	else
+	{
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		if (primary_slot_name != NULL)
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	/*
+	 * Remove log file generated by this tool, if it runs successfully.
+	 * Otherwise, file is kept that may provide useful debugging information.
+	 */
+	unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
new file mode 100644
index 0000000000..4ebff76b2d
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_subscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_subscriber');
+program_version_ok('pg_subscriber');
+program_options_handling_ok('pg_subscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_subscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+		'--subscriber-conninfo', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
new file mode 100644
index 0000000000..fbcd0fc82b
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_subscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_subscriber on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_subscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
-- 
2.43.0

v20240117-0002-Address-some-comments-proposed-on-hackers.patchapplication/octet-stream; name=v20240117-0002-Address-some-comments-proposed-on-hackers.patchDownload
From 2f8c9faa7705ec13c2e140045faf393a8cc6928a Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 12 Jan 2024 15:55:30 +0530
Subject: [PATCH v20240117 2/2] Address some comments proposed on -hackers

This patch contains below changes.

* Add Timeout option and default timeout while waiting the recovery
* Restrict the target to be a standby node
* Reject when the --subscriber-conninfo specifies non-local server
---
 src/bin/pg_basebackup/pg_subscriber.c        | 153 +++++++++++++++----
 src/bin/pg_basebackup/t/040_pg_subscriber.pl |   8 +
 2 files changed, 133 insertions(+), 28 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index e998c29f9e..2414c0f7ed 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -28,6 +28,7 @@
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
+#include "libpq/pqcomm.h"
 #include "utils/pidfile.h"
 
 #define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
@@ -75,9 +76,13 @@ static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
 static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void start_standby_server(char *server_start_log);
 
 #define	USEC_PER_SEC	1000000
-#define	WAIT_INTERVAL	1		/* 1 second */
+#define DEFAULT_WAIT	60
+#define WAITS_PER_SEC	10		/* should divide USEC_PER_SEC evenly */
+
+static int	wait_seconds = DEFAULT_WAIT;
 
 /* Options */
 static const char *progname;
@@ -222,6 +227,27 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 			continue;
 		}
 
+		/*
+		 * If the dbname is NULL (this means the conninfo is for the
+		 * subscriber), we also check that the connection string does not
+		 * specify the non-local server.
+		 */
+		if (!dbname &&
+			conn_opt->val != NULL &&
+			(strcmp(conn_opt->keyword, "host") == 0 ||
+			 strcmp(conn_opt->keyword, "hostaddr") == 0))
+		{
+			const char *value = conn_opt->val;
+
+			if (strlen(value) > 0 &&
+			/* check for 'local' host values */
+				(strcmp(value, "localhost") != 0 &&
+				 strcmp(value, "127.0.0.1") != 0 &&
+				 strcmp(value, "::1") != 0 && !is_unixsock_path(value)))
+				pg_fatal("--subscriber-conninfo must not be non-local connection: %s",
+						 value);
+		}
+
 		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
 		{
 			if (i > 0)
@@ -830,6 +856,9 @@ wait_for_end_recovery(const char *conninfo)
 	PGconn	   *conn;
 	PGresult   *res;
 	int			status = POSTMASTER_STILL_STARTING;
+	int			cnt;
+	int			rc;
+	char	   *pg_ctl_cmd;
 
 	pg_log_info("waiting the postmaster to reach the consistent state");
 
@@ -837,7 +866,7 @@ wait_for_end_recovery(const char *conninfo)
 	if (conn == NULL)
 		exit(1);
 
-	for (;;)
+	for (cnt = 0; cnt < wait_seconds * WAITS_PER_SEC; cnt++)
 	{
 		bool		in_recovery;
 
@@ -870,11 +899,25 @@ wait_for_end_recovery(const char *conninfo)
 		}
 
 		/* Keep waiting. */
-		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+		pg_usleep(USEC_PER_SEC / WAITS_PER_SEC);
 	}
 
 	disconnect_database(conn);
 
+	/*
+	 * if timeout is reached exit the pg_subscriber and stop the standby node
+	 */
+	if (cnt >= wait_seconds * WAITS_PER_SEC)
+	{
+		pg_log_error("recovery timed out");
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+		exit(1);
+	}
+
 	if (status == POSTMASTER_STILL_STARTING)
 	{
 		pg_log_error("server did not end recovery");
@@ -1203,6 +1246,39 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	destroyPQExpBuffer(str);
 }
 
+static void
+start_standby_server(char *server_start_log)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	int			rc;
+	char	   *pg_ctl_cmd;
+
+	if (server_start_log[0] == '\0')
+	{
+		/* append timestamp with ISO 8601 format. */
+		gettimeofday(&time, NULL);
+		tt = (time_t) time.tv_sec;
+		strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+		snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+				 ".%03d", (int) (time.tv_usec / 1000));
+
+
+		len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
+		if (len >= MAXPGPATH)
+		{
+			pg_log_error("log file path is too long");
+			exit(1);
+		}
+	}
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
 int
 main(int argc, char **argv)
 {
@@ -1214,6 +1290,7 @@ main(int argc, char **argv)
 		{"publisher-conninfo", required_argument, NULL, 'P'},
 		{"subscriber-conninfo", required_argument, NULL, 'S'},
 		{"database", required_argument, NULL, 'd'},
+		{"timeout", required_argument, NULL, 't'},
 		{"dry-run", no_argument, NULL, 'n'},
 		{"verbose", no_argument, NULL, 'v'},
 		{NULL, 0, NULL, 0}
@@ -1226,11 +1303,7 @@ main(int argc, char **argv)
 	char	   *pg_ctl_cmd;
 
 	char	   *base_dir;
-	char	   *server_start_log;
-
-	char		timebuf[128];
-	struct timeval time;
-	time_t		tt;
+	char		server_start_log[MAXPGPATH] = {0};
 	int			len;
 
 	char	   *pub_base_conninfo = NULL;
@@ -1250,6 +1323,8 @@ main(int argc, char **argv)
 
 	int			i;
 
+	PGresult   *res;
+
 	pg_logging_init(argv[0]);
 	pg_logging_set_level(PG_LOG_WARNING);
 	progname = get_progname(argv[0]);
@@ -1286,7 +1361,7 @@ main(int argc, char **argv)
 	}
 #endif
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+	while ((c = getopt_long(argc, argv, "D:P:S:d:t:nv",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1308,6 +1383,9 @@ main(int argc, char **argv)
 					num_dbs++;
 				}
 				break;
+			case 't':
+				wait_seconds = atoi(optarg);
+				break;
 			case 'n':
 				dry_run = true;
 				break;
@@ -1443,6 +1521,43 @@ main(int argc, char **argv)
 	/* subscriber PID file. */
 	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
 
+	/*
+	 * Start the standby server if it not running
+	 */
+	if (stat(pidfile, &statbuf) != 0)
+		start_standby_server(server_start_log);
+
+	/*
+	 * Exit the pg_subscriber if the node is not a standby server.
+	 */
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("unexpected result from pg_is_in_recovery function");
+		exit(1);
+	}
+
+	/* check if the server is in recovery */
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("pg_subscriber is supported only on standby server");
+		exit(1);
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
 	/*
 	 * Stop the subscriber if it is a standby server. Before executing the
 	 * transformation steps, make sure the subscriber is not running because
@@ -1532,25 +1647,7 @@ main(int argc, char **argv)
 	 * Start subscriber and wait until accepting connections.
 	 */
 	pg_log_info("starting the subscriber");
-
-	/* append timestamp with ISO 8601 format. */
-	gettimeofday(&time, NULL);
-	tt = (time_t) time.tv_sec;
-	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
-	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
-			 ".%03d", (int) (time.tv_usec / 1000));
-
-	server_start_log = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
-	if (len >= MAXPGPATH)
-	{
-		pg_log_error("log file path is too long");
-		exit(1);
-	}
-
-	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
-	rc = system(pg_ctl_cmd);
-	pg_ctl_status(pg_ctl_cmd, rc, 1);
+	start_standby_server(server_start_log);
 
 	/*
 	 * Waiting the subscriber to be promoted.
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
index 4ebff76b2d..e653df174d 100644
--- a/src/bin/pg_basebackup/t/040_pg_subscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -40,5 +40,13 @@ command_fails(
 		'--subscriber-conninfo', 'dbname=postgres'
 	],
 	'no database name specified');
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+		'--subscriber-conninfo', 'host=192.0.2.1 dbname=postgres'
+	],
+	'subscriber connection string specnfied non-local server');
 
 done_testing();
-- 
2.43.0

#70Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Euler Taveira (#59)
Re: speed up a logical replica setup

Hi,

I have some comments for the v5 patch:

1.
```
+ base_dir = (char *) pg_malloc0(MAXPGPATH);
+ len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
```
Before these lines, I think we should use
'canonicalize_path(subscriber_dir)' to remove extra unnecessary
characters. This function is used in many places like initdb.c,
pg_ctl.c, pg_basebakup.c, etc

2.
I also feels that there are many global variables and can be arranged
as structures as suggested by Kuroda-san in [1]/messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com

[1]: /messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com

Thanks and Regards
Shlok Kyal

Show quoted text

On Fri, 12 Jan 2024 at 03:46, Euler Taveira <euler@eulerto.com> wrote:

On Thu, Jan 11, 2024, at 9:18 AM, Hayato Kuroda (Fujitsu) wrote:

I have been concerned that the patch has not been tested by cfbot due to the
application error. Also, some comments were raised. Therefore, I created a patch
to move forward.

Let me send an updated patch to hopefully keep the CF bot happy. The following
items are included in this patch:

* drop physical replication slot if standby is using one [1].
* cleanup small changes (copyright, .gitignore) [2][3]
* fix getopt_long() options [2]
* fix format specifier for some messages
* move doc to Server Application section [4]
* fix assert failure
* ignore duplicate database names [2]
* store subscriber server log into a separate file
* remove MSVC support

I'm still addressing other reviews and I'll post another version that includes
it soon.

[1] /messages/by-id/e02a2c17-22e5-4ba6-b788-de696ab74f1e@app.fastmail.com
[2] /messages/by-id/CALDaNm1joke42n68LdegN5wCpaeoOMex2EHcdZrVZnGD3UhfNQ@mail.gmail.com
[3] /messages/by-id/TY3PR01MB98895BA6C1D72CB8582CACC4F5682@TY3PR01MB9889.jpnprd01.prod.outlook.com
[4] /messages/by-id/TY3PR01MB988978C7362A101927070D29F56A2@TY3PR01MB9889.jpnprd01.prod.outlook.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

#71Peter Eisentraut
peter@eisentraut.org
In reply to: Euler Taveira (#59)
Re: speed up a logical replica setup

On 11.01.24 23:15, Euler Taveira wrote:

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server (subscriber).

Can we have a discussion on the name?

I find the name pg_subscriber too general.

The replication/backup/recovery tools in PostgreSQL are usually named
along the lines of "verb - object". (Otherwise, they would all be
called "pg_backup"??) Moreover, "pg_subscriber" also sounds like the
name of the program that runs the subscriber itself, like what the
walreceiver does now.

Very early in this thread, someone mentioned the name
pg_create_subscriber, and of course there is pglogical_create_subscriber
as the historical predecessor. Something along those lines seems better
to me. Maybe there are other ideas.

#72Amit Kapila
amit.kapila16@gmail.com
In reply to: Peter Eisentraut (#71)
Re: speed up a logical replica setup

On Thu, Jan 18, 2024 at 2:49 PM Peter Eisentraut <peter@eisentraut.org> wrote:

On 11.01.24 23:15, Euler Taveira wrote:

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server (subscriber).

Can we have a discussion on the name?

I find the name pg_subscriber too general.

The replication/backup/recovery tools in PostgreSQL are usually named
along the lines of "verb - object". (Otherwise, they would all be
called "pg_backup"??) Moreover, "pg_subscriber" also sounds like the
name of the program that runs the subscriber itself, like what the
walreceiver does now.

Very early in this thread, someone mentioned the name
pg_create_subscriber, and of course there is pglogical_create_subscriber
as the historical predecessor. Something along those lines seems better
to me. Maybe there are other ideas.

The other option could be pg_createsubscriber on the lines of
pg_verifybackup and pg_combinebackup. Yet other options could be
pg_buildsubscriber, pg_makesubscriber as 'build' or 'make' in the name
sounds like we are doing some work to create the subscriber which I
think is the case here.

--
With Regards,
Amit Kapila.

#73Amit Kapila
amit.kapila16@gmail.com
In reply to: Shubham Khanna (#68)
Re: speed up a logical replica setup

On Tue, Jan 16, 2024 at 11:58 AM Shubham Khanna
<khannashubham1197@gmail.com> wrote:

On Thu, Dec 21, 2023 at 11:47 AM Amit Kapila <amit.kapila16@gmail.com> wrote:

4. Can we see some numbers with various sizes of databases (cluster)
to see how it impacts the time for small to large-size databases as
compared to the traditional method? This might help us with giving
users advice on when to use this tool. We can do this bit later as
well when the patch is closer to being ready for commit.

I have done the Performance testing and attached the results to
compare the 'Execution Time' between 'logical replication' and
'pg_subscriber' for 100MB, 1GB and 5GB data:
| 100MB | 1GB | 5GB
Logical rep (2 w) | 1.815s | 14.895s | 75.541s
Logical rep (4 w) | 1.194s | 9.484s | 46.938s
Logical rep (8 w) | 0.828s | 6.422s | 31.704s
Logical rep(10 w)| 0.646s | 3.843s | 18.425s
pg_subscriber | 3.977s | 9.988s | 12.665s

Here, 'w' stands for 'workers'. I have included the tests to see the
test result variations with different values for
'max_sync_workers_per_subscription' ranging from 2 to 10. I ran the
tests for different data records; for 100MB I put 3,00,000 Records,
for 1GB I put 30,00,000 Records and for 5GB I put 1,50,00,000 Records.
It is observed that 'pg_subscriber' is better when the table size is
more.

Thanks for the tests. IIUC, it shows for smaller data this tool can
take more time. Can we do perf to see if there is something we can do
about reducing the overhead?

Next I plan to run these tests for 10GB and 20GB to see if this trend
continues or not.

Okay, that makes sense.

With Regards,
Amit Kapila.

#74Shubham Khanna
khannashubham1197@gmail.com
In reply to: Shubham Khanna (#68)
3 attachment(s)
Re: speed up a logical replica setup

On Tue, Jan 16, 2024 at 11:58 AM Shubham Khanna
<khannashubham1197@gmail.com> wrote:

On Thu, Dec 21, 2023 at 11:47 AM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Wed, Dec 6, 2023 at 12:53 PM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Nov 9, 2023, at 8:12 PM, Michael Paquier wrote:

On Thu, Nov 09, 2023 at 03:41:53PM +0100, Peter Eisentraut wrote:

On 08.11.23 00:12, Michael Paquier wrote:

- Should the subdirectory pg_basebackup be renamed into something more
generic at this point? All these things are frontend tools that deal
in some way with the replication protocol to do their work. Say
a replication_tools?

Seems like unnecessary churn. Nobody has complained about any of the other
tools in there.

Not sure. We rename things across releases in the tree from time to
time, and here that's straight-forward.

Based on this discussion it seems we have a consensus that this tool should be
in the pg_basebackup directory. (If/when we agree with the directory renaming,
it could be done in a separate patch.) Besides this move, the v3 provides a dry
run mode. It basically executes every routine but skip when should do
modifications. It is an useful option to check if you will be able to run it
without having issues with connectivity, permission, and existing objects
(replication slots, publications, subscriptions). Tests were slightly improved.
Messages were changed to *not* provide INFO messages by default and --verbose
provides INFO messages and --verbose --verbose also provides DEBUG messages. I
also refactored the connect_database() function into which the connection will
always use the logical replication mode. A bug was fixed in the transient
replication slot name. Ashutosh review [1] was included. The code was also indented.

There are a few suggestions from Ashutosh [2] that I will reply in another
email.

I'm still planning to work on the following points:

1. improve the cleanup routine to point out leftover objects if there is any
connection issue.

I think this is an important part. Shall we try to write to some file
the pending objects to be cleaned up? We do something like that during
the upgrade.

2. remove the physical replication slot if the standby is using one
(primary_slot_name).
3. provide instructions to promote the logical replica into primary, I mean,
stop the replication between the nodes and remove the replication setup
(publications, subscriptions, replication slots). Or even include another
action to do it. We could add both too.

Point 1 should be done. Points 2 and 3 aren't essential but will provide a nice
UI for users that would like to use it.

Isn't point 2 also essential because how would otherwise such a slot
be advanced or removed?

A few other points:
==============
1. Previously, I asked whether we need an additional replication slot
patch created to get consistent LSN and I see the following comment in
the patch:

+ *
+ * XXX we should probably use the last created replication slot to get a
+ * consistent LSN but it should be changed after adding pg_basebackup
+ * support.

Yeah, sure, we may want to do that after backup support and we can
keep a comment for the same but I feel as the patch stands today,
there is no good reason to keep it. Also, is there a reason that we
can't create the slots after backup is complete and before we write
recovery parameters

2.
+ appendPQExpBuffer(str,
+   "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+   "WITH (create_slot = false, copy_data = false, enabled = false)",
+   dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);

Shouldn't we enable two_phase by default for newly created
subscriptions? Is there a reason for not doing so?

3. How about sync slots on the physical standby if present? Do we want
to retain those as it is or do we need to remove those? We are
actively working on the patch [1] for the same.

4. Can we see some numbers with various sizes of databases (cluster)
to see how it impacts the time for small to large-size databases as
compared to the traditional method? This might help us with giving
users advice on when to use this tool. We can do this bit later as
well when the patch is closer to being ready for commit.

I have done the Performance testing and attached the results to
compare the 'Execution Time' between 'logical replication' and
'pg_subscriber' for 100MB, 1GB and 5GB data:
| 100MB | 1GB | 5GB
Logical rep (2 w) | 1.815s | 14.895s | 75.541s
Logical rep (4 w) | 1.194s | 9.484s | 46.938s
Logical rep (8 w) | 0.828s | 6.422s | 31.704s
Logical rep(10 w)| 0.646s | 3.843s | 18.425s
pg_subscriber | 3.977s | 9.988s | 12.665s

Here, 'w' stands for 'workers'. I have included the tests to see the
test result variations with different values for
'max_sync_workers_per_subscription' ranging from 2 to 10. I ran the
tests for different data records; for 100MB I put 3,00,000 Records,
for 1GB I put 30,00,000 Records and for 5GB I put 1,50,00,000 Records.
It is observed that 'pg_subscriber' is better when the table size is
more.
Next I plan to run these tests for 10GB and 20GB to see if this trend
continues or not.

I have done the Performance testing and attached the results to
compare the 'Execution Time' between 'logical replication' and
'pg_subscriber' for 10GB and 20GB data:
| 10GB | 20GB
Logical rep (2 w) | 157.131s| 343.191s
Logical rep (4 w) | 116.627s| 240.480s
Logical rep (8 w) | 95.237s | 275.715s
Logical rep(10 w)| 92.792s | 280.538s
pg_subscriber | 22.734s | 25.661s

As expected, we can see that pg_subscriber is very much better in
ideal cases with approximately 7x times better in case of 10GB and 13x
times better in case of 20GB.
I'm attaching the script files which have the details of the test
scripts used and the excel file has the test run details. The
'pg_subscriber.pl' file is for 'Streaming Replication' and the
'logical_replication.pl' file is for 'Logical Replication'.
Note: For 20GB the record count should be changed to 6,00,00,000 and
the 'max_sync_workers_per_subscription' needs to be adjusted for
different logical replication tests with different workers.

Thanks and Regards,
Shubham Khanna.

Attachments:

logical_replication.plapplication/octet-stream; name=logical_replication.plDownload
pg_subscriber.plapplication/octet-stream; name=pg_subscriber.plDownload
time_stamp(comparison).xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet; name="time_stamp(comparison).xlsx"Download
#75Peter Eisentraut
peter@eisentraut.org
In reply to: Amit Kapila (#72)
Re: speed up a logical replica setup

On 18.01.24 10:37, Amit Kapila wrote:

The other option could be pg_createsubscriber on the lines of
pg_verifybackup and pg_combinebackup.

Yes, that spelling would be more consistent.

Yet other options could be
pg_buildsubscriber, pg_makesubscriber as 'build' or 'make' in the name
sounds like we are doing some work to create the subscriber which I
think is the case here.

I see your point here. pg_createsubscriber is not like createuser in
that it just runs an SQL command. It does something different than
CREATE SUBSCRIBER. So a different verb would make that clearer. Maybe
something from here: https://www.thesaurus.com/browse/convert

#76Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#64)
RE: speed up a logical replica setup

Dear hackers,

15.
I found that subscriptions cannot be started if tuples are inserted on publisher
after creating temp_replslot. After starting a subscriber, I got below output on the
log.

```
ERROR: could not receive data from WAL stream: ERROR: publication
"pg_subscriber_5" does not exist
CONTEXT: slot "pg_subscriber_5_3632", output plugin "pgoutput", in the change
callback, associated LSN 0/30008A8
LOG: background worker "logical replication apply worker" (PID 3669) exited
with exit code 1
```

But this is strange. I confirmed that the specified publication surely exists.
Do you know the reason?

```
publisher=# SELECT pubname FROM pg_publication;
pubname
-----------------
pg_subscriber_5
(1 row)
```

I analyzed and found a reason. This is because publications are invisible for some transactions.

As the first place, below operations were executed in this case.
Tuples were inserted after getting consistent_lsn, but before starting the standby.
After doing the workload, I confirmed again that the publication was created.

1. on primary, logical replication slots were created.
2. on primary, another replication slot was created.
3. ===on primary, some tuples were inserted. ===
4. on standby, a server process was started
5. on standby, the process waited until all changes have come.
6. on primary, publications were created.
7. on standby, subscriptions were created.
8. on standby, a replication progress for each subscriptions was set to given LSN (got at step2).
=====pg_subscriber finished here=====
9. on standby, a server process was started again
10. on standby, subscriptions were enabled. They referred slots created at step1.
11. on primary, decoding was started but ERROR was raised.

In this case, tuples were inserted *before creating publication*.
So I thought that the decoded transaction could not see the publication because
it was committed after insertions.

One solution is to create a publication before creating a consistent slot.
Changes which came before creating the slot were surely replicated to the standby,
so upcoming transactions can see the object. We are planning to patch set to fix
the issue in this approach.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

#77Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Peter Eisentraut (#75)
RE: speed up a logical replica setup

Dear Peter,

Yet other options could be
pg_buildsubscriber, pg_makesubscriber as 'build' or 'make' in the name
sounds like we are doing some work to create the subscriber which I
think is the case here.

I see your point here. pg_createsubscriber is not like createuser in
that it just runs an SQL command. It does something different than
CREATE SUBSCRIBER. So a different verb would make that clearer. Maybe
something from here: https://www.thesaurus.com/browse/convert

I read the link and found a good verb "switch". So, how about using "pg_switchsubscriber"?

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

#78Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#77)
Re: speed up a logical replica setup

On Mon, Jan 22, 2024 at 2:38 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Yet other options could be
pg_buildsubscriber, pg_makesubscriber as 'build' or 'make' in the name
sounds like we are doing some work to create the subscriber which I
think is the case here.

I see your point here. pg_createsubscriber is not like createuser in
that it just runs an SQL command. It does something different than
CREATE SUBSCRIBER.

Right.

So a different verb would make that clearer. Maybe

something from here: https://www.thesaurus.com/browse/convert

I read the link and found a good verb "switch". So, how about using "pg_switchsubscriber"?

I also initially thought on these lines and came up with a name like
pg_convertsubscriber but didn't feel strongly about it as that would
have sounded meaningful if we use a name like
pg_convertstandbytosubscriber. Now, that has become too long. Having
said that, I am not opposed to it having a name on those lines. BTW,
another option that occurred to me today is pg_preparesubscriber. We
internally create slots and then wait for wal, etc. which makes me
sound like adding 'prepare' in the name can also explain the purpose.

--
With Regards,
Amit Kapila.

#79Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#76)
3 attachment(s)
Re: speed up a logical replica setup

Dear Euler, hackers,

We fixed some of the comments posted in the thread. We have created it
as top-up patch 0002 and 0003.

0002 patch contains the following changes:
* Add a timeout option for the recovery option, per [1]/messages/by-id/CANhcyEUCt-g4JLQU3Q3ofFk_Vt-Tqh3ZdXoLcpT8fjz9LY_-ww@mail.gmail.com. The code was
basically ported from pg_ctl.c.
* Reject if the target server is not a standby, per [2]/messages/by-id/CANhcyEUCt-g4JLQU3Q3ofFk_Vt-Tqh3ZdXoLcpT8fjz9LY_-ww@mail.gmail.com
* Raise FATAL error if --subscriber-conninfo specifies non-local server, per [3]/messages/by-id/TY3PR01MB98895BA6C1D72CB8582CACC4F5682@TY3PR01MB9889.jpnprd01.prod.outlook.com
(not sure it is really needed, so feel free reject the part.)
* Add check for max_replication_slots and wal_level; as per [4]/messages/by-id/CALDaNm098Jkbh+ye6zMj9Ro9j1bBe6FfPV80BFbs1=pUuTJ07g@mail.gmail.com
* Add -u and -p options; as per [5]/messages/by-id/CAA4eK1JB_ko7a5JMS3WfAn583RadAKCDhiE9JgmfMA8ZZ5xcQw@mail.gmail.com
* Addressed comment except 5 and 8 in [6]/messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com and comment in [7]/messages/by-id/CANhcyEXjGmryoZPACS_i-joqvcz5e6Zb3u4g38SAy_iSTGhShg@mail.gmail.com

0003 patch contains fix for bug reported in [8]/messages/by-id/TY3PR01MB9889C5D55206DDD978627D07F5752@TY3PR01MB9889.jpnprd01.prod.outlook.com.

Feel free to merge parts of 0002 and 0003 if it looks good to you.
Thanks Kuroda-san to make patch 0003 and a part of patch 0002.

[1]: /messages/by-id/CANhcyEUCt-g4JLQU3Q3ofFk_Vt-Tqh3ZdXoLcpT8fjz9LY_-ww@mail.gmail.com
[2]: /messages/by-id/CANhcyEUCt-g4JLQU3Q3ofFk_Vt-Tqh3ZdXoLcpT8fjz9LY_-ww@mail.gmail.com
[3]: /messages/by-id/TY3PR01MB98895BA6C1D72CB8582CACC4F5682@TY3PR01MB9889.jpnprd01.prod.outlook.com
[4]: /messages/by-id/CALDaNm098Jkbh+ye6zMj9Ro9j1bBe6FfPV80BFbs1=pUuTJ07g@mail.gmail.com
[5]: /messages/by-id/CAA4eK1JB_ko7a5JMS3WfAn583RadAKCDhiE9JgmfMA8ZZ5xcQw@mail.gmail.com
[6]: /messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com
[7]: /messages/by-id/CANhcyEXjGmryoZPACS_i-joqvcz5e6Zb3u4g38SAy_iSTGhShg@mail.gmail.com
[8]: /messages/by-id/TY3PR01MB9889C5D55206DDD978627D07F5752@TY3PR01MB9889.jpnprd01.prod.outlook.com

Thanks and regards
Shlok Kyal

Attachments:

v6-0003-Fix-publication-does-not-exist-error.patchapplication/octet-stream; name=v6-0003-Fix-publication-does-not-exist-error.patchDownload
From 5d2b49e55888cdc36a38208d58cf16a5960821dc Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 22 Jan 2024 12:36:20 +0530
Subject: [PATCH v6 3/3] Fix publication does not exist error.

Fix publication does not exist error.
---
 src/bin/pg_basebackup/pg_subscriber.c | 23 +++--------------------
 1 file changed, 3 insertions(+), 20 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index 0dc87e919b..8b1a92b68b 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -677,6 +677,9 @@ create_all_logical_replication_slots(PrimaryInfo *primary,
 		if (create_logical_replication_slot(conn, false, perdb) == NULL && !dry_run)
 			return false;
 
+		/* Also create a publication */
+		create_publication(conn, primary, perdb);
+
 		disconnect_database(conn);
 	}
 
@@ -1792,26 +1795,6 @@ main(int argc, char **argv)
 	 */
 	wait_for_end_recovery(standby.base_conninfo, dbarr.perdb[0].dbname);
 
-	/*
-	 * Create a publication for each database. This step should be executed
-	 * after promoting the subscriber to avoid replicating unnecessary
-	 * objects.
-	 */
-	for (i = 0; i < dbarr.ndbs; i++)
-	{
-		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
-
-		/* Connect to publisher. */
-		conn = connect_database(primary.base_conninfo, perdb->dbname);
-		if (conn == NULL)
-			exit(1);
-
-		/* Also create a publication */
-		create_publication(conn, &primary, perdb);
-
-		disconnect_database(conn);
-	}
-
 	/*
 	 * Create a subscription for each database.
 	 */
-- 
2.34.1

v6-0002-Address-some-comments-proposed-on-hackers.patchapplication/octet-stream; name=v6-0002-Address-some-comments-proposed-on-hackers.patchDownload
From ad645b61dad1a8ce03ab0ad28a0c44d0a943cc3e Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 22 Jan 2024 12:42:34 +0530
Subject: [PATCH v6 2/3] Address some comments proposed on -hackers

The patch has following changes:

* Some comments reported on the thread
* Add a timeout option for the recovery option
* Reject if the target server is not a standby
* Reject when the --subscriber-conninfo specifies non-local server
* Add -u and -p options
* Check wal_level and max_replication_slot parameters
---
 doc/src/sgml/ref/pg_subscriber.sgml           |  21 +-
 src/bin/pg_basebackup/pg_subscriber.c         | 911 +++++++++++-------
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   9 +-
 .../t/041_pg_subscriber_standby.pl            |   8 +-
 4 files changed, 600 insertions(+), 349 deletions(-)

diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
index 553185c35f..eaabfc7053 100644
--- a/doc/src/sgml/ref/pg_subscriber.sgml
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -16,12 +16,18 @@ PostgreSQL documentation
 
  <refnamediv>
   <refname>pg_subscriber</refname>
-  <refpurpose>create a new logical replica from a standby server</refpurpose>
+  <refpurpose>Convert a standby replica to a logical replica</refpurpose>
  </refnamediv>
 
  <refsynopsisdiv>
   <cmdsynopsis>
    <command>pg_subscriber</command>
+   <arg choice="plain"><option>-D</option></arg>
+   <arg choice="plain"><replaceable>datadir</replaceable></arg>
+   <arg choice="plain"><option>-P</option>
+   <replaceable>publisher-conninfo</replaceable></arg>
+   <arg choice="plain"><option>-S</option></arg>
+   <arg choice="plain"><replaceable>subscriber-conninfo</replaceable></arg>
    <arg rep="repeat"><replaceable>option</replaceable></arg>
   </cmdsynopsis>
  </refsynopsisdiv>
@@ -29,17 +35,18 @@ PostgreSQL documentation
  <refsect1>
   <title>Description</title>
   <para>
-   <application>pg_subscriber</application> takes the publisher and subscriber
-   connection strings, a cluster directory from a standby server and a list of
-   database names and it sets up a new logical replica using the physical
-   recovery process.
+   pg_subscriber creates a new <link
+   linkend="logical-replication-subscription">subscriber</link> from a physical
+   standby server. This allows users to quickly set up logical replication
+   system.
   </para>
 
   <para>
-   The <application>pg_subscriber</application> should be run at the target
+   The <application>pg_subscriber</application> has to be run at the target
    server. The source server (known as publisher server) should accept logical
    replication connections from the target server (known as subscriber server).
-   The target server should accept local logical replication connection.
+   The target server should accept logical replication connection from
+   localhost.
   </para>
  </refsect1>
 
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index e998c29f9e..0dc87e919b 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * pg_subscriber.c
- *	  Create a new logical replica from a standby server
+ *	  Convert a standby replica to a logical replica
  *
  * Copyright (C) 2024, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
- *		src/bin/pg_subscriber/pg_subscriber.c
+ *		src/bin/pg_basebackup/pg_subscriber.c
  *
  *-------------------------------------------------------------------------
  */
@@ -32,81 +32,122 @@
 
 #define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
 
-typedef struct LogicalRepInfo
+typedef struct LogicalRepPerdbInfo
 {
-	Oid			oid;			/* database OID */
-	char	   *dbname;			/* database name */
-	char	   *pubconninfo;	/* publication connection string for logical
-								 * replication */
-	char	   *subconninfo;	/* subscription connection string for logical
-								 * replication */
-	char	   *pubname;		/* publication name */
-	char	   *subname;		/* subscription name (also replication slot
-								 * name) */
-
-	bool		made_replslot;	/* replication slot was created */
-	bool		made_publication;	/* publication was created */
-	bool		made_subscription;	/* subscription was created */
-} LogicalRepInfo;
+	Oid		oid;
+	char   *dbname;
+	bool	made_replslot;	/* replication slot was created */
+	bool	made_publication;	/* publication was created */
+	bool	made_subscription;	/* subscription was created */
+} LogicalRepPerdbInfo;
+
+typedef struct
+{
+	LogicalRepPerdbInfo	   *perdb;			/* array of db infos */
+	int						ndbs;			/* number of db infos */
+} LogicalRepPerdbInfoArr;
+
+typedef struct PrimaryInfo
+{
+	char   *base_conninfo;
+	uint64	sysid;
+} PrimaryInfo;
+
+typedef struct StandbyInfo
+{
+	char   *base_conninfo;
+	char   *bindir;
+	char   *pgdata;
+	char   *primary_slot_name;
+	uint64	sysid;
+} StandbyInfo;
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname,
-							   const char *noderole);
-static bool get_exec_path(const char *path);
+static char *get_base_conninfo(char *conninfo, char *dbname);
+static bool get_exec_base_path(const char *path);
 static bool check_data_directory(const char *datadir);
+static void store_db_names(LogicalRepPerdbInfo **perdb, int ndbs);
+static void get_sysid_for_primary(PrimaryInfo *primary, char *dbname);
+static void get_control_for_standby(StandbyInfo *standby);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
-static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
-static PGconn *connect_database(const char *conninfo);
+static PGconn *connect_database(const char *base_conninfo, const char*dbname);
 static void disconnect_database(PGconn *conn);
-static uint64 get_sysid_from_conn(const char *conninfo);
-static uint64 get_control_from_datadir(const char *datadir);
-static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
-static char *use_primary_slot_name(void);
-static bool create_all_logical_replication_slots(LogicalRepInfo *dbinfo);
-static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-											 char *slot_name);
-static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *use_primary_slot_name(PrimaryInfo *primary, StandbyInfo *standby,
+								   LogicalRepPerdbInfo *perdb);
+static bool create_all_logical_replication_slots(PrimaryInfo *primary,
+												 LogicalRepPerdbInfoArr *dbarr);
+static char *create_logical_replication_slot(PGconn *conn, bool temporary,
+											 LogicalRepPerdbInfo *perdb);
+static void modify_sysid(const char *bindir, const char *datadir);
+static void drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+								  const char *slot_name);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
-static void wait_for_end_recovery(const char *conninfo);
-static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
-static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void wait_for_end_recovery(const char *base_conninfo,
+								  const char *dbname);
+static void create_publication(PGconn *conn, PrimaryInfo *primary,
+							   LogicalRepPerdbInfo *perdb);
+static void drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void create_subscription(PGconn *conn, StandbyInfo *standby,
+								char *base_conninfo,
+								LogicalRepPerdbInfo *perdb);
+static void drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void start_standby_server(StandbyInfo *standby, unsigned short subport,
+								 char *server_start_log);
+static char *construct_sub_conninfo(char *username, unsigned short subport);
 
 #define	USEC_PER_SEC	1000000
-#define	WAIT_INTERVAL	1		/* 1 second */
+#define DEFAULT_WAIT	60
+#define WAITS_PER_SEC	10              /* should divide USEC_PER_SEC evenly */
+#define DEF_PGSPORT		50111
 
 /* Options */
-static const char *progname;
-
-static char *subscriber_dir = NULL;
 static char *pub_conninfo_str = NULL;
-static char *sub_conninfo_str = NULL;
 static SimpleStringList database_names = {NULL, NULL};
-static char *primary_slot_name = NULL;
+static int	wait_seconds = DEFAULT_WAIT;
+static bool retain = false;
 static bool dry_run = false;
 
 static bool success = false;
+static const char *progname;
+static LogicalRepPerdbInfoArr dbarr;
+static PrimaryInfo primary;
+static StandbyInfo standby;
 
-static char *pg_ctl_path = NULL;
-static char *pg_resetwal_path = NULL;
+enum PGSWaitPMResult
+{
+	PGS_POSTMASTER_READY,
+	PGS_POSTMASTER_STANDBY,
+	PGS_POSTMASTER_STILL_STARTING,
+	PGS_POSTMASTER_FAILED
+};
 
-static LogicalRepInfo *dbinfo;
-static int	num_dbs = 0;
 
-static char temp_replslot[NAMEDATALEN] = {0};
-static bool made_transient_replslot = false;
+/*
+ * Build the replication slot and subscription name. The name must not exceed
+ * NAMEDATALEN - 1. This current schema uses a maximum of 36 characters
+ * (14 + 10 + 1 + 10 + '\0'). System identifier is included to reduce the
+ * probability of collision. By default, subscription name is used as
+ * replication slot name.
+ */
+static inline void
+get_subscription_name(Oid oid, int pid, char *subname, Size szsub)
+{
+	snprintf(subname, szsub, "pg_subscriber_%u_%d", oid, pid);
+}
 
-enum WaitPMResult
+/*
+ * Build the publication name. The name must not exceed NAMEDATALEN -
+ * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+ * '\0').
+ */
+static inline void
+get_publication_name(Oid oid, char *pubname, Size szpub)
 {
-	POSTMASTER_READY,
-	POSTMASTER_STANDBY,
-	POSTMASTER_STILL_STARTING,
-	POSTMASTER_FAILED
-};
+	snprintf(pubname, szpub, "pg_subscriber_%u", oid);
+}
 
 
 /*
@@ -125,41 +166,39 @@ cleanup_objects_atexit(void)
 	if (success)
 		return;
 
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
-		if (dbinfo[i].made_subscription)
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
+
+		if (perdb->made_subscription)
 		{
-			conn = connect_database(dbinfo[i].subconninfo);
+			conn = connect_database(standby.base_conninfo, perdb->dbname);
 			if (conn != NULL)
 			{
-				drop_subscription(conn, &dbinfo[i]);
+				drop_subscription(conn, perdb);
 				disconnect_database(conn);
 			}
 		}
 
-		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		if (perdb->made_publication || perdb->made_replslot)
 		{
-			conn = connect_database(dbinfo[i].pubconninfo);
+			conn = connect_database(primary.base_conninfo, perdb->dbname);
 			if (conn != NULL)
 			{
-				if (dbinfo[i].made_publication)
-					drop_publication(conn, &dbinfo[i]);
-				if (dbinfo[i].made_replslot)
-					drop_replication_slot(conn, &dbinfo[i], NULL);
+				if (perdb->made_publication)
+					drop_publication(conn, perdb);
+				if (perdb->made_replslot)
+				{
+					char replslotname[NAMEDATALEN];
+
+					get_subscription_name(perdb->oid, (int) getpid(),
+										  replslotname, NAMEDATALEN);
+					drop_replication_slot(conn, perdb, replslotname);
+				}
 				disconnect_database(conn);
 			}
 		}
 	}
-
-	if (made_transient_replslot)
-	{
-		conn = connect_database(dbinfo[0].pubconninfo);
-		if (conn != NULL)
-		{
-			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
-			disconnect_database(conn);
-		}
-	}
 }
 
 static void
@@ -184,17 +223,16 @@ usage(void)
 
 /*
  * Validate a connection string. Returns a base connection string that is a
- * connection string without a database name plus a fallback application name.
- * Since we might process multiple databases, each database name will be
- * appended to this base connection string to provide a final connection string.
- * If the second argument (dbname) is not null, returns dbname if the provided
- * connection string contains it. If option --database is not provided, uses
- * dbname as the only database to setup the logical replica.
- * It is the caller's responsibility to free the returned connection string and
- * dbname.
+ * connection string without a database name. Since we might process multiple
+ * databases, each database name will be appended to this base connection
+ * string to provide a final connection string. If the second argument (dbname)
+ * is not null, returns dbname if the provided connection string contains it.
+ * If option --database is not provided, uses dbname as the only database to
+ * setup the logical replica. It is the caller's responsibility to free the
+ * returned connection string and dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+get_base_conninfo(char *conninfo, char *dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -203,7 +241,7 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 	char	   *ret;
 	int			i;
 
-	pg_log_info("validating connection string on %s", noderole);
+	pg_log_info("validating connection string on publisher");
 
 	conn_opts = PQconninfoParse(conninfo, &errmsg);
 	if (conn_opts == NULL)
@@ -231,10 +269,6 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 		}
 	}
 
-	if (i > 0)
-		appendPQExpBufferChar(buf, ' ');
-	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
-
 	ret = pg_strdup(buf->data);
 
 	destroyPQExpBuffer(buf);
@@ -244,15 +278,16 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 }
 
 /*
- * Get the absolute path from other PostgreSQL binaries (pg_ctl and
- * pg_resetwal) that is used by it.
+ * Get the absolute binary path from another PostgreSQL binary (pg_ctl) and set
+ * to StandbyInfo.
  */
 static bool
-get_exec_path(const char *path)
+get_exec_base_path(const char *path)
 {
 	int			rc;
+	char		pg_ctl_path[MAXPGPATH];
+	char	   *p;
 
-	pg_ctl_path = pg_malloc(MAXPGPATH);
 	rc = find_other_exec(path, "pg_ctl",
 						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
 						 pg_ctl_path);
@@ -277,30 +312,10 @@ get_exec_path(const char *path)
 
 	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
 
-	pg_resetwal_path = pg_malloc(MAXPGPATH);
-	rc = find_other_exec(path, "pg_resetwal",
-						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
-						 pg_resetwal_path);
-	if (rc < 0)
-	{
-		char		full_path[MAXPGPATH];
-
-		if (find_my_exec(path, full_path) < 0)
-			strlcpy(full_path, progname, sizeof(full_path));
-		if (rc == -1)
-			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
-						 "same directory as \"%s\".\n"
-						 "Check your installation.",
-						 "pg_resetwal", progname, full_path);
-		else
-			pg_log_error("The program \"%s\" was found by \"%s\"\n"
-						 "but was not the same version as %s.\n"
-						 "Check your installation.",
-						 "pg_resetwal", full_path, progname);
-		return false;
-	}
-
-	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+	/* Extract the directory part from the path */
+	Assert(p = strrchr(pg_ctl_path, 'p'));
+	*p = '\0';
+	standby.bindir = pg_strdup(pg_ctl_path);
 
 	return true;
 }
@@ -364,49 +379,36 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
 }
 
 /*
- * Store publication and subscription information.
+ * Initialize per-db structure and store the name of databases
  */
-static LogicalRepInfo *
-store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+static void
+store_db_names(LogicalRepPerdbInfo **perdb, int ndbs)
 {
-	LogicalRepInfo *dbinfo;
 	SimpleStringListCell *cell;
 	int			i = 0;
 
-	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+	*perdb = (LogicalRepPerdbInfo *) pg_malloc0(sizeof(LogicalRepPerdbInfo) *
+											   ndbs);
 
 	for (cell = database_names.head; cell; cell = cell->next)
 	{
-		char	   *conninfo;
-
-		/* Publisher. */
-		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
-		dbinfo[i].pubconninfo = conninfo;
-		dbinfo[i].dbname = cell->val;
-		dbinfo[i].made_replslot = false;
-		dbinfo[i].made_publication = false;
-		dbinfo[i].made_subscription = false;
-		/* other struct fields will be filled later. */
-
-		/* Subscriber. */
-		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
-		dbinfo[i].subconninfo = conninfo;
-
+		(*perdb)[i].dbname = pg_strdup(cell->val);
 		i++;
 	}
-
-	return dbinfo;
 }
 
 static PGconn *
-connect_database(const char *conninfo)
+connect_database(const char *base_conninfo, const char*dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	const char *rconninfo;
+
+	char	   *rconninfo;
+	char	   *concat_conninfo = concat_conninfo_dbname(base_conninfo,
+														 dbname);
 
 	/* logical replication mode */
-	rconninfo = psprintf("%s replication=database", conninfo);
+	rconninfo = psprintf("%s replication=database", concat_conninfo);
 
 	conn = PQconnectdb(rconninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
@@ -424,6 +426,9 @@ connect_database(const char *conninfo)
 	}
 	PQclear(res);
 
+	pfree(rconninfo);
+	pfree(concat_conninfo);
+
 	return conn;
 }
 
@@ -436,19 +441,18 @@ disconnect_database(PGconn *conn)
 }
 
 /*
- * Obtain the system identifier using the provided connection. It will be used
- * to compare if a data directory is a clone of another one.
+ * Obtain the system identifier from the primary server. It will be used to
+ * compare if a data directory is a clone of another one.
  */
-static uint64
-get_sysid_from_conn(const char *conninfo)
+static void
+get_sysid_for_primary(PrimaryInfo *primary, char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from publisher");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(primary->base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -471,43 +475,39 @@ get_sysid_from_conn(const char *conninfo)
 		exit(1);
 	}
 
-	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+	primary->sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
 
-	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) primary->sysid);
 
 	disconnect_database(conn);
-
-	return sysid;
 }
 
 /*
- * Obtain the system identifier from control file. It will be used to compare
- * if a data directory is a clone of another one. This routine is used locally
- * and avoids a replication connection.
+ * Obtain the system identifier from a standby server. It will be used to
+ * compare if a data directory is a clone of another one. This routine is used
+ * locally and avoids a replication connection.
  */
-static uint64
-get_control_from_datadir(const char *datadir)
+static void
+get_control_for_standby(StandbyInfo *standby)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from subscriber");
 
-	cf = get_controlfile(datadir, &crc_ok);
+	cf = get_controlfile(standby->pgdata, &crc_ok);
 	if (!crc_ok)
 	{
 		pg_log_error("control file appears to be corrupt");
 		exit(1);
 	}
 
-	sysid = cf->system_identifier;
+	standby->sysid = cf->system_identifier;
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) standby->sysid);
 
 	pfree(cf);
-
-	return sysid;
 }
 
 /*
@@ -516,7 +516,7 @@ get_control_from_datadir(const char *datadir)
  * files from one of the systems might be used in the other one.
  */
 static void
-modify_sysid(const char *pg_resetwal_path, const char *datadir)
+modify_sysid(const char *bindir, const char *datadir)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
@@ -551,7 +551,7 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
 
 	pg_log_info("running pg_resetwal on the subscriber");
 
-	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\"", bindir, datadir);
 
 	pg_log_debug("command is: %s", cmd_str);
 
@@ -571,14 +571,15 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
  * Return a palloc'd slot name if the replication is using one.
  */
 static char *
-use_primary_slot_name(void)
+use_primary_slot_name(PrimaryInfo *primary, StandbyInfo *standby,
+					  LogicalRepPerdbInfo *perdb)
 {
 	PGconn	   *conn;
 	PGresult   *res;
 	PQExpBuffer str = createPQExpBuffer();
 	char	   *slot_name;
 
-	conn = connect_database(dbinfo[0].subconninfo);
+	conn = connect_database(standby->base_conninfo, perdb->dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -604,7 +605,7 @@ use_primary_slot_name(void)
 
 	disconnect_database(conn);
 
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary->base_conninfo, perdb->dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -634,17 +635,19 @@ use_primary_slot_name(void)
 }
 
 static bool
-create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+create_all_logical_replication_slots(PrimaryInfo *primary,
+									 LogicalRepPerdbInfoArr *dbarr)
 {
 	int			i;
 
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr->ndbs; i++)
 	{
 		PGconn	   *conn;
 		PGresult   *res;
 		char		replslotname[NAMEDATALEN];
+		LogicalRepPerdbInfo *perdb = &dbarr->perdb[i];
 
-		conn = connect_database(dbinfo[i].pubconninfo);
+		conn = connect_database(primary->base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
@@ -664,27 +667,14 @@ create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
 		}
 
 		/* Remember database OID. */
-		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		perdb->oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
 
 		PQclear(res);
 
-		/*
-		 * Build the replication slot name. The name must not exceed
-		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
-		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
-		 * to reduce the probability of collision. By default, subscription
-		 * name is used as replication slot name.
-		 */
-		snprintf(replslotname, sizeof(replslotname),
-				 "pg_subscriber_%u_%d",
-				 dbinfo[i].oid,
-				 (int) getpid());
-		dbinfo[i].subname = pg_strdup(replslotname);
+		get_subscription_name(perdb->oid, (int) getpid(), replslotname, NAMEDATALEN);
 
 		/* Create replication slot on publisher. */
-		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
-			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
-		else
+		if (create_logical_replication_slot(conn, false, perdb) == NULL && !dry_run)
 			return false;
 
 		disconnect_database(conn);
@@ -701,30 +691,36 @@ create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
  * result set that contains the consistent LSN.
  */
 static char *
-create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-								char *slot_name)
+create_logical_replication_slot(PGconn *conn, bool temporary,
+								LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res = NULL;
 	char	   *lsn = NULL;
-	bool		transient_replslot = false;
+	char		slot_name[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
 	/*
-	 * If no slot name is informed, it is a transient replication slot used
-	 * only for catch up purposes.
+	 * Construct a name of logical replication slot. The formatting is
+	 * different depends on its persistency.
+	 *
+	 * For persistent slots: the name must be same as the subscription.
+	 * For temporary slots: OID is not needed, but another string is added.
 	 */
-	if (slot_name[0] == '\0')
-	{
+	if (!temporary)
+		get_subscription_name(perdb->oid, (int) getpid(), slot_name, NAMEDATALEN);
+	else
 		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
 				 (int) getpid());
-		transient_replslot = true;
-	}
 
-	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+
+	if(temporary)
+		appendPQExpBufferStr(str, " TEMPORARY");
+
 	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
 
 	pg_log_debug("command is: %s", str->data);
@@ -734,17 +730,14 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						 PQresultErrorMessage(res));
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, perdb->dbname, PQresultErrorMessage(res));
 			return lsn;
 		}
 	}
 
 	/* for cleanup purposes */
-	if (transient_replslot)
-		made_transient_replslot = true;
-	else
-		dbinfo->made_replslot = true;
+	perdb->made_replslot = true;
 
 	if (!dry_run)
 	{
@@ -758,14 +751,15 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 }
 
 static void
-drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+					  const char *slot_name)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
 
@@ -775,7 +769,7 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, perdb->dbname,
 						 PQerrorMessage(conn));
 
 		PQclear(res);
@@ -825,19 +819,22 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
  * Returns after the server finishes the recovery process.
  */
 static void
-wait_for_end_recovery(const char *conninfo)
+wait_for_end_recovery(const char *base_conninfo, const char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	int			status = POSTMASTER_STILL_STARTING;
+	int			status = PGS_POSTMASTER_STILL_STARTING;
+	int			cnt;
+	int			rc;
+	char	   *pg_ctl_cmd;
 
 	pg_log_info("waiting the postmaster to reach the consistent state");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
-	for (;;)
+	for (cnt = 0; cnt < wait_seconds * WAITS_PER_SEC; cnt++)
 	{
 		bool		in_recovery;
 
@@ -865,17 +862,32 @@ wait_for_end_recovery(const char *conninfo)
 		 */
 		if (!in_recovery || dry_run)
 		{
-			status = POSTMASTER_READY;
+			status = PGS_POSTMASTER_READY;
 			break;
 		}
 
 		/* Keep waiting. */
-		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+		pg_usleep(USEC_PER_SEC / WAITS_PER_SEC);
 	}
 
 	disconnect_database(conn);
 
-	if (status == POSTMASTER_STILL_STARTING)
+	/*
+	 * If timeout is reached exit the pg_subscriber and stop the standby node.
+	 */
+	if (cnt >= wait_seconds * WAITS_PER_SEC)
+	{
+		pg_log_error("recovery timed out");
+
+		pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
+							  standby.bindir, standby.pgdata);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+		exit(1);
+	}
+
+	if (status == PGS_POSTMASTER_STILL_STARTING)
 	{
 		pg_log_error("server did not end recovery");
 		exit(1);
@@ -888,17 +900,21 @@ wait_for_end_recovery(const char *conninfo)
  * Create a publication that includes all tables in the database.
  */
 static void
-create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+create_publication(PGconn *conn, PrimaryInfo *primary,
+				   LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
 	/* Check if the publication needs to be created. */
 	appendPQExpBuffer(str,
 					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
-					  dbinfo->pubname);
+					  pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
@@ -918,7 +934,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		 */
 		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
 		{
-			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			pg_log_info("publication \"%s\" already exists", pubname);
 			return;
 		}
 		else
@@ -931,7 +947,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 			 * database oid in which puballtables is false.
 			 */
 			pg_log_error("publication \"%s\" does not replicate changes for all tables",
-						 dbinfo->pubname);
+						 pubname);
 			pg_log_error_hint("Consider renaming this publication.");
 			PQclear(res);
 			PQfinish(conn);
@@ -942,9 +958,9 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	PQclear(res);
 	resetPQExpBuffer(str);
 
-	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("creating publication \"%s\" on database \"%s\"", pubname, perdb->dbname);
 
-	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -954,14 +970,14 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
-						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+						 pubname, perdb->dbname, PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
 		}
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_publication = true;
+	perdb->made_publication = true;
 
 	if (!dry_run)
 		PQclear(res);
@@ -973,16 +989,19 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove publication if it couldn't finish all steps.
  */
 static void
-drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", pubname, perdb->dbname);
 
-	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -990,7 +1009,7 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", pubname, perdb->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1011,19 +1030,27 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * initial location.
  */
 static void
-create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+create_subscription(PGconn *conn, StandbyInfo *standby, char *base_conninfo,
+					LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
 					  "WITH (create_slot = false, copy_data = false, enabled = false)",
-					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+					  subname, concat_conninfo_dbname(base_conninfo, perdb->dbname), pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1033,14 +1060,14 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+						 subname, perdb->dbname, PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
 		}
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_subscription = true;
+	perdb->made_subscription = true;
 
 	if (!dry_run)
 		PQclear(res);
@@ -1052,16 +1079,19 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove subscription if it couldn't finish all steps.
  */
 static void
-drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
 
-	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", subname, perdb->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1069,7 +1099,7 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", subname, perdb->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1088,18 +1118,21 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * printing purposes.
  */
 static void
-set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb, const char *lsn)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 	Oid			suboid;
 	char		originname[NAMEDATALEN];
 	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
 	appendPQExpBuffer(str,
-					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", subname);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1140,7 +1173,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	PQclear(res);
 
 	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
-				originname, lsnstr, dbinfo->dbname);
+				originname, lsnstr, perdb->dbname);
 
 	resetPQExpBuffer(str);
 	appendPQExpBuffer(str,
@@ -1154,7 +1187,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
 			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
-						 dbinfo->subname, PQresultErrorMessage(res));
+						 subname, PQresultErrorMessage(res));
 			PQfinish(conn);
 			exit(1);
 		}
@@ -1173,16 +1206,20 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
  * of this setup.
  */
 static void
-enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
 
-	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1191,7 +1228,7 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
-			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+			pg_log_error("could not enable subscription \"%s\": %s", subname,
 						 PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
@@ -1203,6 +1240,61 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	destroyPQExpBuffer(str);
 }
 
+static void
+start_standby_server(StandbyInfo *standby, unsigned short subport,
+					 char *server_start_log)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	int			rc;
+	char	   *pg_ctl_cmd;
+
+	if (server_start_log[0] == '\0')
+	{
+		/* append timestamp with ISO 8601 format. */
+		gettimeofday(&time, NULL);
+		tt = (time_t) time.tv_sec;
+		strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+		snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+				 ".%03d", (int) (time.tv_usec / 1000));
+
+		len = snprintf(server_start_log, MAXPGPATH,
+					   "%s/%s/server_start_%s.log", standby->pgdata,
+					   PGS_OUTPUT_DIR, timebuf);
+		if (len >= MAXPGPATH)
+		{
+			pg_log_error("log file path is too long");
+			exit(1);
+		}
+	}
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -o \"-p %d\" -l \"%s\"",
+						  standby->bindir,
+						  standby->pgdata, subport, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static char *
+construct_sub_conninfo(char *username, unsigned short subport)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	if (username)
+		appendPQExpBuffer(buf, "user=%s ", username);
+
+	appendPQExpBuffer(buf, "port=%d fallback_application_name=%s",
+					  subport, progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
 int
 main(int argc, char **argv)
 {
@@ -1214,6 +1306,10 @@ main(int argc, char **argv)
 		{"publisher-conninfo", required_argument, NULL, 'P'},
 		{"subscriber-conninfo", required_argument, NULL, 'S'},
 		{"database", required_argument, NULL, 'd'},
+		{"timeout", required_argument, NULL, 't'},
+		{"username", required_argument, NULL, 'u'},
+		{"port", required_argument, NULL, 'p'},
+		{"retain", no_argument, NULL, 'r'},
 		{"dry-run", no_argument, NULL, 'n'},
 		{"verbose", no_argument, NULL, 'v'},
 		{NULL, 0, NULL, 0}
@@ -1225,20 +1321,15 @@ main(int argc, char **argv)
 
 	char	   *pg_ctl_cmd;
 
-	char	   *base_dir;
-	char	   *server_start_log;
-
-	char		timebuf[128];
-	struct timeval time;
-	time_t		tt;
+	char		base_dir[MAXPGPATH];
+	char		server_start_log[MAXPGPATH] = {0};
 	int			len;
 
-	char	   *pub_base_conninfo = NULL;
-	char	   *sub_base_conninfo = NULL;
 	char	   *dbname_conninfo = NULL;
 
-	uint64		pub_sysid;
-	uint64		sub_sysid;
+	unsigned short subport = DEF_PGSPORT;
+	char	   *username = NULL;
+
 	struct stat statbuf;
 
 	PGconn	   *conn;
@@ -1250,6 +1341,13 @@ main(int argc, char **argv)
 
 	int			i;
 
+	PGresult   *res;
+
+	char	   *wal_level;
+	int			max_replication_slots;
+	int			nslots_old;
+	int			nslots_new;
+
 	pg_logging_init(argv[0]);
 	pg_logging_set_level(PG_LOG_WARNING);
 	progname = get_progname(argv[0]);
@@ -1286,28 +1384,40 @@ main(int argc, char **argv)
 	}
 #endif
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+	while ((c = getopt_long(argc, argv, "D:P:S:d:t:u:p:rnv",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
 		{
 			case 'D':
-				subscriber_dir = pg_strdup(optarg);
+				standby.pgdata = pg_strdup(optarg);
+				canonicalize_path(standby.pgdata);
 				break;
 			case 'P':
 				pub_conninfo_str = pg_strdup(optarg);
 				break;
-			case 'S':
-				sub_conninfo_str = pg_strdup(optarg);
-				break;
 			case 'd':
 				/* Ignore duplicated database names. */
 				if (!simple_string_list_member(&database_names, optarg))
 				{
 					simple_string_list_append(&database_names, optarg);
-					num_dbs++;
+					dbarr.ndbs++;
 				}
 				break;
+			case 't':
+				wait_seconds = atoi(optarg);
+				break;
+			case 'u':
+				pfree(username);
+				username = pg_strdup(optarg);
+				break;
+			case 'p':
+				if ((subport = atoi(optarg)) <= 0)
+					pg_fatal("invalid old port number");
+				break;
+			case 'r':
+				retain = true;
+				break;
 			case 'n':
 				dry_run = true;
 				break;
@@ -1335,7 +1445,7 @@ main(int argc, char **argv)
 	/*
 	 * Required arguments
 	 */
-	if (subscriber_dir == NULL)
+	if (standby.pgdata == NULL)
 	{
 		pg_log_error("no subscriber data directory specified");
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -1358,21 +1468,14 @@ main(int argc, char **argv)
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 		exit(1);
 	}
-	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
-										  "publisher");
-	if (pub_base_conninfo == NULL)
-		exit(1);
 
-	if (sub_conninfo_str == NULL)
-	{
-		pg_log_error("no subscriber connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
-	if (sub_base_conninfo == NULL)
+	primary.base_conninfo = get_base_conninfo(pub_conninfo_str,
+											  dbname_conninfo);
+	if (primary.base_conninfo == NULL)
 		exit(1);
 
+	standby.base_conninfo = construct_sub_conninfo(username, subport);
+
 	if (database_names.head == NULL)
 	{
 		pg_log_info("no database was specified");
@@ -1385,7 +1488,7 @@ main(int argc, char **argv)
 		if (dbname_conninfo)
 		{
 			simple_string_list_append(&database_names, dbname_conninfo);
-			num_dbs++;
+			dbarr.ndbs++;
 
 			pg_log_info("database \"%s\" was extracted from the publisher connection string",
 						dbname_conninfo);
@@ -1399,25 +1502,25 @@ main(int argc, char **argv)
 	}
 
 	/*
-	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 * Get the absolute path of binaries on the subscriber.
 	 */
-	if (!get_exec_path(argv[0]))
+	if (!get_exec_base_path(argv[0]))
 		exit(1);
 
 	/* rudimentary check for a data directory. */
-	if (!check_data_directory(subscriber_dir))
+	if (!check_data_directory(standby.pgdata))
 		exit(1);
 
-	/* Store database information for publisher and subscriber. */
-	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+	/* Store database information to dbarr */
+	store_db_names(&dbarr.perdb, dbarr.ndbs);
 
 	/*
 	 * Check if the subscriber data directory has the same system identifier
 	 * than the publisher data directory.
 	 */
-	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
-	sub_sysid = get_control_from_datadir(subscriber_dir);
-	if (pub_sysid != sub_sysid)
+	get_sysid_for_primary(&primary, dbarr.perdb[0].dbname);
+	get_control_for_standby(&standby);
+	if (primary.sysid != standby.sysid)
 	{
 		pg_log_error("subscriber data directory is not a copy of the source database cluster");
 		exit(1);
@@ -1426,8 +1529,8 @@ main(int argc, char **argv)
 	/*
 	 * Create the output directory to store any data generated by this tool.
 	 */
-	base_dir = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s",
+				   standby.pgdata, PGS_OUTPUT_DIR);
 	if (len >= MAXPGPATH)
 	{
 		pg_log_error("directory path for subscriber is too long");
@@ -1441,7 +1544,153 @@ main(int argc, char **argv)
 	}
 
 	/* subscriber PID file. */
-	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid",
+			  standby.pgdata);
+
+	/* Start the standby server anyway */
+	start_standby_server(&standby, subport, server_start_log);
+
+	/*
+	 * Check wal_level in publisher and the max_replication_slots of publisher
+	 */
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT count(*) from pg_replication_slots;");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain number of replication slots");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not determine parameter settings on publisher");
+		exit(1);
+	}
+
+	nslots_old = atoi(PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	res = PQexec(conn, "SELECT setting FROM pg_settings "
+				 "WHERE name IN ('wal_level', 'max_replication_slots') "
+				 "ORDER BY name DESC;");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain guc parameters on publisher");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 2)
+	{
+		pg_log_error("could not determine parameter settings on publisher");
+		exit(1);
+	}
+
+	wal_level = PQgetvalue(res, 0, 0);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("wal_level must be \"logical\", but is set to \"%s\"", wal_level);
+		exit(1);
+	}
+
+	max_replication_slots = atoi(PQgetvalue(res, 1, 0));
+	nslots_new = nslots_old + dbarr.ndbs + 1;
+
+	if (nslots_new > max_replication_slots)
+	{
+		pg_log_error("max_replication_slots (%d) must be greater than or equal to "
+					 "the number of replication slots required (%d)", max_replication_slots, nslots_new);
+		exit(1);
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	conn = connect_database(standby.base_conninfo, dbarr.perdb[0].dbname);
+	if (conn == NULL)
+		exit(1);
+
+	/*
+	 * Check the max_replication_slots in subscriber
+	 */
+	res = PQexec(conn, "SELECT count(*) from pg_replication_slots;");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain number of replication slots on subscriber");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not determine parameter settings on subscriber");
+		exit(1);
+	}
+
+	nslots_old = atoi(PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	res = PQexec(conn, "SELECT setting FROM pg_settings "
+				 "WHERE name = 'max_replication_slots';");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain guc parameters");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not determine parameter settings on publisher");
+		exit(1);
+	}
+
+	max_replication_slots = atoi(PQgetvalue(res, 0, 0));
+	nslots_new = nslots_old + dbarr.ndbs;
+
+	if (nslots_new > max_replication_slots)
+	{
+		pg_log_error("max_replication_slots (%d) must be greater than or equal to "
+					 "the number of replication slots required (%d)", max_replication_slots, nslots_new);
+		exit(1);
+	}
+
+	PQclear(res);
+
+	/*
+	 * Exit the pg_subscriber if the node is not a standby server.
+	 */
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("unexpected result from pg_is_in_recovery function");
+		exit(1);
+	}
+
+	/* Check if the server is in recovery */
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("pg_subscriber is supported only on standby server");
+		exit(1);
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", standby.pgdata);
 
 	/*
 	 * Stop the subscriber if it is a standby server. Before executing the
@@ -1457,14 +1706,18 @@ main(int argc, char **argv)
 		 * replication slot has no use after the transformation, hence, it
 		 * will be removed at the end of this process.
 		 */
-		primary_slot_name = use_primary_slot_name();
-		if (primary_slot_name != NULL)
-			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		standby.primary_slot_name = use_primary_slot_name(&primary,
+														   &standby,
+														   &dbarr.perdb[0]);
+		if (standby.primary_slot_name != NULL)
+			pg_log_info("primary has replication slot \"%s\"",
+						standby.primary_slot_name);
 
 		pg_log_info("subscriber is up and running");
 		pg_log_info("stopping the server to start the transformation steps");
 
-		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
+							  standby.bindir, standby.pgdata);
 		rc = system(pg_ctl_cmd);
 		pg_ctl_status(pg_ctl_cmd, rc, 0);
 	}
@@ -1472,7 +1725,7 @@ main(int argc, char **argv)
 	/*
 	 * Create a replication slot for each database on the publisher.
 	 */
-	if (!create_all_logical_replication_slots(dbinfo))
+	if (!create_all_logical_replication_slots(&primary, &dbarr))
 		exit(1);
 
 	/*
@@ -1492,11 +1745,11 @@ main(int argc, char **argv)
 	 * replication connection open (depending when base backup was taken, the
 	 * connection should be open for a few hours).
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
-	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
-													 temp_replslot);
+	consistent_lsn = create_logical_replication_slot(conn, true,
+													 &dbarr.perdb[0]);
 
 	/*
 	 * Write recovery parameters.
@@ -1522,7 +1775,7 @@ main(int argc, char **argv)
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  consistent_lsn);
-		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+		WriteRecoveryConfig(conn, standby.pgdata, recoveryconfcontents);
 	}
 	disconnect_database(conn);
 
@@ -1532,54 +1785,29 @@ main(int argc, char **argv)
 	 * Start subscriber and wait until accepting connections.
 	 */
 	pg_log_info("starting the subscriber");
-
-	/* append timestamp with ISO 8601 format. */
-	gettimeofday(&time, NULL);
-	tt = (time_t) time.tv_sec;
-	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
-	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
-			 ".%03d", (int) (time.tv_usec / 1000));
-
-	server_start_log = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
-	if (len >= MAXPGPATH)
-	{
-		pg_log_error("log file path is too long");
-		exit(1);
-	}
-
-	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
-	rc = system(pg_ctl_cmd);
-	pg_ctl_status(pg_ctl_cmd, rc, 1);
+	start_standby_server(&standby, subport, server_start_log);
 
 	/*
 	 * Waiting the subscriber to be promoted.
 	 */
-	wait_for_end_recovery(dbinfo[0].subconninfo);
+	wait_for_end_recovery(standby.base_conninfo, dbarr.perdb[0].dbname);
 
 	/*
 	 * Create a publication for each database. This step should be executed
 	 * after promoting the subscriber to avoid replicating unnecessary
 	 * objects.
 	 */
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
-		char		pubname[NAMEDATALEN];
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
 
 		/* Connect to publisher. */
-		conn = connect_database(dbinfo[i].pubconninfo);
+		conn = connect_database(primary.base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
-		/*
-		 * Build the publication name. The name must not exceed NAMEDATALEN -
-		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
-		 * '\0').
-		 */
-		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
-		dbinfo[i].pubname = pg_strdup(pubname);
-
-		create_publication(conn, &dbinfo[i]);
+		/* Also create a publication */
+		create_publication(conn, &primary, perdb);
 
 		disconnect_database(conn);
 	}
@@ -1587,20 +1815,25 @@ main(int argc, char **argv)
 	/*
 	 * Create a subscription for each database.
 	 */
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
+
 		/* Connect to subscriber. */
-		conn = connect_database(dbinfo[i].subconninfo);
+		conn = connect_database(standby.base_conninfo, perdb->dbname);
+
 		if (conn == NULL)
 			exit(1);
 
-		create_subscription(conn, &dbinfo[i]);
+		create_subscription(conn, &standby, primary.base_conninfo, perdb);
 
 		/* Set the replication progress to the correct LSN. */
-		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+		set_replication_progress(conn, perdb, consistent_lsn);
 
 		/* Enable subscription. */
-		enable_subscription(conn, &dbinfo[i]);
+		enable_subscription(conn, perdb);
+
+		drop_publication(conn, perdb);
 
 		disconnect_database(conn);
 	}
@@ -1613,19 +1846,21 @@ main(int argc, char **argv)
 	 * XXX we might not fail here. Instead, we provide a warning so the user
 	 * eventually drops the replication slot later.
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
 	if (conn == NULL)
 	{
-		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
-		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		char *primary_slot_name = standby.primary_slot_name;
+
 		if (primary_slot_name != NULL)
 			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
 	}
 	else
 	{
-		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[0];
+		char *primary_slot_name = standby.primary_slot_name;
+
 		if (primary_slot_name != NULL)
-			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+			drop_replication_slot(conn, perdb, primary_slot_name);
 		disconnect_database(conn);
 	}
 
@@ -1634,20 +1869,22 @@ main(int argc, char **argv)
 	 */
 	pg_log_info("stopping the subscriber");
 
-	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
+						  standby.bindir, standby.pgdata);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 0);
 
 	/*
 	 * Change system identifier.
 	 */
-	modify_sysid(pg_resetwal_path, subscriber_dir);
+	modify_sysid(standby.bindir, standby.pgdata);
 
 	/*
 	 * Remove log file generated by this tool, if it runs successfully.
 	 * Otherwise, file is kept that may provide useful debugging information.
 	 */
-	unlink(server_start_log);
+	if (!retain)
+		unlink(server_start_log);
 
 	success = true;
 
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
index 4ebff76b2d..9915b8cb3c 100644
--- a/src/bin/pg_basebackup/t/040_pg_subscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -37,8 +37,13 @@ command_fails(
 		'--verbose',
 		'--pgdata', $datadir,
 		'--publisher-conninfo', 'dbname=postgres',
-		'--subscriber-conninfo', 'dbname=postgres'
 	],
 	'no database name specified');
-
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+	],
+	'subscriber connection string specnfied non-local server');
 done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
index fbcd0fc82b..4e26607611 100644
--- a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -51,25 +51,27 @@ $node_s->start;
 $node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
 $node_p->wait_for_replay_catchup($node_s);
 
+$node_f->stop;
+
 # Run pg_subscriber on about-to-fail node F
 command_fails(
 	[
 		'pg_subscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
 		'--publisher-conninfo', $node_p->connstr('pg1'),
-		'--subscriber-conninfo', $node_f->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
 	'subscriber data directory is not a copy of the source database cluster');
 
+$node_s->stop;
+
 # dry run mode on node S
 command_ok(
 	[
 		'pg_subscriber', '--verbose', '--dry-run',
 		'--pgdata', $node_s->data_dir,
 		'--publisher-conninfo', $node_p->connstr('pg1'),
-		'--subscriber-conninfo', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
@@ -82,6 +84,7 @@ $node_s->start;
 # Check if node S is still a standby
 is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
 	't', 'standby is in recovery');
+$node_s->stop;
 
 # Run pg_subscriber on node S
 command_ok(
@@ -89,7 +92,6 @@ command_ok(
 		'pg_subscriber', '--verbose',
 		'--pgdata', $node_s->data_dir,
 		'--publisher-conninfo', $node_p->connstr('pg1'),
-		'--subscriber-conninfo', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
-- 
2.34.1

v6-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchapplication/octet-stream; name=v6-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchDownload
From 7b808c5a927e3abf98b1e3bb62ec64dd5b80b013 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v6 1/3] Creates a new logical replica from a standby server

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server. Remove
the additional replication slot that was used to get the consistent LSN.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_subscriber.sgml           |  284 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_subscriber.c         | 1657 +++++++++++++++++
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   44 +
 .../t/041_pg_subscriber_standby.pl            |  139 ++
 9 files changed, 2153 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_subscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_subscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_subscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..3862c976d7 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgSubscriber       SYSTEM "pg_subscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
new file mode 100644
index 0000000000..553185c35f
--- /dev/null
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -0,0 +1,284 @@
+<!--
+doc/src/sgml/ref/pg_subscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgsubscriber">
+ <indexterm zone="app-pgsubscriber">
+  <primary>pg_subscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_subscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>create a new logical replica from a standby server</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_subscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a standby server and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_subscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a standby
+        server.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--publisher-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_subscriber</application> to output progress messages
+        and detailed information about each step.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_subscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_subscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_subscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the target data
+     directory is used by a standby server. Stop the standby server if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_subscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  Another
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_subscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_subscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_subscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_subscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> removes the additional replication
+     slot that was used to get the consistent LSN on the source server.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a standby server at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..266f4e515a 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..0e5384a1d5 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_subscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..f6281b7676 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_subscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_subscriber: $(WIN32RES) pg_subscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_subscriber$(X) '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_subscriber$(X) pg_subscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..ccfd7bb7a5 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_subscriber_sources = files(
+  'pg_subscriber.c'
+)
+
+if host_system == 'windows'
+  pg_subscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_subscriber',
+	'--FILEDESC', 'pg_subscriber - create a new logical replica from a standby server',])
+endif
+
+pg_subscriber = executable('pg_subscriber',
+  pg_subscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_subscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_subscriber.pl',
+      't/041_pg_subscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
new file mode 100644
index 0000000000..e998c29f9e
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -0,0 +1,1657 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_subscriber/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static char *use_primary_slot_name(void);
+static bool create_all_logical_replication_slots(LogicalRepInfo *dbinfo);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static char temp_replslot[NAMEDATALEN] = {0};
+static bool made_transient_replslot = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_subscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+
+	if (made_transient_replslot)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+			disconnect_database(conn);
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-conninfo=CONNINFO   publisher connection string\n"));
+	printf(_(" -S, --subscriber-conninfo=CONNINFO  subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name plus a fallback application name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	if (i > 0)
+		appendPQExpBufferChar(buf, ' ');
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Return a palloc'd slot name if the replication is using one.
+ */
+static char *
+use_primary_slot_name(void)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+	char	   *slot_name;
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT setting FROM pg_settings WHERE name = 'primary_slot_name'");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain parameter information: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+
+	/*
+	 * If primary_slot_name is an empty string, the current replication
+	 * connection is not using a replication slot, bail out.
+	 */
+	if (strcmp(PQgetvalue(res, 0, 0), "") == 0)
+	{
+		PQclear(res);
+		return NULL;
+	}
+
+	slot_name = pg_strdup(PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_replication_slots r INNER JOIN pg_stat_activity a ON (r.active_pid = a.pid) WHERE slot_name = '%s'", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		return NULL;
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return slot_name;
+}
+
+static bool
+create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+{
+	int			i;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
+		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
+		 * to reduce the probability of collision. By default, subscription
+		 * name is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_subscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (transient_replslot)
+		made_transient_replslot = true;
+	else
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_subscriber must have created this
+		 * publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_subscriber_ prefix followed by the exact
+			 * database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-conninfo", required_argument, NULL, 'P'},
+		{"subscriber-conninfo", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+	int			rc;
+
+	char	   *pg_ctl_cmd;
+
+	char	   *base_dir;
+	char	   *server_start_log;
+
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	int			i;
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_subscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&database_names, optarg))
+				{
+					simple_string_list_append(&database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("directory path for subscriber is too long");
+		exit(1);
+	}
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+	{
+		pg_log_error("could not create directory \"%s\": %m", base_dir);
+		exit(1);
+	}
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * Stop the subscriber if it is a standby server. Before executing the
+	 * transformation steps, make sure the subscriber is not running because
+	 * one of the steps is to modify some recovery parameters that require a
+	 * restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Since the standby server is running, check if it is using an
+		 * existing replication slot for WAL retention purposes. This
+		 * replication slot has no use after the transformation, hence, it
+		 * will be removed at the end of this process.
+		 */
+		primary_slot_name = use_primary_slot_name();
+		if (primary_slot_name != NULL)
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+
+		pg_log_info("subscriber is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+	}
+
+	/*
+	 * Create a replication slot for each database on the publisher.
+	 */
+	if (!create_all_logical_replication_slots(dbinfo))
+		exit(1);
+
+	/*
+	 * Create a logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. We cannot use the last created replication slot
+	 * because the consistent LSN should be obtained *after* the base backup
+	 * finishes (and the base backup should include the logical replication
+	 * slots).
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 *
+	 * A temporary replication slot is not used here to avoid keeping a
+	 * replication connection open (depending when base backup was taken, the
+	 * connection should be open for a few hours).
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	server_start_log = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("log file path is too long");
+		exit(1);
+	}
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create a publication for each database. This step should be executed
+	 * after promoting the subscriber to avoid replicating unnecessary
+	 * objects.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+
+		/* Connect to publisher. */
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		create_publication(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Create a subscription for each database.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * The transient replication slot is no longer required. Drop it.
+	 *
+	 * If the physical replication slot exists, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops the replication slot later.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+	{
+		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		if (primary_slot_name != NULL)
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+	}
+	else
+	{
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		if (primary_slot_name != NULL)
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	/*
+	 * Remove log file generated by this tool, if it runs successfully.
+	 * Otherwise, file is kept that may provide useful debugging information.
+	 */
+	unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
new file mode 100644
index 0000000000..4ebff76b2d
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_subscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_subscriber');
+program_version_ok('pg_subscriber');
+program_options_handling_ok('pg_subscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_subscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+		'--subscriber-conninfo', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
new file mode 100644
index 0000000000..fbcd0fc82b
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_subscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_subscriber on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_subscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
-- 
2.34.1

#80Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Shlok Kyal (#79)
3 attachment(s)
RE: speed up a logical replica setup

Dear hackers,

We fixed some of the comments posted in the thread. We have created it
as top-up patch 0002 and 0003.

I found that the CFbot raised an ERROR. Also, it may not work well in case
of production build.
PSA the fixed patch set.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

Attachments:

v7-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchapplication/octet-stream; name=v7-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchDownload
From 773b6d187892a7e0aea0edf1547b86ad2b2c8e2f Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v7 1/3] Creates a new logical replica from a standby server

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server. Remove
the additional replication slot that was used to get the consistent LSN.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_subscriber.sgml           |  284 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_subscriber.c         | 1657 +++++++++++++++++
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   44 +
 .../t/041_pg_subscriber_standby.pl            |  139 ++
 9 files changed, 2153 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_subscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_subscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_subscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..3862c976d7 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgSubscriber       SYSTEM "pg_subscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
new file mode 100644
index 0000000000..553185c35f
--- /dev/null
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -0,0 +1,284 @@
+<!--
+doc/src/sgml/ref/pg_subscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgsubscriber">
+ <indexterm zone="app-pgsubscriber">
+  <primary>pg_subscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_subscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>create a new logical replica from a standby server</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_subscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a standby server and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_subscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a standby
+        server.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--publisher-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_subscriber</application> to output progress messages
+        and detailed information about each step.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_subscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_subscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_subscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the target data
+     directory is used by a standby server. Stop the standby server if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_subscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  Another
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_subscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_subscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_subscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_subscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> removes the additional replication
+     slot that was used to get the consistent LSN on the source server.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a standby server at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..266f4e515a 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..0e5384a1d5 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_subscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..f6281b7676 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_subscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_subscriber: $(WIN32RES) pg_subscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_subscriber$(X) '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_subscriber$(X) pg_subscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..ccfd7bb7a5 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_subscriber_sources = files(
+  'pg_subscriber.c'
+)
+
+if host_system == 'windows'
+  pg_subscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_subscriber',
+	'--FILEDESC', 'pg_subscriber - create a new logical replica from a standby server',])
+endif
+
+pg_subscriber = executable('pg_subscriber',
+  pg_subscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_subscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_subscriber.pl',
+      't/041_pg_subscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
new file mode 100644
index 0000000000..e998c29f9e
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -0,0 +1,1657 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_subscriber/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static char *use_primary_slot_name(void);
+static bool create_all_logical_replication_slots(LogicalRepInfo *dbinfo);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static char temp_replslot[NAMEDATALEN] = {0};
+static bool made_transient_replslot = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_subscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+
+	if (made_transient_replslot)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+			disconnect_database(conn);
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-conninfo=CONNINFO   publisher connection string\n"));
+	printf(_(" -S, --subscriber-conninfo=CONNINFO  subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name plus a fallback application name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	if (i > 0)
+		appendPQExpBufferChar(buf, ' ');
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Return a palloc'd slot name if the replication is using one.
+ */
+static char *
+use_primary_slot_name(void)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+	char	   *slot_name;
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT setting FROM pg_settings WHERE name = 'primary_slot_name'");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain parameter information: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+
+	/*
+	 * If primary_slot_name is an empty string, the current replication
+	 * connection is not using a replication slot, bail out.
+	 */
+	if (strcmp(PQgetvalue(res, 0, 0), "") == 0)
+	{
+		PQclear(res);
+		return NULL;
+	}
+
+	slot_name = pg_strdup(PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_replication_slots r INNER JOIN pg_stat_activity a ON (r.active_pid = a.pid) WHERE slot_name = '%s'", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		return NULL;
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return slot_name;
+}
+
+static bool
+create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+{
+	int			i;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
+		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
+		 * to reduce the probability of collision. By default, subscription
+		 * name is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_subscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (transient_replslot)
+		made_transient_replslot = true;
+	else
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_subscriber must have created this
+		 * publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_subscriber_ prefix followed by the exact
+			 * database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-conninfo", required_argument, NULL, 'P'},
+		{"subscriber-conninfo", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+	int			rc;
+
+	char	   *pg_ctl_cmd;
+
+	char	   *base_dir;
+	char	   *server_start_log;
+
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	int			i;
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_subscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&database_names, optarg))
+				{
+					simple_string_list_append(&database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("directory path for subscriber is too long");
+		exit(1);
+	}
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+	{
+		pg_log_error("could not create directory \"%s\": %m", base_dir);
+		exit(1);
+	}
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * Stop the subscriber if it is a standby server. Before executing the
+	 * transformation steps, make sure the subscriber is not running because
+	 * one of the steps is to modify some recovery parameters that require a
+	 * restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Since the standby server is running, check if it is using an
+		 * existing replication slot for WAL retention purposes. This
+		 * replication slot has no use after the transformation, hence, it
+		 * will be removed at the end of this process.
+		 */
+		primary_slot_name = use_primary_slot_name();
+		if (primary_slot_name != NULL)
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+
+		pg_log_info("subscriber is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+	}
+
+	/*
+	 * Create a replication slot for each database on the publisher.
+	 */
+	if (!create_all_logical_replication_slots(dbinfo))
+		exit(1);
+
+	/*
+	 * Create a logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. We cannot use the last created replication slot
+	 * because the consistent LSN should be obtained *after* the base backup
+	 * finishes (and the base backup should include the logical replication
+	 * slots).
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 *
+	 * A temporary replication slot is not used here to avoid keeping a
+	 * replication connection open (depending when base backup was taken, the
+	 * connection should be open for a few hours).
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	server_start_log = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("log file path is too long");
+		exit(1);
+	}
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create a publication for each database. This step should be executed
+	 * after promoting the subscriber to avoid replicating unnecessary
+	 * objects.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+
+		/* Connect to publisher. */
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		create_publication(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Create a subscription for each database.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * The transient replication slot is no longer required. Drop it.
+	 *
+	 * If the physical replication slot exists, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops the replication slot later.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+	{
+		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		if (primary_slot_name != NULL)
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+	}
+	else
+	{
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		if (primary_slot_name != NULL)
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	/*
+	 * Remove log file generated by this tool, if it runs successfully.
+	 * Otherwise, file is kept that may provide useful debugging information.
+	 */
+	unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
new file mode 100644
index 0000000000..4ebff76b2d
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_subscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_subscriber');
+program_version_ok('pg_subscriber');
+program_options_handling_ok('pg_subscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_subscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+		'--subscriber-conninfo', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
new file mode 100644
index 0000000000..fbcd0fc82b
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_subscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_subscriber on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_subscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
-- 
2.43.0

v7-0002-Address-some-comments-proposed-on-hackers.patchapplication/octet-stream; name=v7-0002-Address-some-comments-proposed-on-hackers.patchDownload
From cfb77f4c599417527f7bfbcb7e8d90a4b09b5108 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 22 Jan 2024 12:42:34 +0530
Subject: [PATCH v7 2/3] Address some comments proposed on -hackers

The patch has following changes:

* Some comments reported on the thread
* Add a timeout option for the recovery option
* Reject if the target server is not a standby
* Reject when the --subscriber-conninfo specifies non-local server
* Add -u and -p options
* Check wal_level and max_replication_slot parameters
---
 doc/src/sgml/ref/pg_subscriber.sgml           |  21 +-
 src/bin/pg_basebackup/pg_subscriber.c         | 911 +++++++++++-------
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   9 +-
 .../t/041_pg_subscriber_standby.pl            |   8 +-
 4 files changed, 601 insertions(+), 348 deletions(-)

diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
index 553185c35f..eaabfc7053 100644
--- a/doc/src/sgml/ref/pg_subscriber.sgml
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -16,12 +16,18 @@ PostgreSQL documentation
 
  <refnamediv>
   <refname>pg_subscriber</refname>
-  <refpurpose>create a new logical replica from a standby server</refpurpose>
+  <refpurpose>Convert a standby replica to a logical replica</refpurpose>
  </refnamediv>
 
  <refsynopsisdiv>
   <cmdsynopsis>
    <command>pg_subscriber</command>
+   <arg choice="plain"><option>-D</option></arg>
+   <arg choice="plain"><replaceable>datadir</replaceable></arg>
+   <arg choice="plain"><option>-P</option>
+   <replaceable>publisher-conninfo</replaceable></arg>
+   <arg choice="plain"><option>-S</option></arg>
+   <arg choice="plain"><replaceable>subscriber-conninfo</replaceable></arg>
    <arg rep="repeat"><replaceable>option</replaceable></arg>
   </cmdsynopsis>
  </refsynopsisdiv>
@@ -29,17 +35,18 @@ PostgreSQL documentation
  <refsect1>
   <title>Description</title>
   <para>
-   <application>pg_subscriber</application> takes the publisher and subscriber
-   connection strings, a cluster directory from a standby server and a list of
-   database names and it sets up a new logical replica using the physical
-   recovery process.
+   pg_subscriber creates a new <link
+   linkend="logical-replication-subscription">subscriber</link> from a physical
+   standby server. This allows users to quickly set up logical replication
+   system.
   </para>
 
   <para>
-   The <application>pg_subscriber</application> should be run at the target
+   The <application>pg_subscriber</application> has to be run at the target
    server. The source server (known as publisher server) should accept logical
    replication connections from the target server (known as subscriber server).
-   The target server should accept local logical replication connection.
+   The target server should accept logical replication connection from
+   localhost.
   </para>
  </refsect1>
 
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index e998c29f9e..3880d15ef9 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * pg_subscriber.c
- *	  Create a new logical replica from a standby server
+ *	  Convert a standby replica to a logical replica
  *
  * Copyright (C) 2024, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
- *		src/bin/pg_subscriber/pg_subscriber.c
+ *		src/bin/pg_basebackup/pg_subscriber.c
  *
  *-------------------------------------------------------------------------
  */
@@ -32,81 +32,122 @@
 
 #define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
 
-typedef struct LogicalRepInfo
+typedef struct LogicalRepPerdbInfo
 {
-	Oid			oid;			/* database OID */
-	char	   *dbname;			/* database name */
-	char	   *pubconninfo;	/* publication connection string for logical
-								 * replication */
-	char	   *subconninfo;	/* subscription connection string for logical
-								 * replication */
-	char	   *pubname;		/* publication name */
-	char	   *subname;		/* subscription name (also replication slot
-								 * name) */
-
-	bool		made_replslot;	/* replication slot was created */
-	bool		made_publication;	/* publication was created */
-	bool		made_subscription;	/* subscription was created */
-} LogicalRepInfo;
+	Oid		oid;
+	char   *dbname;
+	bool	made_replslot;	/* replication slot was created */
+	bool	made_publication;	/* publication was created */
+	bool	made_subscription;	/* subscription was created */
+} LogicalRepPerdbInfo;
+
+typedef struct
+{
+	LogicalRepPerdbInfo	   *perdb;			/* array of db infos */
+	int						ndbs;			/* number of db infos */
+} LogicalRepPerdbInfoArr;
+
+typedef struct PrimaryInfo
+{
+	char   *base_conninfo;
+	uint64	sysid;
+} PrimaryInfo;
+
+typedef struct StandbyInfo
+{
+	char   *base_conninfo;
+	char   *bindir;
+	char   *pgdata;
+	char   *primary_slot_name;
+	uint64	sysid;
+} StandbyInfo;
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname,
-							   const char *noderole);
-static bool get_exec_path(const char *path);
+static char *get_base_conninfo(char *conninfo, char *dbname);
+static bool get_exec_base_path(const char *path);
 static bool check_data_directory(const char *datadir);
+static void store_db_names(LogicalRepPerdbInfo **perdb, int ndbs);
+static void get_sysid_for_primary(PrimaryInfo *primary, char *dbname);
+static void get_control_for_standby(StandbyInfo *standby);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
-static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
-static PGconn *connect_database(const char *conninfo);
+static PGconn *connect_database(const char *base_conninfo, const char*dbname);
 static void disconnect_database(PGconn *conn);
-static uint64 get_sysid_from_conn(const char *conninfo);
-static uint64 get_control_from_datadir(const char *datadir);
-static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
-static char *use_primary_slot_name(void);
-static bool create_all_logical_replication_slots(LogicalRepInfo *dbinfo);
-static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-											 char *slot_name);
-static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *use_primary_slot_name(PrimaryInfo *primary, StandbyInfo *standby,
+								   LogicalRepPerdbInfo *perdb);
+static bool create_all_logical_replication_slots(PrimaryInfo *primary,
+												 LogicalRepPerdbInfoArr *dbarr);
+static char *create_logical_replication_slot(PGconn *conn, bool temporary,
+											 LogicalRepPerdbInfo *perdb);
+static void modify_sysid(const char *bindir, const char *datadir);
+static void drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+								  const char *slot_name);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
-static void wait_for_end_recovery(const char *conninfo);
-static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
-static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void wait_for_end_recovery(const char *base_conninfo,
+								  const char *dbname);
+static void create_publication(PGconn *conn, PrimaryInfo *primary,
+							   LogicalRepPerdbInfo *perdb);
+static void drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void create_subscription(PGconn *conn, StandbyInfo *standby,
+								char *base_conninfo,
+								LogicalRepPerdbInfo *perdb);
+static void drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void start_standby_server(StandbyInfo *standby, unsigned short subport,
+								 char *server_start_log);
+static char *construct_sub_conninfo(char *username, unsigned short subport);
 
 #define	USEC_PER_SEC	1000000
-#define	WAIT_INTERVAL	1		/* 1 second */
+#define DEFAULT_WAIT	60
+#define WAITS_PER_SEC	10              /* should divide USEC_PER_SEC evenly */
+#define DEF_PGSPORT		50111
 
 /* Options */
-static const char *progname;
-
-static char *subscriber_dir = NULL;
 static char *pub_conninfo_str = NULL;
-static char *sub_conninfo_str = NULL;
 static SimpleStringList database_names = {NULL, NULL};
-static char *primary_slot_name = NULL;
+static int	wait_seconds = DEFAULT_WAIT;
+static bool retain = false;
 static bool dry_run = false;
 
 static bool success = false;
+static const char *progname;
+static LogicalRepPerdbInfoArr dbarr;
+static PrimaryInfo primary;
+static StandbyInfo standby;
 
-static char *pg_ctl_path = NULL;
-static char *pg_resetwal_path = NULL;
+enum PGSWaitPMResult
+{
+	PGS_POSTMASTER_READY,
+	PGS_POSTMASTER_STANDBY,
+	PGS_POSTMASTER_STILL_STARTING,
+	PGS_POSTMASTER_FAILED
+};
 
-static LogicalRepInfo *dbinfo;
-static int	num_dbs = 0;
 
-static char temp_replslot[NAMEDATALEN] = {0};
-static bool made_transient_replslot = false;
+/*
+ * Build the replication slot and subscription name. The name must not exceed
+ * NAMEDATALEN - 1. This current schema uses a maximum of 36 characters
+ * (14 + 10 + 1 + 10 + '\0'). System identifier is included to reduce the
+ * probability of collision. By default, subscription name is used as
+ * replication slot name.
+ */
+static inline void
+get_subscription_name(Oid oid, int pid, char *subname, Size szsub)
+{
+	snprintf(subname, szsub, "pg_subscriber_%u_%d", oid, pid);
+}
 
-enum WaitPMResult
+/*
+ * Build the publication name. The name must not exceed NAMEDATALEN -
+ * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+ * '\0').
+ */
+static inline void
+get_publication_name(Oid oid, char *pubname, Size szpub)
 {
-	POSTMASTER_READY,
-	POSTMASTER_STANDBY,
-	POSTMASTER_STILL_STARTING,
-	POSTMASTER_FAILED
-};
+	snprintf(pubname, szpub, "pg_subscriber_%u", oid);
+}
 
 
 /*
@@ -125,41 +166,39 @@ cleanup_objects_atexit(void)
 	if (success)
 		return;
 
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
-		if (dbinfo[i].made_subscription)
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
+
+		if (perdb->made_subscription)
 		{
-			conn = connect_database(dbinfo[i].subconninfo);
+			conn = connect_database(standby.base_conninfo, perdb->dbname);
 			if (conn != NULL)
 			{
-				drop_subscription(conn, &dbinfo[i]);
+				drop_subscription(conn, perdb);
 				disconnect_database(conn);
 			}
 		}
 
-		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		if (perdb->made_publication || perdb->made_replslot)
 		{
-			conn = connect_database(dbinfo[i].pubconninfo);
+			conn = connect_database(primary.base_conninfo, perdb->dbname);
 			if (conn != NULL)
 			{
-				if (dbinfo[i].made_publication)
-					drop_publication(conn, &dbinfo[i]);
-				if (dbinfo[i].made_replslot)
-					drop_replication_slot(conn, &dbinfo[i], NULL);
+				if (perdb->made_publication)
+					drop_publication(conn, perdb);
+				if (perdb->made_replslot)
+				{
+					char replslotname[NAMEDATALEN];
+
+					get_subscription_name(perdb->oid, (int) getpid(),
+										  replslotname, NAMEDATALEN);
+					drop_replication_slot(conn, perdb, replslotname);
+				}
 				disconnect_database(conn);
 			}
 		}
 	}
-
-	if (made_transient_replslot)
-	{
-		conn = connect_database(dbinfo[0].pubconninfo);
-		if (conn != NULL)
-		{
-			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
-			disconnect_database(conn);
-		}
-	}
 }
 
 static void
@@ -184,17 +223,16 @@ usage(void)
 
 /*
  * Validate a connection string. Returns a base connection string that is a
- * connection string without a database name plus a fallback application name.
- * Since we might process multiple databases, each database name will be
- * appended to this base connection string to provide a final connection string.
- * If the second argument (dbname) is not null, returns dbname if the provided
- * connection string contains it. If option --database is not provided, uses
- * dbname as the only database to setup the logical replica.
- * It is the caller's responsibility to free the returned connection string and
- * dbname.
+ * connection string without a database name. Since we might process multiple
+ * databases, each database name will be appended to this base connection
+ * string to provide a final connection string. If the second argument (dbname)
+ * is not null, returns dbname if the provided connection string contains it.
+ * If option --database is not provided, uses dbname as the only database to
+ * setup the logical replica. It is the caller's responsibility to free the
+ * returned connection string and dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+get_base_conninfo(char *conninfo, char *dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -203,7 +241,7 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 	char	   *ret;
 	int			i;
 
-	pg_log_info("validating connection string on %s", noderole);
+	pg_log_info("validating connection string on publisher");
 
 	conn_opts = PQconninfoParse(conninfo, &errmsg);
 	if (conn_opts == NULL)
@@ -231,10 +269,6 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 		}
 	}
 
-	if (i > 0)
-		appendPQExpBufferChar(buf, ' ');
-	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
-
 	ret = pg_strdup(buf->data);
 
 	destroyPQExpBuffer(buf);
@@ -244,15 +278,16 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 }
 
 /*
- * Get the absolute path from other PostgreSQL binaries (pg_ctl and
- * pg_resetwal) that is used by it.
+ * Get the absolute binary path from another PostgreSQL binary (pg_ctl) and set
+ * to StandbyInfo.
  */
 static bool
-get_exec_path(const char *path)
+get_exec_base_path(const char *path)
 {
 	int			rc;
+	char		pg_ctl_path[MAXPGPATH];
+	char	   *p;
 
-	pg_ctl_path = pg_malloc(MAXPGPATH);
 	rc = find_other_exec(path, "pg_ctl",
 						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
 						 pg_ctl_path);
@@ -277,30 +312,12 @@ get_exec_path(const char *path)
 
 	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
 
-	pg_resetwal_path = pg_malloc(MAXPGPATH);
-	rc = find_other_exec(path, "pg_resetwal",
-						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
-						 pg_resetwal_path);
-	if (rc < 0)
-	{
-		char		full_path[MAXPGPATH];
-
-		if (find_my_exec(path, full_path) < 0)
-			strlcpy(full_path, progname, sizeof(full_path));
-		if (rc == -1)
-			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
-						 "same directory as \"%s\".\n"
-						 "Check your installation.",
-						 "pg_resetwal", progname, full_path);
-		else
-			pg_log_error("The program \"%s\" was found by \"%s\"\n"
-						 "but was not the same version as %s.\n"
-						 "Check your installation.",
-						 "pg_resetwal", full_path, progname);
-		return false;
-	}
+	/* Extract the directory part from the path */
+	p = strrchr(pg_ctl_path, 'p');
+	Assert(p);
 
-	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+	*p = '\0';
+	standby.bindir = pg_strdup(pg_ctl_path);
 
 	return true;
 }
@@ -364,49 +381,36 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
 }
 
 /*
- * Store publication and subscription information.
+ * Initialize per-db structure and store the name of databases
  */
-static LogicalRepInfo *
-store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+static void
+store_db_names(LogicalRepPerdbInfo **perdb, int ndbs)
 {
-	LogicalRepInfo *dbinfo;
 	SimpleStringListCell *cell;
 	int			i = 0;
 
-	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+	*perdb = (LogicalRepPerdbInfo *) pg_malloc0(sizeof(LogicalRepPerdbInfo) *
+											   ndbs);
 
 	for (cell = database_names.head; cell; cell = cell->next)
 	{
-		char	   *conninfo;
-
-		/* Publisher. */
-		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
-		dbinfo[i].pubconninfo = conninfo;
-		dbinfo[i].dbname = cell->val;
-		dbinfo[i].made_replslot = false;
-		dbinfo[i].made_publication = false;
-		dbinfo[i].made_subscription = false;
-		/* other struct fields will be filled later. */
-
-		/* Subscriber. */
-		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
-		dbinfo[i].subconninfo = conninfo;
-
+		(*perdb)[i].dbname = pg_strdup(cell->val);
 		i++;
 	}
-
-	return dbinfo;
 }
 
 static PGconn *
-connect_database(const char *conninfo)
+connect_database(const char *base_conninfo, const char*dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	const char *rconninfo;
+
+	char	   *rconninfo;
+	char	   *concat_conninfo = concat_conninfo_dbname(base_conninfo,
+														 dbname);
 
 	/* logical replication mode */
-	rconninfo = psprintf("%s replication=database", conninfo);
+	rconninfo = psprintf("%s replication=database", concat_conninfo);
 
 	conn = PQconnectdb(rconninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
@@ -424,6 +428,9 @@ connect_database(const char *conninfo)
 	}
 	PQclear(res);
 
+	pfree(rconninfo);
+	pfree(concat_conninfo);
+
 	return conn;
 }
 
@@ -436,19 +443,18 @@ disconnect_database(PGconn *conn)
 }
 
 /*
- * Obtain the system identifier using the provided connection. It will be used
- * to compare if a data directory is a clone of another one.
+ * Obtain the system identifier from the primary server. It will be used to
+ * compare if a data directory is a clone of another one.
  */
-static uint64
-get_sysid_from_conn(const char *conninfo)
+static void
+get_sysid_for_primary(PrimaryInfo *primary, char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from publisher");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(primary->base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -471,43 +477,39 @@ get_sysid_from_conn(const char *conninfo)
 		exit(1);
 	}
 
-	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+	primary->sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
 
-	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) primary->sysid);
 
 	disconnect_database(conn);
-
-	return sysid;
 }
 
 /*
- * Obtain the system identifier from control file. It will be used to compare
- * if a data directory is a clone of another one. This routine is used locally
- * and avoids a replication connection.
+ * Obtain the system identifier from a standby server. It will be used to
+ * compare if a data directory is a clone of another one. This routine is used
+ * locally and avoids a replication connection.
  */
-static uint64
-get_control_from_datadir(const char *datadir)
+static void
+get_control_for_standby(StandbyInfo *standby)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from subscriber");
 
-	cf = get_controlfile(datadir, &crc_ok);
+	cf = get_controlfile(standby->pgdata, &crc_ok);
 	if (!crc_ok)
 	{
 		pg_log_error("control file appears to be corrupt");
 		exit(1);
 	}
 
-	sysid = cf->system_identifier;
+	standby->sysid = cf->system_identifier;
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) standby->sysid);
 
 	pfree(cf);
-
-	return sysid;
 }
 
 /*
@@ -516,7 +518,7 @@ get_control_from_datadir(const char *datadir)
  * files from one of the systems might be used in the other one.
  */
 static void
-modify_sysid(const char *pg_resetwal_path, const char *datadir)
+modify_sysid(const char *bindir, const char *datadir)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
@@ -551,7 +553,7 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
 
 	pg_log_info("running pg_resetwal on the subscriber");
 
-	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\"", bindir, datadir);
 
 	pg_log_debug("command is: %s", cmd_str);
 
@@ -571,14 +573,15 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
  * Return a palloc'd slot name if the replication is using one.
  */
 static char *
-use_primary_slot_name(void)
+use_primary_slot_name(PrimaryInfo *primary, StandbyInfo *standby,
+					  LogicalRepPerdbInfo *perdb)
 {
 	PGconn	   *conn;
 	PGresult   *res;
 	PQExpBuffer str = createPQExpBuffer();
 	char	   *slot_name;
 
-	conn = connect_database(dbinfo[0].subconninfo);
+	conn = connect_database(standby->base_conninfo, perdb->dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -604,7 +607,7 @@ use_primary_slot_name(void)
 
 	disconnect_database(conn);
 
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary->base_conninfo, perdb->dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -634,17 +637,19 @@ use_primary_slot_name(void)
 }
 
 static bool
-create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+create_all_logical_replication_slots(PrimaryInfo *primary,
+									 LogicalRepPerdbInfoArr *dbarr)
 {
 	int			i;
 
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr->ndbs; i++)
 	{
 		PGconn	   *conn;
 		PGresult   *res;
 		char		replslotname[NAMEDATALEN];
+		LogicalRepPerdbInfo *perdb = &dbarr->perdb[i];
 
-		conn = connect_database(dbinfo[i].pubconninfo);
+		conn = connect_database(primary->base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
@@ -664,27 +669,14 @@ create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
 		}
 
 		/* Remember database OID. */
-		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		perdb->oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
 
 		PQclear(res);
 
-		/*
-		 * Build the replication slot name. The name must not exceed
-		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
-		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
-		 * to reduce the probability of collision. By default, subscription
-		 * name is used as replication slot name.
-		 */
-		snprintf(replslotname, sizeof(replslotname),
-				 "pg_subscriber_%u_%d",
-				 dbinfo[i].oid,
-				 (int) getpid());
-		dbinfo[i].subname = pg_strdup(replslotname);
+		get_subscription_name(perdb->oid, (int) getpid(), replslotname, NAMEDATALEN);
 
 		/* Create replication slot on publisher. */
-		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
-			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
-		else
+		if (create_logical_replication_slot(conn, false, perdb) == NULL && !dry_run)
 			return false;
 
 		disconnect_database(conn);
@@ -701,30 +693,36 @@ create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
  * result set that contains the consistent LSN.
  */
 static char *
-create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-								char *slot_name)
+create_logical_replication_slot(PGconn *conn, bool temporary,
+								LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res = NULL;
 	char	   *lsn = NULL;
-	bool		transient_replslot = false;
+	char		slot_name[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
 	/*
-	 * If no slot name is informed, it is a transient replication slot used
-	 * only for catch up purposes.
+	 * Construct a name of logical replication slot. The formatting is
+	 * different depends on its persistency.
+	 *
+	 * For persistent slots: the name must be same as the subscription.
+	 * For temporary slots: OID is not needed, but another string is added.
 	 */
-	if (slot_name[0] == '\0')
-	{
+	if (!temporary)
+		get_subscription_name(perdb->oid, (int) getpid(), slot_name, NAMEDATALEN);
+	else
 		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
 				 (int) getpid());
-		transient_replslot = true;
-	}
 
-	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+
+	if(temporary)
+		appendPQExpBufferStr(str, " TEMPORARY");
+
 	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
 
 	pg_log_debug("command is: %s", str->data);
@@ -734,17 +732,14 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						 PQresultErrorMessage(res));
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, perdb->dbname, PQresultErrorMessage(res));
 			return lsn;
 		}
 	}
 
 	/* for cleanup purposes */
-	if (transient_replslot)
-		made_transient_replslot = true;
-	else
-		dbinfo->made_replslot = true;
+	perdb->made_replslot = true;
 
 	if (!dry_run)
 	{
@@ -758,14 +753,15 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 }
 
 static void
-drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+					  const char *slot_name)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
 
@@ -775,7 +771,7 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, perdb->dbname,
 						 PQerrorMessage(conn));
 
 		PQclear(res);
@@ -825,19 +821,22 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
  * Returns after the server finishes the recovery process.
  */
 static void
-wait_for_end_recovery(const char *conninfo)
+wait_for_end_recovery(const char *base_conninfo, const char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	int			status = POSTMASTER_STILL_STARTING;
+	int			status = PGS_POSTMASTER_STILL_STARTING;
+	int			cnt;
+	int			rc;
+	char	   *pg_ctl_cmd;
 
 	pg_log_info("waiting the postmaster to reach the consistent state");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
-	for (;;)
+	for (cnt = 0; cnt < wait_seconds * WAITS_PER_SEC; cnt++)
 	{
 		bool		in_recovery;
 
@@ -865,17 +864,32 @@ wait_for_end_recovery(const char *conninfo)
 		 */
 		if (!in_recovery || dry_run)
 		{
-			status = POSTMASTER_READY;
+			status = PGS_POSTMASTER_READY;
 			break;
 		}
 
 		/* Keep waiting. */
-		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+		pg_usleep(USEC_PER_SEC / WAITS_PER_SEC);
 	}
 
 	disconnect_database(conn);
 
-	if (status == POSTMASTER_STILL_STARTING)
+	/*
+	 * If timeout is reached exit the pg_subscriber and stop the standby node.
+	 */
+	if (cnt >= wait_seconds * WAITS_PER_SEC)
+	{
+		pg_log_error("recovery timed out");
+
+		pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
+							  standby.bindir, standby.pgdata);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+		exit(1);
+	}
+
+	if (status == PGS_POSTMASTER_STILL_STARTING)
 	{
 		pg_log_error("server did not end recovery");
 		exit(1);
@@ -888,17 +902,21 @@ wait_for_end_recovery(const char *conninfo)
  * Create a publication that includes all tables in the database.
  */
 static void
-create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+create_publication(PGconn *conn, PrimaryInfo *primary,
+				   LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
 	/* Check if the publication needs to be created. */
 	appendPQExpBuffer(str,
 					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
-					  dbinfo->pubname);
+					  pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
@@ -918,7 +936,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		 */
 		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
 		{
-			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			pg_log_info("publication \"%s\" already exists", pubname);
 			return;
 		}
 		else
@@ -931,7 +949,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 			 * database oid in which puballtables is false.
 			 */
 			pg_log_error("publication \"%s\" does not replicate changes for all tables",
-						 dbinfo->pubname);
+						 pubname);
 			pg_log_error_hint("Consider renaming this publication.");
 			PQclear(res);
 			PQfinish(conn);
@@ -942,9 +960,9 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	PQclear(res);
 	resetPQExpBuffer(str);
 
-	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("creating publication \"%s\" on database \"%s\"", pubname, perdb->dbname);
 
-	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -954,14 +972,14 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
-						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+						 pubname, perdb->dbname, PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
 		}
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_publication = true;
+	perdb->made_publication = true;
 
 	if (!dry_run)
 		PQclear(res);
@@ -973,16 +991,19 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove publication if it couldn't finish all steps.
  */
 static void
-drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
 
-	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", pubname, perdb->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -990,7 +1011,7 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", pubname, perdb->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1011,19 +1032,27 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * initial location.
  */
 static void
-create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+create_subscription(PGconn *conn, StandbyInfo *standby, char *base_conninfo,
+					LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
 					  "WITH (create_slot = false, copy_data = false, enabled = false)",
-					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+					  subname, concat_conninfo_dbname(base_conninfo, perdb->dbname), pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1033,14 +1062,14 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+						 subname, perdb->dbname, PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
 		}
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_subscription = true;
+	perdb->made_subscription = true;
 
 	if (!dry_run)
 		PQclear(res);
@@ -1052,16 +1081,19 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove subscription if it couldn't finish all steps.
  */
 static void
-drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", subname, perdb->dbname);
 
-	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1069,7 +1101,7 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", subname, perdb->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1088,18 +1120,21 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * printing purposes.
  */
 static void
-set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb, const char *lsn)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 	Oid			suboid;
 	char		originname[NAMEDATALEN];
 	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
 	appendPQExpBuffer(str,
-					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", subname);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1140,7 +1175,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	PQclear(res);
 
 	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
-				originname, lsnstr, dbinfo->dbname);
+				originname, lsnstr, perdb->dbname);
 
 	resetPQExpBuffer(str);
 	appendPQExpBuffer(str,
@@ -1154,7 +1189,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
 			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
-						 dbinfo->subname, PQresultErrorMessage(res));
+						 subname, PQresultErrorMessage(res));
 			PQfinish(conn);
 			exit(1);
 		}
@@ -1173,16 +1208,20 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
  * of this setup.
  */
 static void
-enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
 
-	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1191,7 +1230,7 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
-			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+			pg_log_error("could not enable subscription \"%s\": %s", subname,
 						 PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
@@ -1203,6 +1242,61 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	destroyPQExpBuffer(str);
 }
 
+static void
+start_standby_server(StandbyInfo *standby, unsigned short subport,
+					 char *server_start_log)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	int			rc;
+	char	   *pg_ctl_cmd;
+
+	if (server_start_log[0] == '\0')
+	{
+		/* append timestamp with ISO 8601 format. */
+		gettimeofday(&time, NULL);
+		tt = (time_t) time.tv_sec;
+		strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+		snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+				 ".%03d", (int) (time.tv_usec / 1000));
+
+		len = snprintf(server_start_log, MAXPGPATH,
+					   "%s/%s/server_start_%s.log", standby->pgdata,
+					   PGS_OUTPUT_DIR, timebuf);
+		if (len >= MAXPGPATH)
+		{
+			pg_log_error("log file path is too long");
+			exit(1);
+		}
+	}
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -o \"-p %d\" -l \"%s\"",
+						  standby->bindir,
+						  standby->pgdata, subport, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static char *
+construct_sub_conninfo(char *username, unsigned short subport)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	if (username)
+		appendPQExpBuffer(buf, "user=%s ", username);
+
+	appendPQExpBuffer(buf, "port=%d fallback_application_name=%s",
+					  subport, progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
 int
 main(int argc, char **argv)
 {
@@ -1214,6 +1308,10 @@ main(int argc, char **argv)
 		{"publisher-conninfo", required_argument, NULL, 'P'},
 		{"subscriber-conninfo", required_argument, NULL, 'S'},
 		{"database", required_argument, NULL, 'd'},
+		{"timeout", required_argument, NULL, 't'},
+		{"username", required_argument, NULL, 'u'},
+		{"port", required_argument, NULL, 'p'},
+		{"retain", no_argument, NULL, 'r'},
 		{"dry-run", no_argument, NULL, 'n'},
 		{"verbose", no_argument, NULL, 'v'},
 		{NULL, 0, NULL, 0}
@@ -1225,20 +1323,15 @@ main(int argc, char **argv)
 
 	char	   *pg_ctl_cmd;
 
-	char	   *base_dir;
-	char	   *server_start_log;
-
-	char		timebuf[128];
-	struct timeval time;
-	time_t		tt;
+	char		base_dir[MAXPGPATH];
+	char		server_start_log[MAXPGPATH] = {0};
 	int			len;
 
-	char	   *pub_base_conninfo = NULL;
-	char	   *sub_base_conninfo = NULL;
 	char	   *dbname_conninfo = NULL;
 
-	uint64		pub_sysid;
-	uint64		sub_sysid;
+	unsigned short subport = DEF_PGSPORT;
+	char	   *username = NULL;
+
 	struct stat statbuf;
 
 	PGconn	   *conn;
@@ -1250,6 +1343,13 @@ main(int argc, char **argv)
 
 	int			i;
 
+	PGresult   *res;
+
+	char	   *wal_level;
+	int			max_replication_slots;
+	int			nslots_old;
+	int			nslots_new;
+
 	pg_logging_init(argv[0]);
 	pg_logging_set_level(PG_LOG_WARNING);
 	progname = get_progname(argv[0]);
@@ -1286,28 +1386,40 @@ main(int argc, char **argv)
 	}
 #endif
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+	while ((c = getopt_long(argc, argv, "D:P:S:d:t:u:p:rnv",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
 		{
 			case 'D':
-				subscriber_dir = pg_strdup(optarg);
+				standby.pgdata = pg_strdup(optarg);
+				canonicalize_path(standby.pgdata);
 				break;
 			case 'P':
 				pub_conninfo_str = pg_strdup(optarg);
 				break;
-			case 'S':
-				sub_conninfo_str = pg_strdup(optarg);
-				break;
 			case 'd':
 				/* Ignore duplicated database names. */
 				if (!simple_string_list_member(&database_names, optarg))
 				{
 					simple_string_list_append(&database_names, optarg);
-					num_dbs++;
+					dbarr.ndbs++;
 				}
 				break;
+			case 't':
+				wait_seconds = atoi(optarg);
+				break;
+			case 'u':
+				pfree(username);
+				username = pg_strdup(optarg);
+				break;
+			case 'p':
+				if ((subport = atoi(optarg)) <= 0)
+					pg_fatal("invalid old port number");
+				break;
+			case 'r':
+				retain = true;
+				break;
 			case 'n':
 				dry_run = true;
 				break;
@@ -1335,7 +1447,7 @@ main(int argc, char **argv)
 	/*
 	 * Required arguments
 	 */
-	if (subscriber_dir == NULL)
+	if (standby.pgdata == NULL)
 	{
 		pg_log_error("no subscriber data directory specified");
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -1358,21 +1470,14 @@ main(int argc, char **argv)
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 		exit(1);
 	}
-	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
-										  "publisher");
-	if (pub_base_conninfo == NULL)
-		exit(1);
 
-	if (sub_conninfo_str == NULL)
-	{
-		pg_log_error("no subscriber connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
-	if (sub_base_conninfo == NULL)
+	primary.base_conninfo = get_base_conninfo(pub_conninfo_str,
+											  dbname_conninfo);
+	if (primary.base_conninfo == NULL)
 		exit(1);
 
+	standby.base_conninfo = construct_sub_conninfo(username, subport);
+
 	if (database_names.head == NULL)
 	{
 		pg_log_info("no database was specified");
@@ -1385,7 +1490,7 @@ main(int argc, char **argv)
 		if (dbname_conninfo)
 		{
 			simple_string_list_append(&database_names, dbname_conninfo);
-			num_dbs++;
+			dbarr.ndbs++;
 
 			pg_log_info("database \"%s\" was extracted from the publisher connection string",
 						dbname_conninfo);
@@ -1399,25 +1504,25 @@ main(int argc, char **argv)
 	}
 
 	/*
-	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 * Get the absolute path of binaries on the subscriber.
 	 */
-	if (!get_exec_path(argv[0]))
+	if (!get_exec_base_path(argv[0]))
 		exit(1);
 
 	/* rudimentary check for a data directory. */
-	if (!check_data_directory(subscriber_dir))
+	if (!check_data_directory(standby.pgdata))
 		exit(1);
 
-	/* Store database information for publisher and subscriber. */
-	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+	/* Store database information to dbarr */
+	store_db_names(&dbarr.perdb, dbarr.ndbs);
 
 	/*
 	 * Check if the subscriber data directory has the same system identifier
 	 * than the publisher data directory.
 	 */
-	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
-	sub_sysid = get_control_from_datadir(subscriber_dir);
-	if (pub_sysid != sub_sysid)
+	get_sysid_for_primary(&primary, dbarr.perdb[0].dbname);
+	get_control_for_standby(&standby);
+	if (primary.sysid != standby.sysid)
 	{
 		pg_log_error("subscriber data directory is not a copy of the source database cluster");
 		exit(1);
@@ -1426,8 +1531,8 @@ main(int argc, char **argv)
 	/*
 	 * Create the output directory to store any data generated by this tool.
 	 */
-	base_dir = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s",
+				   standby.pgdata, PGS_OUTPUT_DIR);
 	if (len >= MAXPGPATH)
 	{
 		pg_log_error("directory path for subscriber is too long");
@@ -1441,7 +1546,153 @@ main(int argc, char **argv)
 	}
 
 	/* subscriber PID file. */
-	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid",
+			  standby.pgdata);
+
+	/* Start the standby server anyway */
+	start_standby_server(&standby, subport, server_start_log);
+
+	/*
+	 * Check wal_level in publisher and the max_replication_slots of publisher
+	 */
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT count(*) from pg_replication_slots;");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain number of replication slots");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not determine parameter settings on publisher");
+		exit(1);
+	}
+
+	nslots_old = atoi(PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	res = PQexec(conn, "SELECT setting FROM pg_settings "
+				 "WHERE name IN ('wal_level', 'max_replication_slots') "
+				 "ORDER BY name DESC;");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain guc parameters on publisher");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 2)
+	{
+		pg_log_error("could not determine parameter settings on publisher");
+		exit(1);
+	}
+
+	wal_level = PQgetvalue(res, 0, 0);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("wal_level must be \"logical\", but is set to \"%s\"", wal_level);
+		exit(1);
+	}
+
+	max_replication_slots = atoi(PQgetvalue(res, 1, 0));
+	nslots_new = nslots_old + dbarr.ndbs + 1;
+
+	if (nslots_new > max_replication_slots)
+	{
+		pg_log_error("max_replication_slots (%d) must be greater than or equal to "
+					 "the number of replication slots required (%d)", max_replication_slots, nslots_new);
+		exit(1);
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	conn = connect_database(standby.base_conninfo, dbarr.perdb[0].dbname);
+	if (conn == NULL)
+		exit(1);
+
+	/*
+	 * Check the max_replication_slots in subscriber
+	 */
+	res = PQexec(conn, "SELECT count(*) from pg_replication_slots;");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain number of replication slots on subscriber");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not determine parameter settings on subscriber");
+		exit(1);
+	}
+
+	nslots_old = atoi(PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	res = PQexec(conn, "SELECT setting FROM pg_settings "
+				 "WHERE name = 'max_replication_slots';");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain guc parameters");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not determine parameter settings on publisher");
+		exit(1);
+	}
+
+	max_replication_slots = atoi(PQgetvalue(res, 0, 0));
+	nslots_new = nslots_old + dbarr.ndbs;
+
+	if (nslots_new > max_replication_slots)
+	{
+		pg_log_error("max_replication_slots (%d) must be greater than or equal to "
+					 "the number of replication slots required (%d)", max_replication_slots, nslots_new);
+		exit(1);
+	}
+
+	PQclear(res);
+
+	/*
+	 * Exit the pg_subscriber if the node is not a standby server.
+	 */
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("unexpected result from pg_is_in_recovery function");
+		exit(1);
+	}
+
+	/* Check if the server is in recovery */
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("pg_subscriber is supported only on standby server");
+		exit(1);
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", standby.pgdata);
 
 	/*
 	 * Stop the subscriber if it is a standby server. Before executing the
@@ -1457,14 +1708,18 @@ main(int argc, char **argv)
 		 * replication slot has no use after the transformation, hence, it
 		 * will be removed at the end of this process.
 		 */
-		primary_slot_name = use_primary_slot_name();
-		if (primary_slot_name != NULL)
-			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		standby.primary_slot_name = use_primary_slot_name(&primary,
+														   &standby,
+														   &dbarr.perdb[0]);
+		if (standby.primary_slot_name != NULL)
+			pg_log_info("primary has replication slot \"%s\"",
+						standby.primary_slot_name);
 
 		pg_log_info("subscriber is up and running");
 		pg_log_info("stopping the server to start the transformation steps");
 
-		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
+							  standby.bindir, standby.pgdata);
 		rc = system(pg_ctl_cmd);
 		pg_ctl_status(pg_ctl_cmd, rc, 0);
 	}
@@ -1472,7 +1727,7 @@ main(int argc, char **argv)
 	/*
 	 * Create a replication slot for each database on the publisher.
 	 */
-	if (!create_all_logical_replication_slots(dbinfo))
+	if (!create_all_logical_replication_slots(&primary, &dbarr))
 		exit(1);
 
 	/*
@@ -1492,11 +1747,11 @@ main(int argc, char **argv)
 	 * replication connection open (depending when base backup was taken, the
 	 * connection should be open for a few hours).
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
-	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
-													 temp_replslot);
+	consistent_lsn = create_logical_replication_slot(conn, true,
+													 &dbarr.perdb[0]);
 
 	/*
 	 * Write recovery parameters.
@@ -1522,7 +1777,7 @@ main(int argc, char **argv)
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  consistent_lsn);
-		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+		WriteRecoveryConfig(conn, standby.pgdata, recoveryconfcontents);
 	}
 	disconnect_database(conn);
 
@@ -1532,54 +1787,29 @@ main(int argc, char **argv)
 	 * Start subscriber and wait until accepting connections.
 	 */
 	pg_log_info("starting the subscriber");
-
-	/* append timestamp with ISO 8601 format. */
-	gettimeofday(&time, NULL);
-	tt = (time_t) time.tv_sec;
-	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
-	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
-			 ".%03d", (int) (time.tv_usec / 1000));
-
-	server_start_log = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
-	if (len >= MAXPGPATH)
-	{
-		pg_log_error("log file path is too long");
-		exit(1);
-	}
-
-	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
-	rc = system(pg_ctl_cmd);
-	pg_ctl_status(pg_ctl_cmd, rc, 1);
+	start_standby_server(&standby, subport, server_start_log);
 
 	/*
 	 * Waiting the subscriber to be promoted.
 	 */
-	wait_for_end_recovery(dbinfo[0].subconninfo);
+	wait_for_end_recovery(standby.base_conninfo, dbarr.perdb[0].dbname);
 
 	/*
 	 * Create a publication for each database. This step should be executed
 	 * after promoting the subscriber to avoid replicating unnecessary
 	 * objects.
 	 */
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
-		char		pubname[NAMEDATALEN];
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
 
 		/* Connect to publisher. */
-		conn = connect_database(dbinfo[i].pubconninfo);
+		conn = connect_database(primary.base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
-		/*
-		 * Build the publication name. The name must not exceed NAMEDATALEN -
-		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
-		 * '\0').
-		 */
-		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
-		dbinfo[i].pubname = pg_strdup(pubname);
-
-		create_publication(conn, &dbinfo[i]);
+		/* Also create a publication */
+		create_publication(conn, &primary, perdb);
 
 		disconnect_database(conn);
 	}
@@ -1587,20 +1817,25 @@ main(int argc, char **argv)
 	/*
 	 * Create a subscription for each database.
 	 */
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
+
 		/* Connect to subscriber. */
-		conn = connect_database(dbinfo[i].subconninfo);
+		conn = connect_database(standby.base_conninfo, perdb->dbname);
+
 		if (conn == NULL)
 			exit(1);
 
-		create_subscription(conn, &dbinfo[i]);
+		create_subscription(conn, &standby, primary.base_conninfo, perdb);
 
 		/* Set the replication progress to the correct LSN. */
-		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+		set_replication_progress(conn, perdb, consistent_lsn);
 
 		/* Enable subscription. */
-		enable_subscription(conn, &dbinfo[i]);
+		enable_subscription(conn, perdb);
+
+		drop_publication(conn, perdb);
 
 		disconnect_database(conn);
 	}
@@ -1613,19 +1848,21 @@ main(int argc, char **argv)
 	 * XXX we might not fail here. Instead, we provide a warning so the user
 	 * eventually drops the replication slot later.
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
 	if (conn == NULL)
 	{
-		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
-		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		char *primary_slot_name = standby.primary_slot_name;
+
 		if (primary_slot_name != NULL)
 			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
 	}
 	else
 	{
-		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[0];
+		char *primary_slot_name = standby.primary_slot_name;
+
 		if (primary_slot_name != NULL)
-			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+			drop_replication_slot(conn, perdb, primary_slot_name);
 		disconnect_database(conn);
 	}
 
@@ -1634,20 +1871,22 @@ main(int argc, char **argv)
 	 */
 	pg_log_info("stopping the subscriber");
 
-	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
+						  standby.bindir, standby.pgdata);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 0);
 
 	/*
 	 * Change system identifier.
 	 */
-	modify_sysid(pg_resetwal_path, subscriber_dir);
+	modify_sysid(standby.bindir, standby.pgdata);
 
 	/*
 	 * Remove log file generated by this tool, if it runs successfully.
 	 * Otherwise, file is kept that may provide useful debugging information.
 	 */
-	unlink(server_start_log);
+	if (!retain)
+		unlink(server_start_log);
 
 	success = true;
 
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
index 4ebff76b2d..9915b8cb3c 100644
--- a/src/bin/pg_basebackup/t/040_pg_subscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -37,8 +37,13 @@ command_fails(
 		'--verbose',
 		'--pgdata', $datadir,
 		'--publisher-conninfo', 'dbname=postgres',
-		'--subscriber-conninfo', 'dbname=postgres'
 	],
 	'no database name specified');
-
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+	],
+	'subscriber connection string specnfied non-local server');
 done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
index fbcd0fc82b..4e26607611 100644
--- a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -51,25 +51,27 @@ $node_s->start;
 $node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
 $node_p->wait_for_replay_catchup($node_s);
 
+$node_f->stop;
+
 # Run pg_subscriber on about-to-fail node F
 command_fails(
 	[
 		'pg_subscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
 		'--publisher-conninfo', $node_p->connstr('pg1'),
-		'--subscriber-conninfo', $node_f->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
 	'subscriber data directory is not a copy of the source database cluster');
 
+$node_s->stop;
+
 # dry run mode on node S
 command_ok(
 	[
 		'pg_subscriber', '--verbose', '--dry-run',
 		'--pgdata', $node_s->data_dir,
 		'--publisher-conninfo', $node_p->connstr('pg1'),
-		'--subscriber-conninfo', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
@@ -82,6 +84,7 @@ $node_s->start;
 # Check if node S is still a standby
 is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
 	't', 'standby is in recovery');
+$node_s->stop;
 
 # Run pg_subscriber on node S
 command_ok(
@@ -89,7 +92,6 @@ command_ok(
 		'pg_subscriber', '--verbose',
 		'--pgdata', $node_s->data_dir,
 		'--publisher-conninfo', $node_p->connstr('pg1'),
-		'--subscriber-conninfo', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
-- 
2.43.0

v7-0003-Fix-publication-does-not-exist-error.patchapplication/octet-stream; name=v7-0003-Fix-publication-does-not-exist-error.patchDownload
From fe1c57b974a2228b5ab2349b31de16d04db24aac Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 22 Jan 2024 12:36:20 +0530
Subject: [PATCH v7 3/3] Fix publication does not exist error.

Fix publication does not exist error.
---
 src/bin/pg_basebackup/pg_subscriber.c | 23 +++--------------------
 1 file changed, 3 insertions(+), 20 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index 3880d15ef9..355738c20c 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -679,6 +679,9 @@ create_all_logical_replication_slots(PrimaryInfo *primary,
 		if (create_logical_replication_slot(conn, false, perdb) == NULL && !dry_run)
 			return false;
 
+		/* Also create a publication */
+		create_publication(conn, primary, perdb);
+
 		disconnect_database(conn);
 	}
 
@@ -1794,26 +1797,6 @@ main(int argc, char **argv)
 	 */
 	wait_for_end_recovery(standby.base_conninfo, dbarr.perdb[0].dbname);
 
-	/*
-	 * Create a publication for each database. This step should be executed
-	 * after promoting the subscriber to avoid replicating unnecessary
-	 * objects.
-	 */
-	for (i = 0; i < dbarr.ndbs; i++)
-	{
-		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
-
-		/* Connect to publisher. */
-		conn = connect_database(primary.base_conninfo, perdb->dbname);
-		if (conn == NULL)
-			exit(1);
-
-		/* Also create a publication */
-		create_publication(conn, &primary, perdb);
-
-		disconnect_database(conn);
-	}
-
 	/*
 	 * Create a subscription for each database.
 	 */
-- 
2.43.0

#81Euler Taveira
euler@eulerto.com
In reply to: Amit Kapila (#78)
Re: speed up a logical replica setup

On Mon, Jan 22, 2024, at 6:22 AM, Amit Kapila wrote:

On Mon, Jan 22, 2024 at 2:38 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Yet other options could be
pg_buildsubscriber, pg_makesubscriber as 'build' or 'make' in the name
sounds like we are doing some work to create the subscriber which I
think is the case here.

I see your point here. pg_createsubscriber is not like createuser in
that it just runs an SQL command. It does something different than
CREATE SUBSCRIBER.

Right.

Subscriber has a different meaning of subscription. Subscription is an SQL
object. Subscriber is the server (node in replication terminology) where the
subscription resides. Having said that pg_createsubscriber doesn't seem a bad
name because you are creating a new subscriber. (Indeed, you are transforming /
converting but "create" seems closer and users can infer that it is a tool to
build a new logical replica.

So a different verb would make that clearer. Maybe

something from here: https://www.thesaurus.com/browse/convert

I read the link and found a good verb "switch". So, how about using "pg_switchsubscriber"?

I also initially thought on these lines and came up with a name like
pg_convertsubscriber but didn't feel strongly about it as that would
have sounded meaningful if we use a name like
pg_convertstandbytosubscriber. Now, that has become too long. Having
said that, I am not opposed to it having a name on those lines. BTW,
another option that occurred to me today is pg_preparesubscriber. We
internally create slots and then wait for wal, etc. which makes me
sound like adding 'prepare' in the name can also explain the purpose.

I think "convert" and "transform" fit for this case. However, "create",
"convert" and "transform" have 6, 7 and 9 characters, respectively. I suggest
that we avoid long names (subscriber already has 10 characters). My preference
is pg_createsubscriber.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#82Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#76)
Re: speed up a logical replica setup

On Mon, Jan 22, 2024, at 4:06 AM, Hayato Kuroda (Fujitsu) wrote:

I analyzed and found a reason. This is because publications are invisible for some transactions.

As the first place, below operations were executed in this case.
Tuples were inserted after getting consistent_lsn, but before starting the standby.
After doing the workload, I confirmed again that the publication was created.

1. on primary, logical replication slots were created.
2. on primary, another replication slot was created.
3. ===on primary, some tuples were inserted. ===
4. on standby, a server process was started
5. on standby, the process waited until all changes have come.
6. on primary, publications were created.
7. on standby, subscriptions were created.
8. on standby, a replication progress for each subscriptions was set to given LSN (got at step2).
=====pg_subscriber finished here=====
9. on standby, a server process was started again
10. on standby, subscriptions were enabled. They referred slots created at step1.
11. on primary, decoding was started but ERROR was raised.

Good catch! It is a design flaw.

In this case, tuples were inserted *before creating publication*.
So I thought that the decoded transaction could not see the publication because
it was committed after insertions.

One solution is to create a publication before creating a consistent slot.
Changes which came before creating the slot were surely replicated to the standby,
so upcoming transactions can see the object. We are planning to patch set to fix
the issue in this approach.

I'll include a similar code in the next patch and also explain why we should
create the publication earlier. (I'm renaming
create_all_logical_replication_slots to setup_publisher and calling
create_publication from there and also adding the proposed GUC checks in it.)

--
Euler Taveira
EDB https://www.enterprisedb.com/

#83Euler Taveira
euler@eulerto.com
In reply to: Shlok Kyal (#79)
Re: speed up a logical replica setup

On Mon, Jan 22, 2024, at 6:30 AM, Shlok Kyal wrote:

We fixed some of the comments posted in the thread. We have created it
as top-up patch 0002 and 0003.

Cool.

0002 patch contains the following changes:
* Add a timeout option for the recovery option, per [1]. The code was
basically ported from pg_ctl.c.
* Reject if the target server is not a standby, per [2]
* Raise FATAL error if --subscriber-conninfo specifies non-local server, per [3]
(not sure it is really needed, so feel free reject the part.)
* Add check for max_replication_slots and wal_level; as per [4]
* Add -u and -p options; as per [5]
* Addressed comment except 5 and 8 in [6] and comment in [7]

My suggestion is that you create separate patches for each change. It helps
with review and alternative proposals. Some of these items conflict with what I
have in my local branch and removing one of them is time consuming. For this
one, I did the job but let's avoid rework.

0003 patch contains fix for bug reported in [8].

LGTM. As I said in the other email, I included it.

I'll post a new one soon.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#84Shubham Khanna
khannashubham1197@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#80)
Re: speed up a logical replica setup

On Tue, Jan 23, 2024 at 7:41 AM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear hackers,

We fixed some of the comments posted in the thread. We have created it
as top-up patch 0002 and 0003.

I found that the CFbot raised an ERROR. Also, it may not work well in case
of production build.
PSA the fixed patch set.

Segmentation fault was found after testing the given command(There is
an extra '/' between 'new_standby2' and '-P') '$ gdb --args
./pg_subscriber -D ../new_standby2 / -P "host=localhost
port=5432 dbname=postgres" -d postgres'
While executing the above command, I got the following error:
pg_subscriber: error: too many command-line arguments (first is "/")
pg_subscriber: hint: Try "pg_subscriber --help" for more information.

Program received signal SIGSEGV, Segmentation fault.
0x0000555555557e5b in cleanup_objects_atexit () at pg_subscriber.c:173
173 if (perdb->made_subscription)
(gdb) p perdb
$1 = (LogicalRepPerdbInfo *) 0x0

Thanks and Regards,
Shubham Khanna.

#85Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Shubham Khanna (#84)
4 attachment(s)
RE: speed up a logical replica setup

Dear Shubham,

Segmentation fault was found after testing the given command(There is
an extra '/' between 'new_standby2' and '-P') '$ gdb --args
./pg_subscriber -D ../new_standby2 / -P "host=localhost
port=5432 dbname=postgres" -d postgres'
While executing the above command, I got the following error:
pg_subscriber: error: too many command-line arguments (first is "/")
pg_subscriber: hint: Try "pg_subscriber --help" for more information.

Program received signal SIGSEGV, Segmentation fault.
0x0000555555557e5b in cleanup_objects_atexit () at pg_subscriber.c:173
173 if (perdb->made_subscription)
(gdb) p perdb
$1 = (LogicalRepPerdbInfo *) 0x0

Good catch, I could reproduce the issue. This crash was occurred because the
cleanup function was called before initialization memory.

There are several ways to fix it, but I chose to move the callback registration
behind. The function does actual tasks only after database objects are created.
So 0004 registers the function just before doing them. The memory allocation has
been done at that time. If required, Assert() can be added in the callback.

Can you test it and confirm the issue was solved?

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

Attachments:

v8-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchapplication/octet-stream; name=v8-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchDownload
From 26bc1ee9371409e360588ac6aacafaf4fafb5e96 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v8 1/4] Creates a new logical replica from a standby server

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server. Remove
the additional replication slot that was used to get the consistent LSN.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_subscriber.sgml           |  284 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_subscriber.c         | 1657 +++++++++++++++++
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   44 +
 .../t/041_pg_subscriber_standby.pl            |  139 ++
 9 files changed, 2153 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_subscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_subscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_subscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..3862c976d7 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgSubscriber       SYSTEM "pg_subscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
new file mode 100644
index 0000000000..553185c35f
--- /dev/null
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -0,0 +1,284 @@
+<!--
+doc/src/sgml/ref/pg_subscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgsubscriber">
+ <indexterm zone="app-pgsubscriber">
+  <primary>pg_subscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_subscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>create a new logical replica from a standby server</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_subscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a standby server and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_subscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a standby
+        server.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--publisher-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_subscriber</application> to output progress messages
+        and detailed information about each step.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_subscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_subscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_subscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the target data
+     directory is used by a standby server. Stop the standby server if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_subscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  Another
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_subscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_subscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_subscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_subscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> removes the additional replication
+     slot that was used to get the consistent LSN on the source server.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a standby server at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..266f4e515a 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..0e5384a1d5 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_subscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..f6281b7676 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_subscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_subscriber: $(WIN32RES) pg_subscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_subscriber$(X) '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_subscriber$(X) pg_subscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..ccfd7bb7a5 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_subscriber_sources = files(
+  'pg_subscriber.c'
+)
+
+if host_system == 'windows'
+  pg_subscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_subscriber',
+	'--FILEDESC', 'pg_subscriber - create a new logical replica from a standby server',])
+endif
+
+pg_subscriber = executable('pg_subscriber',
+  pg_subscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_subscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_subscriber.pl',
+      't/041_pg_subscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
new file mode 100644
index 0000000000..e998c29f9e
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -0,0 +1,1657 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_subscriber/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static char *use_primary_slot_name(void);
+static bool create_all_logical_replication_slots(LogicalRepInfo *dbinfo);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static char temp_replslot[NAMEDATALEN] = {0};
+static bool made_transient_replslot = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_subscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+
+	if (made_transient_replslot)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+			disconnect_database(conn);
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-conninfo=CONNINFO   publisher connection string\n"));
+	printf(_(" -S, --subscriber-conninfo=CONNINFO  subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name plus a fallback application name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	if (i > 0)
+		appendPQExpBufferChar(buf, ' ');
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Return a palloc'd slot name if the replication is using one.
+ */
+static char *
+use_primary_slot_name(void)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+	char	   *slot_name;
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT setting FROM pg_settings WHERE name = 'primary_slot_name'");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain parameter information: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+
+	/*
+	 * If primary_slot_name is an empty string, the current replication
+	 * connection is not using a replication slot, bail out.
+	 */
+	if (strcmp(PQgetvalue(res, 0, 0), "") == 0)
+	{
+		PQclear(res);
+		return NULL;
+	}
+
+	slot_name = pg_strdup(PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_replication_slots r INNER JOIN pg_stat_activity a ON (r.active_pid = a.pid) WHERE slot_name = '%s'", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		return NULL;
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return slot_name;
+}
+
+static bool
+create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+{
+	int			i;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
+		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
+		 * to reduce the probability of collision. By default, subscription
+		 * name is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_subscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (transient_replslot)
+		made_transient_replslot = true;
+	else
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_subscriber must have created this
+		 * publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_subscriber_ prefix followed by the exact
+			 * database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-conninfo", required_argument, NULL, 'P'},
+		{"subscriber-conninfo", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+	int			rc;
+
+	char	   *pg_ctl_cmd;
+
+	char	   *base_dir;
+	char	   *server_start_log;
+
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	int			i;
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_subscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&database_names, optarg))
+				{
+					simple_string_list_append(&database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("directory path for subscriber is too long");
+		exit(1);
+	}
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+	{
+		pg_log_error("could not create directory \"%s\": %m", base_dir);
+		exit(1);
+	}
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * Stop the subscriber if it is a standby server. Before executing the
+	 * transformation steps, make sure the subscriber is not running because
+	 * one of the steps is to modify some recovery parameters that require a
+	 * restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Since the standby server is running, check if it is using an
+		 * existing replication slot for WAL retention purposes. This
+		 * replication slot has no use after the transformation, hence, it
+		 * will be removed at the end of this process.
+		 */
+		primary_slot_name = use_primary_slot_name();
+		if (primary_slot_name != NULL)
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+
+		pg_log_info("subscriber is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+	}
+
+	/*
+	 * Create a replication slot for each database on the publisher.
+	 */
+	if (!create_all_logical_replication_slots(dbinfo))
+		exit(1);
+
+	/*
+	 * Create a logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. We cannot use the last created replication slot
+	 * because the consistent LSN should be obtained *after* the base backup
+	 * finishes (and the base backup should include the logical replication
+	 * slots).
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 *
+	 * A temporary replication slot is not used here to avoid keeping a
+	 * replication connection open (depending when base backup was taken, the
+	 * connection should be open for a few hours).
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	server_start_log = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("log file path is too long");
+		exit(1);
+	}
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create a publication for each database. This step should be executed
+	 * after promoting the subscriber to avoid replicating unnecessary
+	 * objects.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+
+		/* Connect to publisher. */
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		create_publication(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Create a subscription for each database.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * The transient replication slot is no longer required. Drop it.
+	 *
+	 * If the physical replication slot exists, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops the replication slot later.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+	{
+		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		if (primary_slot_name != NULL)
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+	}
+	else
+	{
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		if (primary_slot_name != NULL)
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	/*
+	 * Remove log file generated by this tool, if it runs successfully.
+	 * Otherwise, file is kept that may provide useful debugging information.
+	 */
+	unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
new file mode 100644
index 0000000000..4ebff76b2d
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_subscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_subscriber');
+program_version_ok('pg_subscriber');
+program_options_handling_ok('pg_subscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_subscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+		'--subscriber-conninfo', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
new file mode 100644
index 0000000000..fbcd0fc82b
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_subscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_subscriber on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_subscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
-- 
2.43.0

v8-0002-Address-some-comments-proposed-on-hackers.patchapplication/octet-stream; name=v8-0002-Address-some-comments-proposed-on-hackers.patchDownload
From 327fa75f88f913b4731e73b066e1d30b9225ed44 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 22 Jan 2024 12:42:34 +0530
Subject: [PATCH v8 2/4] Address some comments proposed on -hackers

The patch has following changes:

* Some comments reported on the thread
* Add a timeout option for the recovery option
* Reject if the target server is not a standby
* Reject when the --subscriber-conninfo specifies non-local server
* Add -u and -p options
* Check wal_level and max_replication_slot parameters
---
 doc/src/sgml/ref/pg_subscriber.sgml           |  21 +-
 src/bin/pg_basebackup/pg_subscriber.c         | 911 +++++++++++-------
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   9 +-
 .../t/041_pg_subscriber_standby.pl            |   8 +-
 4 files changed, 601 insertions(+), 348 deletions(-)

diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
index 553185c35f..eaabfc7053 100644
--- a/doc/src/sgml/ref/pg_subscriber.sgml
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -16,12 +16,18 @@ PostgreSQL documentation
 
  <refnamediv>
   <refname>pg_subscriber</refname>
-  <refpurpose>create a new logical replica from a standby server</refpurpose>
+  <refpurpose>Convert a standby replica to a logical replica</refpurpose>
  </refnamediv>
 
  <refsynopsisdiv>
   <cmdsynopsis>
    <command>pg_subscriber</command>
+   <arg choice="plain"><option>-D</option></arg>
+   <arg choice="plain"><replaceable>datadir</replaceable></arg>
+   <arg choice="plain"><option>-P</option>
+   <replaceable>publisher-conninfo</replaceable></arg>
+   <arg choice="plain"><option>-S</option></arg>
+   <arg choice="plain"><replaceable>subscriber-conninfo</replaceable></arg>
    <arg rep="repeat"><replaceable>option</replaceable></arg>
   </cmdsynopsis>
  </refsynopsisdiv>
@@ -29,17 +35,18 @@ PostgreSQL documentation
  <refsect1>
   <title>Description</title>
   <para>
-   <application>pg_subscriber</application> takes the publisher and subscriber
-   connection strings, a cluster directory from a standby server and a list of
-   database names and it sets up a new logical replica using the physical
-   recovery process.
+   pg_subscriber creates a new <link
+   linkend="logical-replication-subscription">subscriber</link> from a physical
+   standby server. This allows users to quickly set up logical replication
+   system.
   </para>
 
   <para>
-   The <application>pg_subscriber</application> should be run at the target
+   The <application>pg_subscriber</application> has to be run at the target
    server. The source server (known as publisher server) should accept logical
    replication connections from the target server (known as subscriber server).
-   The target server should accept local logical replication connection.
+   The target server should accept logical replication connection from
+   localhost.
   </para>
  </refsect1>
 
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index e998c29f9e..3880d15ef9 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * pg_subscriber.c
- *	  Create a new logical replica from a standby server
+ *	  Convert a standby replica to a logical replica
  *
  * Copyright (C) 2024, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
- *		src/bin/pg_subscriber/pg_subscriber.c
+ *		src/bin/pg_basebackup/pg_subscriber.c
  *
  *-------------------------------------------------------------------------
  */
@@ -32,81 +32,122 @@
 
 #define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
 
-typedef struct LogicalRepInfo
+typedef struct LogicalRepPerdbInfo
 {
-	Oid			oid;			/* database OID */
-	char	   *dbname;			/* database name */
-	char	   *pubconninfo;	/* publication connection string for logical
-								 * replication */
-	char	   *subconninfo;	/* subscription connection string for logical
-								 * replication */
-	char	   *pubname;		/* publication name */
-	char	   *subname;		/* subscription name (also replication slot
-								 * name) */
-
-	bool		made_replslot;	/* replication slot was created */
-	bool		made_publication;	/* publication was created */
-	bool		made_subscription;	/* subscription was created */
-} LogicalRepInfo;
+	Oid		oid;
+	char   *dbname;
+	bool	made_replslot;	/* replication slot was created */
+	bool	made_publication;	/* publication was created */
+	bool	made_subscription;	/* subscription was created */
+} LogicalRepPerdbInfo;
+
+typedef struct
+{
+	LogicalRepPerdbInfo	   *perdb;			/* array of db infos */
+	int						ndbs;			/* number of db infos */
+} LogicalRepPerdbInfoArr;
+
+typedef struct PrimaryInfo
+{
+	char   *base_conninfo;
+	uint64	sysid;
+} PrimaryInfo;
+
+typedef struct StandbyInfo
+{
+	char   *base_conninfo;
+	char   *bindir;
+	char   *pgdata;
+	char   *primary_slot_name;
+	uint64	sysid;
+} StandbyInfo;
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname,
-							   const char *noderole);
-static bool get_exec_path(const char *path);
+static char *get_base_conninfo(char *conninfo, char *dbname);
+static bool get_exec_base_path(const char *path);
 static bool check_data_directory(const char *datadir);
+static void store_db_names(LogicalRepPerdbInfo **perdb, int ndbs);
+static void get_sysid_for_primary(PrimaryInfo *primary, char *dbname);
+static void get_control_for_standby(StandbyInfo *standby);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
-static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
-static PGconn *connect_database(const char *conninfo);
+static PGconn *connect_database(const char *base_conninfo, const char*dbname);
 static void disconnect_database(PGconn *conn);
-static uint64 get_sysid_from_conn(const char *conninfo);
-static uint64 get_control_from_datadir(const char *datadir);
-static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
-static char *use_primary_slot_name(void);
-static bool create_all_logical_replication_slots(LogicalRepInfo *dbinfo);
-static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-											 char *slot_name);
-static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *use_primary_slot_name(PrimaryInfo *primary, StandbyInfo *standby,
+								   LogicalRepPerdbInfo *perdb);
+static bool create_all_logical_replication_slots(PrimaryInfo *primary,
+												 LogicalRepPerdbInfoArr *dbarr);
+static char *create_logical_replication_slot(PGconn *conn, bool temporary,
+											 LogicalRepPerdbInfo *perdb);
+static void modify_sysid(const char *bindir, const char *datadir);
+static void drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+								  const char *slot_name);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
-static void wait_for_end_recovery(const char *conninfo);
-static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
-static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void wait_for_end_recovery(const char *base_conninfo,
+								  const char *dbname);
+static void create_publication(PGconn *conn, PrimaryInfo *primary,
+							   LogicalRepPerdbInfo *perdb);
+static void drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void create_subscription(PGconn *conn, StandbyInfo *standby,
+								char *base_conninfo,
+								LogicalRepPerdbInfo *perdb);
+static void drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void start_standby_server(StandbyInfo *standby, unsigned short subport,
+								 char *server_start_log);
+static char *construct_sub_conninfo(char *username, unsigned short subport);
 
 #define	USEC_PER_SEC	1000000
-#define	WAIT_INTERVAL	1		/* 1 second */
+#define DEFAULT_WAIT	60
+#define WAITS_PER_SEC	10              /* should divide USEC_PER_SEC evenly */
+#define DEF_PGSPORT		50111
 
 /* Options */
-static const char *progname;
-
-static char *subscriber_dir = NULL;
 static char *pub_conninfo_str = NULL;
-static char *sub_conninfo_str = NULL;
 static SimpleStringList database_names = {NULL, NULL};
-static char *primary_slot_name = NULL;
+static int	wait_seconds = DEFAULT_WAIT;
+static bool retain = false;
 static bool dry_run = false;
 
 static bool success = false;
+static const char *progname;
+static LogicalRepPerdbInfoArr dbarr;
+static PrimaryInfo primary;
+static StandbyInfo standby;
 
-static char *pg_ctl_path = NULL;
-static char *pg_resetwal_path = NULL;
+enum PGSWaitPMResult
+{
+	PGS_POSTMASTER_READY,
+	PGS_POSTMASTER_STANDBY,
+	PGS_POSTMASTER_STILL_STARTING,
+	PGS_POSTMASTER_FAILED
+};
 
-static LogicalRepInfo *dbinfo;
-static int	num_dbs = 0;
 
-static char temp_replslot[NAMEDATALEN] = {0};
-static bool made_transient_replslot = false;
+/*
+ * Build the replication slot and subscription name. The name must not exceed
+ * NAMEDATALEN - 1. This current schema uses a maximum of 36 characters
+ * (14 + 10 + 1 + 10 + '\0'). System identifier is included to reduce the
+ * probability of collision. By default, subscription name is used as
+ * replication slot name.
+ */
+static inline void
+get_subscription_name(Oid oid, int pid, char *subname, Size szsub)
+{
+	snprintf(subname, szsub, "pg_subscriber_%u_%d", oid, pid);
+}
 
-enum WaitPMResult
+/*
+ * Build the publication name. The name must not exceed NAMEDATALEN -
+ * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+ * '\0').
+ */
+static inline void
+get_publication_name(Oid oid, char *pubname, Size szpub)
 {
-	POSTMASTER_READY,
-	POSTMASTER_STANDBY,
-	POSTMASTER_STILL_STARTING,
-	POSTMASTER_FAILED
-};
+	snprintf(pubname, szpub, "pg_subscriber_%u", oid);
+}
 
 
 /*
@@ -125,41 +166,39 @@ cleanup_objects_atexit(void)
 	if (success)
 		return;
 
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
-		if (dbinfo[i].made_subscription)
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
+
+		if (perdb->made_subscription)
 		{
-			conn = connect_database(dbinfo[i].subconninfo);
+			conn = connect_database(standby.base_conninfo, perdb->dbname);
 			if (conn != NULL)
 			{
-				drop_subscription(conn, &dbinfo[i]);
+				drop_subscription(conn, perdb);
 				disconnect_database(conn);
 			}
 		}
 
-		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		if (perdb->made_publication || perdb->made_replslot)
 		{
-			conn = connect_database(dbinfo[i].pubconninfo);
+			conn = connect_database(primary.base_conninfo, perdb->dbname);
 			if (conn != NULL)
 			{
-				if (dbinfo[i].made_publication)
-					drop_publication(conn, &dbinfo[i]);
-				if (dbinfo[i].made_replslot)
-					drop_replication_slot(conn, &dbinfo[i], NULL);
+				if (perdb->made_publication)
+					drop_publication(conn, perdb);
+				if (perdb->made_replslot)
+				{
+					char replslotname[NAMEDATALEN];
+
+					get_subscription_name(perdb->oid, (int) getpid(),
+										  replslotname, NAMEDATALEN);
+					drop_replication_slot(conn, perdb, replslotname);
+				}
 				disconnect_database(conn);
 			}
 		}
 	}
-
-	if (made_transient_replslot)
-	{
-		conn = connect_database(dbinfo[0].pubconninfo);
-		if (conn != NULL)
-		{
-			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
-			disconnect_database(conn);
-		}
-	}
 }
 
 static void
@@ -184,17 +223,16 @@ usage(void)
 
 /*
  * Validate a connection string. Returns a base connection string that is a
- * connection string without a database name plus a fallback application name.
- * Since we might process multiple databases, each database name will be
- * appended to this base connection string to provide a final connection string.
- * If the second argument (dbname) is not null, returns dbname if the provided
- * connection string contains it. If option --database is not provided, uses
- * dbname as the only database to setup the logical replica.
- * It is the caller's responsibility to free the returned connection string and
- * dbname.
+ * connection string without a database name. Since we might process multiple
+ * databases, each database name will be appended to this base connection
+ * string to provide a final connection string. If the second argument (dbname)
+ * is not null, returns dbname if the provided connection string contains it.
+ * If option --database is not provided, uses dbname as the only database to
+ * setup the logical replica. It is the caller's responsibility to free the
+ * returned connection string and dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+get_base_conninfo(char *conninfo, char *dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -203,7 +241,7 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 	char	   *ret;
 	int			i;
 
-	pg_log_info("validating connection string on %s", noderole);
+	pg_log_info("validating connection string on publisher");
 
 	conn_opts = PQconninfoParse(conninfo, &errmsg);
 	if (conn_opts == NULL)
@@ -231,10 +269,6 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 		}
 	}
 
-	if (i > 0)
-		appendPQExpBufferChar(buf, ' ');
-	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
-
 	ret = pg_strdup(buf->data);
 
 	destroyPQExpBuffer(buf);
@@ -244,15 +278,16 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 }
 
 /*
- * Get the absolute path from other PostgreSQL binaries (pg_ctl and
- * pg_resetwal) that is used by it.
+ * Get the absolute binary path from another PostgreSQL binary (pg_ctl) and set
+ * to StandbyInfo.
  */
 static bool
-get_exec_path(const char *path)
+get_exec_base_path(const char *path)
 {
 	int			rc;
+	char		pg_ctl_path[MAXPGPATH];
+	char	   *p;
 
-	pg_ctl_path = pg_malloc(MAXPGPATH);
 	rc = find_other_exec(path, "pg_ctl",
 						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
 						 pg_ctl_path);
@@ -277,30 +312,12 @@ get_exec_path(const char *path)
 
 	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
 
-	pg_resetwal_path = pg_malloc(MAXPGPATH);
-	rc = find_other_exec(path, "pg_resetwal",
-						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
-						 pg_resetwal_path);
-	if (rc < 0)
-	{
-		char		full_path[MAXPGPATH];
-
-		if (find_my_exec(path, full_path) < 0)
-			strlcpy(full_path, progname, sizeof(full_path));
-		if (rc == -1)
-			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
-						 "same directory as \"%s\".\n"
-						 "Check your installation.",
-						 "pg_resetwal", progname, full_path);
-		else
-			pg_log_error("The program \"%s\" was found by \"%s\"\n"
-						 "but was not the same version as %s.\n"
-						 "Check your installation.",
-						 "pg_resetwal", full_path, progname);
-		return false;
-	}
+	/* Extract the directory part from the path */
+	p = strrchr(pg_ctl_path, 'p');
+	Assert(p);
 
-	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+	*p = '\0';
+	standby.bindir = pg_strdup(pg_ctl_path);
 
 	return true;
 }
@@ -364,49 +381,36 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
 }
 
 /*
- * Store publication and subscription information.
+ * Initialize per-db structure and store the name of databases
  */
-static LogicalRepInfo *
-store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+static void
+store_db_names(LogicalRepPerdbInfo **perdb, int ndbs)
 {
-	LogicalRepInfo *dbinfo;
 	SimpleStringListCell *cell;
 	int			i = 0;
 
-	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+	*perdb = (LogicalRepPerdbInfo *) pg_malloc0(sizeof(LogicalRepPerdbInfo) *
+											   ndbs);
 
 	for (cell = database_names.head; cell; cell = cell->next)
 	{
-		char	   *conninfo;
-
-		/* Publisher. */
-		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
-		dbinfo[i].pubconninfo = conninfo;
-		dbinfo[i].dbname = cell->val;
-		dbinfo[i].made_replslot = false;
-		dbinfo[i].made_publication = false;
-		dbinfo[i].made_subscription = false;
-		/* other struct fields will be filled later. */
-
-		/* Subscriber. */
-		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
-		dbinfo[i].subconninfo = conninfo;
-
+		(*perdb)[i].dbname = pg_strdup(cell->val);
 		i++;
 	}
-
-	return dbinfo;
 }
 
 static PGconn *
-connect_database(const char *conninfo)
+connect_database(const char *base_conninfo, const char*dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	const char *rconninfo;
+
+	char	   *rconninfo;
+	char	   *concat_conninfo = concat_conninfo_dbname(base_conninfo,
+														 dbname);
 
 	/* logical replication mode */
-	rconninfo = psprintf("%s replication=database", conninfo);
+	rconninfo = psprintf("%s replication=database", concat_conninfo);
 
 	conn = PQconnectdb(rconninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
@@ -424,6 +428,9 @@ connect_database(const char *conninfo)
 	}
 	PQclear(res);
 
+	pfree(rconninfo);
+	pfree(concat_conninfo);
+
 	return conn;
 }
 
@@ -436,19 +443,18 @@ disconnect_database(PGconn *conn)
 }
 
 /*
- * Obtain the system identifier using the provided connection. It will be used
- * to compare if a data directory is a clone of another one.
+ * Obtain the system identifier from the primary server. It will be used to
+ * compare if a data directory is a clone of another one.
  */
-static uint64
-get_sysid_from_conn(const char *conninfo)
+static void
+get_sysid_for_primary(PrimaryInfo *primary, char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from publisher");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(primary->base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -471,43 +477,39 @@ get_sysid_from_conn(const char *conninfo)
 		exit(1);
 	}
 
-	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+	primary->sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
 
-	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) primary->sysid);
 
 	disconnect_database(conn);
-
-	return sysid;
 }
 
 /*
- * Obtain the system identifier from control file. It will be used to compare
- * if a data directory is a clone of another one. This routine is used locally
- * and avoids a replication connection.
+ * Obtain the system identifier from a standby server. It will be used to
+ * compare if a data directory is a clone of another one. This routine is used
+ * locally and avoids a replication connection.
  */
-static uint64
-get_control_from_datadir(const char *datadir)
+static void
+get_control_for_standby(StandbyInfo *standby)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from subscriber");
 
-	cf = get_controlfile(datadir, &crc_ok);
+	cf = get_controlfile(standby->pgdata, &crc_ok);
 	if (!crc_ok)
 	{
 		pg_log_error("control file appears to be corrupt");
 		exit(1);
 	}
 
-	sysid = cf->system_identifier;
+	standby->sysid = cf->system_identifier;
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) standby->sysid);
 
 	pfree(cf);
-
-	return sysid;
 }
 
 /*
@@ -516,7 +518,7 @@ get_control_from_datadir(const char *datadir)
  * files from one of the systems might be used in the other one.
  */
 static void
-modify_sysid(const char *pg_resetwal_path, const char *datadir)
+modify_sysid(const char *bindir, const char *datadir)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
@@ -551,7 +553,7 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
 
 	pg_log_info("running pg_resetwal on the subscriber");
 
-	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\"", bindir, datadir);
 
 	pg_log_debug("command is: %s", cmd_str);
 
@@ -571,14 +573,15 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
  * Return a palloc'd slot name if the replication is using one.
  */
 static char *
-use_primary_slot_name(void)
+use_primary_slot_name(PrimaryInfo *primary, StandbyInfo *standby,
+					  LogicalRepPerdbInfo *perdb)
 {
 	PGconn	   *conn;
 	PGresult   *res;
 	PQExpBuffer str = createPQExpBuffer();
 	char	   *slot_name;
 
-	conn = connect_database(dbinfo[0].subconninfo);
+	conn = connect_database(standby->base_conninfo, perdb->dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -604,7 +607,7 @@ use_primary_slot_name(void)
 
 	disconnect_database(conn);
 
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary->base_conninfo, perdb->dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -634,17 +637,19 @@ use_primary_slot_name(void)
 }
 
 static bool
-create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
+create_all_logical_replication_slots(PrimaryInfo *primary,
+									 LogicalRepPerdbInfoArr *dbarr)
 {
 	int			i;
 
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr->ndbs; i++)
 	{
 		PGconn	   *conn;
 		PGresult   *res;
 		char		replslotname[NAMEDATALEN];
+		LogicalRepPerdbInfo *perdb = &dbarr->perdb[i];
 
-		conn = connect_database(dbinfo[i].pubconninfo);
+		conn = connect_database(primary->base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
@@ -664,27 +669,14 @@ create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
 		}
 
 		/* Remember database OID. */
-		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		perdb->oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
 
 		PQclear(res);
 
-		/*
-		 * Build the replication slot name. The name must not exceed
-		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
-		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
-		 * to reduce the probability of collision. By default, subscription
-		 * name is used as replication slot name.
-		 */
-		snprintf(replslotname, sizeof(replslotname),
-				 "pg_subscriber_%u_%d",
-				 dbinfo[i].oid,
-				 (int) getpid());
-		dbinfo[i].subname = pg_strdup(replslotname);
+		get_subscription_name(perdb->oid, (int) getpid(), replslotname, NAMEDATALEN);
 
 		/* Create replication slot on publisher. */
-		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
-			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
-		else
+		if (create_logical_replication_slot(conn, false, perdb) == NULL && !dry_run)
 			return false;
 
 		disconnect_database(conn);
@@ -701,30 +693,36 @@ create_all_logical_replication_slots(LogicalRepInfo *dbinfo)
  * result set that contains the consistent LSN.
  */
 static char *
-create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-								char *slot_name)
+create_logical_replication_slot(PGconn *conn, bool temporary,
+								LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res = NULL;
 	char	   *lsn = NULL;
-	bool		transient_replslot = false;
+	char		slot_name[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
 	/*
-	 * If no slot name is informed, it is a transient replication slot used
-	 * only for catch up purposes.
+	 * Construct a name of logical replication slot. The formatting is
+	 * different depends on its persistency.
+	 *
+	 * For persistent slots: the name must be same as the subscription.
+	 * For temporary slots: OID is not needed, but another string is added.
 	 */
-	if (slot_name[0] == '\0')
-	{
+	if (!temporary)
+		get_subscription_name(perdb->oid, (int) getpid(), slot_name, NAMEDATALEN);
+	else
 		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
 				 (int) getpid());
-		transient_replslot = true;
-	}
 
-	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+
+	if(temporary)
+		appendPQExpBufferStr(str, " TEMPORARY");
+
 	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
 
 	pg_log_debug("command is: %s", str->data);
@@ -734,17 +732,14 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						 PQresultErrorMessage(res));
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, perdb->dbname, PQresultErrorMessage(res));
 			return lsn;
 		}
 	}
 
 	/* for cleanup purposes */
-	if (transient_replslot)
-		made_transient_replslot = true;
-	else
-		dbinfo->made_replslot = true;
+	perdb->made_replslot = true;
 
 	if (!dry_run)
 	{
@@ -758,14 +753,15 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 }
 
 static void
-drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+					  const char *slot_name)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
 
@@ -775,7 +771,7 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, perdb->dbname,
 						 PQerrorMessage(conn));
 
 		PQclear(res);
@@ -825,19 +821,22 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
  * Returns after the server finishes the recovery process.
  */
 static void
-wait_for_end_recovery(const char *conninfo)
+wait_for_end_recovery(const char *base_conninfo, const char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	int			status = POSTMASTER_STILL_STARTING;
+	int			status = PGS_POSTMASTER_STILL_STARTING;
+	int			cnt;
+	int			rc;
+	char	   *pg_ctl_cmd;
 
 	pg_log_info("waiting the postmaster to reach the consistent state");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
-	for (;;)
+	for (cnt = 0; cnt < wait_seconds * WAITS_PER_SEC; cnt++)
 	{
 		bool		in_recovery;
 
@@ -865,17 +864,32 @@ wait_for_end_recovery(const char *conninfo)
 		 */
 		if (!in_recovery || dry_run)
 		{
-			status = POSTMASTER_READY;
+			status = PGS_POSTMASTER_READY;
 			break;
 		}
 
 		/* Keep waiting. */
-		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+		pg_usleep(USEC_PER_SEC / WAITS_PER_SEC);
 	}
 
 	disconnect_database(conn);
 
-	if (status == POSTMASTER_STILL_STARTING)
+	/*
+	 * If timeout is reached exit the pg_subscriber and stop the standby node.
+	 */
+	if (cnt >= wait_seconds * WAITS_PER_SEC)
+	{
+		pg_log_error("recovery timed out");
+
+		pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
+							  standby.bindir, standby.pgdata);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+		exit(1);
+	}
+
+	if (status == PGS_POSTMASTER_STILL_STARTING)
 	{
 		pg_log_error("server did not end recovery");
 		exit(1);
@@ -888,17 +902,21 @@ wait_for_end_recovery(const char *conninfo)
  * Create a publication that includes all tables in the database.
  */
 static void
-create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+create_publication(PGconn *conn, PrimaryInfo *primary,
+				   LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
 	/* Check if the publication needs to be created. */
 	appendPQExpBuffer(str,
 					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
-					  dbinfo->pubname);
+					  pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
@@ -918,7 +936,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		 */
 		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
 		{
-			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			pg_log_info("publication \"%s\" already exists", pubname);
 			return;
 		}
 		else
@@ -931,7 +949,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 			 * database oid in which puballtables is false.
 			 */
 			pg_log_error("publication \"%s\" does not replicate changes for all tables",
-						 dbinfo->pubname);
+						 pubname);
 			pg_log_error_hint("Consider renaming this publication.");
 			PQclear(res);
 			PQfinish(conn);
@@ -942,9 +960,9 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	PQclear(res);
 	resetPQExpBuffer(str);
 
-	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("creating publication \"%s\" on database \"%s\"", pubname, perdb->dbname);
 
-	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -954,14 +972,14 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
-						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+						 pubname, perdb->dbname, PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
 		}
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_publication = true;
+	perdb->made_publication = true;
 
 	if (!dry_run)
 		PQclear(res);
@@ -973,16 +991,19 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove publication if it couldn't finish all steps.
  */
 static void
-drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
 
-	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", pubname, perdb->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -990,7 +1011,7 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", pubname, perdb->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1011,19 +1032,27 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * initial location.
  */
 static void
-create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+create_subscription(PGconn *conn, StandbyInfo *standby, char *base_conninfo,
+					LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
 					  "WITH (create_slot = false, copy_data = false, enabled = false)",
-					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+					  subname, concat_conninfo_dbname(base_conninfo, perdb->dbname), pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1033,14 +1062,14 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+						 subname, perdb->dbname, PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
 		}
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_subscription = true;
+	perdb->made_subscription = true;
 
 	if (!dry_run)
 		PQclear(res);
@@ -1052,16 +1081,19 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove subscription if it couldn't finish all steps.
  */
 static void
-drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", subname, perdb->dbname);
 
-	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1069,7 +1101,7 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", subname, perdb->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1088,18 +1120,21 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * printing purposes.
  */
 static void
-set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb, const char *lsn)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 	Oid			suboid;
 	char		originname[NAMEDATALEN];
 	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
 	appendPQExpBuffer(str,
-					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", subname);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1140,7 +1175,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	PQclear(res);
 
 	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
-				originname, lsnstr, dbinfo->dbname);
+				originname, lsnstr, perdb->dbname);
 
 	resetPQExpBuffer(str);
 	appendPQExpBuffer(str,
@@ -1154,7 +1189,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
 			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
-						 dbinfo->subname, PQresultErrorMessage(res));
+						 subname, PQresultErrorMessage(res));
 			PQfinish(conn);
 			exit(1);
 		}
@@ -1173,16 +1208,20 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
  * of this setup.
  */
 static void
-enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
 
-	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1191,7 +1230,7 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
-			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+			pg_log_error("could not enable subscription \"%s\": %s", subname,
 						 PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
@@ -1203,6 +1242,61 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	destroyPQExpBuffer(str);
 }
 
+static void
+start_standby_server(StandbyInfo *standby, unsigned short subport,
+					 char *server_start_log)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	int			rc;
+	char	   *pg_ctl_cmd;
+
+	if (server_start_log[0] == '\0')
+	{
+		/* append timestamp with ISO 8601 format. */
+		gettimeofday(&time, NULL);
+		tt = (time_t) time.tv_sec;
+		strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+		snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+				 ".%03d", (int) (time.tv_usec / 1000));
+
+		len = snprintf(server_start_log, MAXPGPATH,
+					   "%s/%s/server_start_%s.log", standby->pgdata,
+					   PGS_OUTPUT_DIR, timebuf);
+		if (len >= MAXPGPATH)
+		{
+			pg_log_error("log file path is too long");
+			exit(1);
+		}
+	}
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -o \"-p %d\" -l \"%s\"",
+						  standby->bindir,
+						  standby->pgdata, subport, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static char *
+construct_sub_conninfo(char *username, unsigned short subport)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	if (username)
+		appendPQExpBuffer(buf, "user=%s ", username);
+
+	appendPQExpBuffer(buf, "port=%d fallback_application_name=%s",
+					  subport, progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
 int
 main(int argc, char **argv)
 {
@@ -1214,6 +1308,10 @@ main(int argc, char **argv)
 		{"publisher-conninfo", required_argument, NULL, 'P'},
 		{"subscriber-conninfo", required_argument, NULL, 'S'},
 		{"database", required_argument, NULL, 'd'},
+		{"timeout", required_argument, NULL, 't'},
+		{"username", required_argument, NULL, 'u'},
+		{"port", required_argument, NULL, 'p'},
+		{"retain", no_argument, NULL, 'r'},
 		{"dry-run", no_argument, NULL, 'n'},
 		{"verbose", no_argument, NULL, 'v'},
 		{NULL, 0, NULL, 0}
@@ -1225,20 +1323,15 @@ main(int argc, char **argv)
 
 	char	   *pg_ctl_cmd;
 
-	char	   *base_dir;
-	char	   *server_start_log;
-
-	char		timebuf[128];
-	struct timeval time;
-	time_t		tt;
+	char		base_dir[MAXPGPATH];
+	char		server_start_log[MAXPGPATH] = {0};
 	int			len;
 
-	char	   *pub_base_conninfo = NULL;
-	char	   *sub_base_conninfo = NULL;
 	char	   *dbname_conninfo = NULL;
 
-	uint64		pub_sysid;
-	uint64		sub_sysid;
+	unsigned short subport = DEF_PGSPORT;
+	char	   *username = NULL;
+
 	struct stat statbuf;
 
 	PGconn	   *conn;
@@ -1250,6 +1343,13 @@ main(int argc, char **argv)
 
 	int			i;
 
+	PGresult   *res;
+
+	char	   *wal_level;
+	int			max_replication_slots;
+	int			nslots_old;
+	int			nslots_new;
+
 	pg_logging_init(argv[0]);
 	pg_logging_set_level(PG_LOG_WARNING);
 	progname = get_progname(argv[0]);
@@ -1286,28 +1386,40 @@ main(int argc, char **argv)
 	}
 #endif
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+	while ((c = getopt_long(argc, argv, "D:P:S:d:t:u:p:rnv",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
 		{
 			case 'D':
-				subscriber_dir = pg_strdup(optarg);
+				standby.pgdata = pg_strdup(optarg);
+				canonicalize_path(standby.pgdata);
 				break;
 			case 'P':
 				pub_conninfo_str = pg_strdup(optarg);
 				break;
-			case 'S':
-				sub_conninfo_str = pg_strdup(optarg);
-				break;
 			case 'd':
 				/* Ignore duplicated database names. */
 				if (!simple_string_list_member(&database_names, optarg))
 				{
 					simple_string_list_append(&database_names, optarg);
-					num_dbs++;
+					dbarr.ndbs++;
 				}
 				break;
+			case 't':
+				wait_seconds = atoi(optarg);
+				break;
+			case 'u':
+				pfree(username);
+				username = pg_strdup(optarg);
+				break;
+			case 'p':
+				if ((subport = atoi(optarg)) <= 0)
+					pg_fatal("invalid old port number");
+				break;
+			case 'r':
+				retain = true;
+				break;
 			case 'n':
 				dry_run = true;
 				break;
@@ -1335,7 +1447,7 @@ main(int argc, char **argv)
 	/*
 	 * Required arguments
 	 */
-	if (subscriber_dir == NULL)
+	if (standby.pgdata == NULL)
 	{
 		pg_log_error("no subscriber data directory specified");
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -1358,21 +1470,14 @@ main(int argc, char **argv)
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 		exit(1);
 	}
-	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
-										  "publisher");
-	if (pub_base_conninfo == NULL)
-		exit(1);
 
-	if (sub_conninfo_str == NULL)
-	{
-		pg_log_error("no subscriber connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
-	if (sub_base_conninfo == NULL)
+	primary.base_conninfo = get_base_conninfo(pub_conninfo_str,
+											  dbname_conninfo);
+	if (primary.base_conninfo == NULL)
 		exit(1);
 
+	standby.base_conninfo = construct_sub_conninfo(username, subport);
+
 	if (database_names.head == NULL)
 	{
 		pg_log_info("no database was specified");
@@ -1385,7 +1490,7 @@ main(int argc, char **argv)
 		if (dbname_conninfo)
 		{
 			simple_string_list_append(&database_names, dbname_conninfo);
-			num_dbs++;
+			dbarr.ndbs++;
 
 			pg_log_info("database \"%s\" was extracted from the publisher connection string",
 						dbname_conninfo);
@@ -1399,25 +1504,25 @@ main(int argc, char **argv)
 	}
 
 	/*
-	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 * Get the absolute path of binaries on the subscriber.
 	 */
-	if (!get_exec_path(argv[0]))
+	if (!get_exec_base_path(argv[0]))
 		exit(1);
 
 	/* rudimentary check for a data directory. */
-	if (!check_data_directory(subscriber_dir))
+	if (!check_data_directory(standby.pgdata))
 		exit(1);
 
-	/* Store database information for publisher and subscriber. */
-	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+	/* Store database information to dbarr */
+	store_db_names(&dbarr.perdb, dbarr.ndbs);
 
 	/*
 	 * Check if the subscriber data directory has the same system identifier
 	 * than the publisher data directory.
 	 */
-	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
-	sub_sysid = get_control_from_datadir(subscriber_dir);
-	if (pub_sysid != sub_sysid)
+	get_sysid_for_primary(&primary, dbarr.perdb[0].dbname);
+	get_control_for_standby(&standby);
+	if (primary.sysid != standby.sysid)
 	{
 		pg_log_error("subscriber data directory is not a copy of the source database cluster");
 		exit(1);
@@ -1426,8 +1531,8 @@ main(int argc, char **argv)
 	/*
 	 * Create the output directory to store any data generated by this tool.
 	 */
-	base_dir = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s",
+				   standby.pgdata, PGS_OUTPUT_DIR);
 	if (len >= MAXPGPATH)
 	{
 		pg_log_error("directory path for subscriber is too long");
@@ -1441,7 +1546,153 @@ main(int argc, char **argv)
 	}
 
 	/* subscriber PID file. */
-	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid",
+			  standby.pgdata);
+
+	/* Start the standby server anyway */
+	start_standby_server(&standby, subport, server_start_log);
+
+	/*
+	 * Check wal_level in publisher and the max_replication_slots of publisher
+	 */
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT count(*) from pg_replication_slots;");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain number of replication slots");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not determine parameter settings on publisher");
+		exit(1);
+	}
+
+	nslots_old = atoi(PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	res = PQexec(conn, "SELECT setting FROM pg_settings "
+				 "WHERE name IN ('wal_level', 'max_replication_slots') "
+				 "ORDER BY name DESC;");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain guc parameters on publisher");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 2)
+	{
+		pg_log_error("could not determine parameter settings on publisher");
+		exit(1);
+	}
+
+	wal_level = PQgetvalue(res, 0, 0);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("wal_level must be \"logical\", but is set to \"%s\"", wal_level);
+		exit(1);
+	}
+
+	max_replication_slots = atoi(PQgetvalue(res, 1, 0));
+	nslots_new = nslots_old + dbarr.ndbs + 1;
+
+	if (nslots_new > max_replication_slots)
+	{
+		pg_log_error("max_replication_slots (%d) must be greater than or equal to "
+					 "the number of replication slots required (%d)", max_replication_slots, nslots_new);
+		exit(1);
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	conn = connect_database(standby.base_conninfo, dbarr.perdb[0].dbname);
+	if (conn == NULL)
+		exit(1);
+
+	/*
+	 * Check the max_replication_slots in subscriber
+	 */
+	res = PQexec(conn, "SELECT count(*) from pg_replication_slots;");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain number of replication slots on subscriber");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not determine parameter settings on subscriber");
+		exit(1);
+	}
+
+	nslots_old = atoi(PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	res = PQexec(conn, "SELECT setting FROM pg_settings "
+				 "WHERE name = 'max_replication_slots';");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain guc parameters");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not determine parameter settings on publisher");
+		exit(1);
+	}
+
+	max_replication_slots = atoi(PQgetvalue(res, 0, 0));
+	nslots_new = nslots_old + dbarr.ndbs;
+
+	if (nslots_new > max_replication_slots)
+	{
+		pg_log_error("max_replication_slots (%d) must be greater than or equal to "
+					 "the number of replication slots required (%d)", max_replication_slots, nslots_new);
+		exit(1);
+	}
+
+	PQclear(res);
+
+	/*
+	 * Exit the pg_subscriber if the node is not a standby server.
+	 */
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("unexpected result from pg_is_in_recovery function");
+		exit(1);
+	}
+
+	/* Check if the server is in recovery */
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("pg_subscriber is supported only on standby server");
+		exit(1);
+	}
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", standby.pgdata);
 
 	/*
 	 * Stop the subscriber if it is a standby server. Before executing the
@@ -1457,14 +1708,18 @@ main(int argc, char **argv)
 		 * replication slot has no use after the transformation, hence, it
 		 * will be removed at the end of this process.
 		 */
-		primary_slot_name = use_primary_slot_name();
-		if (primary_slot_name != NULL)
-			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		standby.primary_slot_name = use_primary_slot_name(&primary,
+														   &standby,
+														   &dbarr.perdb[0]);
+		if (standby.primary_slot_name != NULL)
+			pg_log_info("primary has replication slot \"%s\"",
+						standby.primary_slot_name);
 
 		pg_log_info("subscriber is up and running");
 		pg_log_info("stopping the server to start the transformation steps");
 
-		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
+							  standby.bindir, standby.pgdata);
 		rc = system(pg_ctl_cmd);
 		pg_ctl_status(pg_ctl_cmd, rc, 0);
 	}
@@ -1472,7 +1727,7 @@ main(int argc, char **argv)
 	/*
 	 * Create a replication slot for each database on the publisher.
 	 */
-	if (!create_all_logical_replication_slots(dbinfo))
+	if (!create_all_logical_replication_slots(&primary, &dbarr))
 		exit(1);
 
 	/*
@@ -1492,11 +1747,11 @@ main(int argc, char **argv)
 	 * replication connection open (depending when base backup was taken, the
 	 * connection should be open for a few hours).
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
-	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
-													 temp_replslot);
+	consistent_lsn = create_logical_replication_slot(conn, true,
+													 &dbarr.perdb[0]);
 
 	/*
 	 * Write recovery parameters.
@@ -1522,7 +1777,7 @@ main(int argc, char **argv)
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  consistent_lsn);
-		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+		WriteRecoveryConfig(conn, standby.pgdata, recoveryconfcontents);
 	}
 	disconnect_database(conn);
 
@@ -1532,54 +1787,29 @@ main(int argc, char **argv)
 	 * Start subscriber and wait until accepting connections.
 	 */
 	pg_log_info("starting the subscriber");
-
-	/* append timestamp with ISO 8601 format. */
-	gettimeofday(&time, NULL);
-	tt = (time_t) time.tv_sec;
-	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
-	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
-			 ".%03d", (int) (time.tv_usec / 1000));
-
-	server_start_log = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
-	if (len >= MAXPGPATH)
-	{
-		pg_log_error("log file path is too long");
-		exit(1);
-	}
-
-	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
-	rc = system(pg_ctl_cmd);
-	pg_ctl_status(pg_ctl_cmd, rc, 1);
+	start_standby_server(&standby, subport, server_start_log);
 
 	/*
 	 * Waiting the subscriber to be promoted.
 	 */
-	wait_for_end_recovery(dbinfo[0].subconninfo);
+	wait_for_end_recovery(standby.base_conninfo, dbarr.perdb[0].dbname);
 
 	/*
 	 * Create a publication for each database. This step should be executed
 	 * after promoting the subscriber to avoid replicating unnecessary
 	 * objects.
 	 */
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
-		char		pubname[NAMEDATALEN];
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
 
 		/* Connect to publisher. */
-		conn = connect_database(dbinfo[i].pubconninfo);
+		conn = connect_database(primary.base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
-		/*
-		 * Build the publication name. The name must not exceed NAMEDATALEN -
-		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
-		 * '\0').
-		 */
-		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
-		dbinfo[i].pubname = pg_strdup(pubname);
-
-		create_publication(conn, &dbinfo[i]);
+		/* Also create a publication */
+		create_publication(conn, &primary, perdb);
 
 		disconnect_database(conn);
 	}
@@ -1587,20 +1817,25 @@ main(int argc, char **argv)
 	/*
 	 * Create a subscription for each database.
 	 */
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
+
 		/* Connect to subscriber. */
-		conn = connect_database(dbinfo[i].subconninfo);
+		conn = connect_database(standby.base_conninfo, perdb->dbname);
+
 		if (conn == NULL)
 			exit(1);
 
-		create_subscription(conn, &dbinfo[i]);
+		create_subscription(conn, &standby, primary.base_conninfo, perdb);
 
 		/* Set the replication progress to the correct LSN. */
-		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+		set_replication_progress(conn, perdb, consistent_lsn);
 
 		/* Enable subscription. */
-		enable_subscription(conn, &dbinfo[i]);
+		enable_subscription(conn, perdb);
+
+		drop_publication(conn, perdb);
 
 		disconnect_database(conn);
 	}
@@ -1613,19 +1848,21 @@ main(int argc, char **argv)
 	 * XXX we might not fail here. Instead, we provide a warning so the user
 	 * eventually drops the replication slot later.
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
 	if (conn == NULL)
 	{
-		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
-		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		char *primary_slot_name = standby.primary_slot_name;
+
 		if (primary_slot_name != NULL)
 			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
 	}
 	else
 	{
-		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[0];
+		char *primary_slot_name = standby.primary_slot_name;
+
 		if (primary_slot_name != NULL)
-			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+			drop_replication_slot(conn, perdb, primary_slot_name);
 		disconnect_database(conn);
 	}
 
@@ -1634,20 +1871,22 @@ main(int argc, char **argv)
 	 */
 	pg_log_info("stopping the subscriber");
 
-	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
+						  standby.bindir, standby.pgdata);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 0);
 
 	/*
 	 * Change system identifier.
 	 */
-	modify_sysid(pg_resetwal_path, subscriber_dir);
+	modify_sysid(standby.bindir, standby.pgdata);
 
 	/*
 	 * Remove log file generated by this tool, if it runs successfully.
 	 * Otherwise, file is kept that may provide useful debugging information.
 	 */
-	unlink(server_start_log);
+	if (!retain)
+		unlink(server_start_log);
 
 	success = true;
 
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
index 4ebff76b2d..9915b8cb3c 100644
--- a/src/bin/pg_basebackup/t/040_pg_subscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -37,8 +37,13 @@ command_fails(
 		'--verbose',
 		'--pgdata', $datadir,
 		'--publisher-conninfo', 'dbname=postgres',
-		'--subscriber-conninfo', 'dbname=postgres'
 	],
 	'no database name specified');
-
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+	],
+	'subscriber connection string specnfied non-local server');
 done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
index fbcd0fc82b..4e26607611 100644
--- a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -51,25 +51,27 @@ $node_s->start;
 $node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
 $node_p->wait_for_replay_catchup($node_s);
 
+$node_f->stop;
+
 # Run pg_subscriber on about-to-fail node F
 command_fails(
 	[
 		'pg_subscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
 		'--publisher-conninfo', $node_p->connstr('pg1'),
-		'--subscriber-conninfo', $node_f->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
 	'subscriber data directory is not a copy of the source database cluster');
 
+$node_s->stop;
+
 # dry run mode on node S
 command_ok(
 	[
 		'pg_subscriber', '--verbose', '--dry-run',
 		'--pgdata', $node_s->data_dir,
 		'--publisher-conninfo', $node_p->connstr('pg1'),
-		'--subscriber-conninfo', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
@@ -82,6 +84,7 @@ $node_s->start;
 # Check if node S is still a standby
 is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
 	't', 'standby is in recovery');
+$node_s->stop;
 
 # Run pg_subscriber on node S
 command_ok(
@@ -89,7 +92,6 @@ command_ok(
 		'pg_subscriber', '--verbose',
 		'--pgdata', $node_s->data_dir,
 		'--publisher-conninfo', $node_p->connstr('pg1'),
-		'--subscriber-conninfo', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
-- 
2.43.0

v8-0003-Fix-publication-does-not-exist-error.patchapplication/octet-stream; name=v8-0003-Fix-publication-does-not-exist-error.patchDownload
From 9f538ed90a02fb13404ff0df4884d1e809a13f1c Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 22 Jan 2024 12:36:20 +0530
Subject: [PATCH v8 3/4] Fix publication does not exist error.

Fix publication does not exist error.
---
 src/bin/pg_basebackup/pg_subscriber.c | 23 +++--------------------
 1 file changed, 3 insertions(+), 20 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index 3880d15ef9..355738c20c 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -679,6 +679,9 @@ create_all_logical_replication_slots(PrimaryInfo *primary,
 		if (create_logical_replication_slot(conn, false, perdb) == NULL && !dry_run)
 			return false;
 
+		/* Also create a publication */
+		create_publication(conn, primary, perdb);
+
 		disconnect_database(conn);
 	}
 
@@ -1794,26 +1797,6 @@ main(int argc, char **argv)
 	 */
 	wait_for_end_recovery(standby.base_conninfo, dbarr.perdb[0].dbname);
 
-	/*
-	 * Create a publication for each database. This step should be executed
-	 * after promoting the subscriber to avoid replicating unnecessary
-	 * objects.
-	 */
-	for (i = 0; i < dbarr.ndbs; i++)
-	{
-		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
-
-		/* Connect to publisher. */
-		conn = connect_database(primary.base_conninfo, perdb->dbname);
-		if (conn == NULL)
-			exit(1);
-
-		/* Also create a publication */
-		create_publication(conn, &primary, perdb);
-
-		disconnect_database(conn);
-	}
-
 	/*
 	 * Create a subscription for each database.
 	 */
-- 
2.43.0

v8-0004-Move-a-registration-of-atexit-callback-to-behind.patchapplication/octet-stream; name=v8-0004-Move-a-registration-of-atexit-callback-to-behind.patchDownload
From eb912015aa8b5d8481b852d91e3d146fc1a31703 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 24 Jan 2024 07:18:47 +0000
Subject: [PATCH v8 4/4] Move a registration of atexit() callback to behind

---
 src/bin/pg_basebackup/pg_subscriber.c | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index 355738c20c..17a6b552af 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -1373,8 +1373,6 @@ main(int argc, char **argv)
 		}
 	}
 
-	atexit(cleanup_objects_atexit);
-
 	/*
 	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
 	 * it either.
@@ -1727,6 +1725,12 @@ main(int argc, char **argv)
 		pg_ctl_status(pg_ctl_cmd, rc, 0);
 	}
 
+	/*
+	 * Subsequent operations define some database objects on both primary and
+	 * standby. The callback is useful to clean up them in case of failure.
+	 */
+	atexit(cleanup_objects_atexit);
+
 	/*
 	 * Create a replication slot for each database on the publisher.
 	 */
-- 
2.43.0

#86Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Shubham Khanna (#74)
3 attachment(s)
RE: speed up a logical replica setup

Dear hackers,

Based on the requirement, I have profiled the performance test. It showed bottlenecks
are in small-data case are mainly two - starting a server and waiting until the
recovery is done.

# Tested source code

V7 patch set was applied atop HEAD(0eb23285). No configure options were specified
when it was built.

# Tested workload

I focused on only 100MB/1GB cases because bigger ones have already had good performance.
(Number of inserted tuples were same as previous tests)
I used bash script instead of tap test framework. See attached. Executed SQLs and
operations were almost the same.

As you can see, I tested only one-db case. Results may be changed if the number
of databases were changed.

# Measurement
Some debug logs which output current time were added (please see diff file).
I picked up some events and done at before/after them. Below bullets showed the measured ones:

* Starting a server
* Stopping a server
* Creating replication slots
* Creating publications
* Waiting until the recovery ended
* Creating subscriptions

# Result
Below table shows the elapsed time for these events. Raw data is also available
by the attached excel file.

|Event category |100MB case [sec]|1GB [sec]|
|Starting a server |1.414 |1.417 |
|Stoping a server |0.506 |0.506 |
|Creating replication slots |0.005 |0.007 |
|Creating publications |0.001 |0.002 |
|Waiting until the recovery ended|1.603 |14.529 |
|Creating subscriptions |0.012 |0.012 |
|Total |3.541 |16.473 |
|actual time |4.37 |17.271 |

As you can see, starting servers and waiting seem slow. We cannot omit these,
but setting smaller shared_buffers will reduce the start time. One approach is
to overwrite the GUC to smaller value, but I think we cannot determine the
appropriate value.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

Attachments:

run.shapplication/octet-stream; name=run.shDownload
perf_result.xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet; name=perf_result.xlsxDownload
add_debug_log.diffapplication/octet-stream; name=add_debug_log.diffDownload
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index 355738c..290be7c 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -675,13 +675,66 @@ create_all_logical_replication_slots(PrimaryInfo *primary,
 
 		get_subscription_name(perdb->oid, (int) getpid(), replslotname, NAMEDATALEN);
 
+		{
+			char        timebuf[128];
+			struct timeval time;
+			time_t      tt;
+
+			gettimeofday(&time, NULL);
+			tt = (time_t) time.tv_sec;
+			strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+			snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+					 ".%03d", (int) (time.tv_usec / 1000));
+			pg_log_warning("creating a replication slot %s start: %s", replslotname, timebuf);
+		}
+
 		/* Create replication slot on publisher. */
 		if (create_logical_replication_slot(conn, false, perdb) == NULL && !dry_run)
 			return false;
 
+		{
+			char        timebuf[128];
+			struct timeval time;
+			time_t      tt;
+
+			gettimeofday(&time, NULL);
+			tt = (time_t) time.tv_sec;
+			strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+			snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+					 ".%03d", (int) (time.tv_usec / 1000));
+			pg_log_warning("creating a replication slot %s end: %s", replslotname, timebuf);
+		}
+
+
+		{
+			char        timebuf[128];
+			struct timeval time;
+			time_t      tt;
+
+			gettimeofday(&time, NULL);
+			tt = (time_t) time.tv_sec;
+			strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+			snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+					 ".%03d", (int) (time.tv_usec / 1000));
+			pg_log_warning("creating a publication start: %s", timebuf);
+		}
+
 		/* Also create a publication */
 		create_publication(conn, primary, perdb);
 
+		{
+			char        timebuf[128];
+			struct timeval time;
+			time_t      tt;
+
+			gettimeofday(&time, NULL);
+			tt = (time_t) time.tv_sec;
+			strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+			snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+					 ".%03d", (int) (time.tv_usec / 1000));
+			pg_log_warning("creating a publication end: %s", timebuf);
+		}
+
 		disconnect_database(conn);
 	}
 
@@ -839,6 +892,19 @@ wait_for_end_recovery(const char *base_conninfo, const char *dbname)
 	if (conn == NULL)
 		exit(1);
 
+	{
+		char        timebuf[128];
+		struct timeval time;
+		time_t      tt;
+
+		gettimeofday(&time, NULL);
+		tt = (time_t) time.tv_sec;
+		strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+		snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+				 ".%03d", (int) (time.tv_usec / 1000));
+		pg_log_warning("creating a wait_for start: %s", timebuf);
+    }
+
 	for (cnt = 0; cnt < wait_seconds * WAITS_PER_SEC; cnt++)
 	{
 		bool		in_recovery;
@@ -875,6 +941,19 @@ wait_for_end_recovery(const char *base_conninfo, const char *dbname)
 		pg_usleep(USEC_PER_SEC / WAITS_PER_SEC);
 	}
 
+	{
+		char        timebuf[128];
+		struct timeval time;
+		time_t      tt;
+
+		gettimeofday(&time, NULL);
+		tt = (time_t) time.tv_sec;
+		strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+		snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+				 ".%03d", (int) (time.tv_usec / 1000));
+		pg_log_warning("creating a wait_for stop: %s", timebuf);
+    }
+
 	disconnect_database(conn);
 
 	/*
@@ -1059,6 +1138,19 @@ create_subscription(PGconn *conn, StandbyInfo *standby, char *base_conninfo,
 
 	pg_log_debug("command is: %s", str->data);
 
+	{
+		char        timebuf[128];
+		struct timeval time;
+		time_t      tt;
+
+		gettimeofday(&time, NULL);
+		tt = (time_t) time.tv_sec;
+		strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+		snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+				 ".%03d", (int) (time.tv_usec / 1000));
+		pg_log_warning("creating a subscription %s start: %s", subname, timebuf);
+	}
+
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
@@ -1071,6 +1163,19 @@ create_subscription(PGconn *conn, StandbyInfo *standby, char *base_conninfo,
 		}
 	}
 
+	{
+		char        timebuf[128];
+		struct timeval time;
+		time_t      tt;
+
+		gettimeofday(&time, NULL);
+		tt = (time_t) time.tv_sec;
+		strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+		snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+				 ".%03d", (int) (time.tv_usec / 1000));
+		pg_log_warning("creating a subscription %s end: %s", subname, timebuf);
+	}
+
 	/* for cleanup purposes */
 	perdb->made_subscription = true;
 
@@ -1277,8 +1382,35 @@ start_standby_server(StandbyInfo *standby, unsigned short subport,
 	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -o \"-p %d\" -l \"%s\"",
 						  standby->bindir,
 						  standby->pgdata, subport, server_start_log);
+
+	{
+        char        timebuf[128];
+        struct timeval time;
+        time_t      tt;
+
+        gettimeofday(&time, NULL);
+        tt = (time_t) time.tv_sec;
+        strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+        snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+                 ".%03d", (int) (time.tv_usec / 1000));
+        pg_log_warning("pg_ctl start begin: %s", timebuf);
+    }
+
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	{
+        char        timebuf[128];
+        struct timeval time;
+        time_t      tt;
+
+        gettimeofday(&time, NULL);
+        tt = (time_t) time.tv_sec;
+        strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+        snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+                 ".%03d", (int) (time.tv_usec / 1000));
+        pg_log_warning("pg_ctl start end: %s", timebuf);
+    }
 }
 
 static char *
@@ -1723,8 +1855,38 @@ main(int argc, char **argv)
 
 		pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
 							  standby.bindir, standby.pgdata);
+
+		
+        {
+            char        timebuf[128];
+            struct timeval time;
+            time_t      tt;
+
+            gettimeofday(&time, NULL);
+            tt = (time_t) time.tv_sec;
+            strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+            snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+                     ".%03d", (int) (time.tv_usec / 1000));
+            pg_log_warning("pg_ctl stop begin: %s", timebuf);
+        }
+
+
 		rc = system(pg_ctl_cmd);
 		pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+
+		{
+            char        timebuf[128];
+            struct timeval time;
+            time_t      tt;
+
+            gettimeofday(&time, NULL);
+            tt = (time_t) time.tv_sec;
+            strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+            snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+                     ".%03d", (int) (time.tv_usec / 1000));
+            pg_log_warning("pg_ctl stop end: %s", timebuf);
+        }
 	}
 
 	/*
@@ -1756,6 +1918,7 @@ main(int argc, char **argv)
 	consistent_lsn = create_logical_replication_slot(conn, true,
 													 &dbarr.perdb[0]);
 
+
 	/*
 	 * Write recovery parameters.
 	 *
@@ -1856,9 +2019,36 @@ main(int argc, char **argv)
 
 	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
 						  standby.bindir, standby.pgdata);
+
+	{
+		char        timebuf[128];
+		struct timeval time;
+		time_t      tt;
+
+		gettimeofday(&time, NULL);
+		tt = (time_t) time.tv_sec;
+		strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+		snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+				 ".%03d", (int) (time.tv_usec / 1000));
+		pg_log_warning("pg_ctl stop for subscriber begin: %s", timebuf);
+	}
+
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 0);
 
+	{
+        char        timebuf[128];
+        struct timeval time;
+        time_t      tt;
+
+        gettimeofday(&time, NULL);
+        tt = (time_t) time.tv_sec;
+        strftime(timebuf, sizeof(timebuf), "%H%M%S", localtime(&tt));
+        snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+                 ".%03d", (int) (time.tv_usec / 1000));
+        pg_log_warning("pg_ctl stop for subscriber end: %s", timebuf);
+    }
+
 	/*
 	 * Change system identifier.
 	 */
#87Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#85)
RE: speed up a logical replica setup

Dear hackers,

Here are comments for v8 patch set. I may revise them by myself,
but I want to post here to share all of them.

01.
```
/* Options */
static char *pub_conninfo_str = NULL;
static SimpleStringList database_names = {NULL, NULL};
static int wait_seconds = DEFAULT_WAIT;
static bool retain = false;
static bool dry_run = false;
```

Just to confirm - is there a policy why we store the specified options? If you
want to store as global ones, username and port should follow (my fault...).
Or, should we have a structure to store them?

02.
```
{"subscriber-conninfo", required_argument, NULL, 'S'},
```

This is my fault, but "--subscriber-conninfo" is still remained. It should be
removed if it is not really needed.

03.
```
{"username", required_argument, NULL, 'u'},
```

Should we accept 'U' instead of 'u'?

04.
```
{"dry-run", no_argument, NULL, 'n'},
```

I'm not sure why the dry_run mode exists. In terms pg_resetwal, it shows the
which value would be changed based on the input. As for the pg_upgrade, it checks
whether the node can be upgraded for now. I think, we should have the checking
feature, so it should be renamed to --check. Also, the process should exit earlier
at that time.

05.
I felt we should accept some settings from enviroment variables, like pg_upgrade.
Currently, below items should be acceted.

- data directory
- username
- port
- timeout

06.
```
pg_logging_set_level(PG_LOG_WARNING);
```

If the default log level is warning, there are no ways to output debug logs.
(-v option only raises one, so INFO would be output)
I think it should be PG_LOG_INFO.

07.
Can we combine verifications into two functions, e.g., check_primary() and check_standby/check_subscriber()?

08.
Not sure, but if we want to record outputs by pg_subscriber, the sub-directory
should be created. The name should contain the timestamp.

09.
Not sure, but should we check max_slot_wal_keep_size of primary server? It can
avoid to fail starting of logical replicaiton.

10.
```
nslots_new = nslots_old + dbarr.ndbs;

if (nslots_new > max_replication_slots)
{
pg_log_error("max_replication_slots (%d) must be greater than or equal to "
"the number of replication slots required (%d)", max_replication_slots, nslots_new);
exit(1);
}
```

I think standby server must not have replication slots. Because subsequent
pg_resetwal command discards all the WAL file, so WAL records pointed by them
are removed. Currently pg_resetwal does not raise ERROR at that time.

11.
```
/*
* Stop the subscriber if it is a standby server. Before executing the
* transformation steps, make sure the subscriber is not running because
* one of the steps is to modify some recovery parameters that require a
* restart.
*/
if (stat(pidfile, &statbuf) == 0)
```

I kept just in case, but I'm not sure it is still needed. How do you think?
Removing it can reduce an inclusion of pidfile.h.

12.
```
pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
standby.bindir, standby.pgdata);
rc = system(pg_ctl_cmd);
pg_ctl_status(pg_ctl_cmd, rc, 0);
```

There are two places to stop the instance. Can you divide it into a function?

13.
```
* A temporary replication slot is not used here to avoid keeping a
* replication connection open (depending when base backup was taken, the
* connection should be open for a few hours).
*/
conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
if (conn == NULL)
exit(1);
consistent_lsn = create_logical_replication_slot(conn, true,
&dbarr.perdb[0]);
```

I didn't notice the comment, but still not sure the reason. Why we must reserve
the slot until pg_subscriber finishes? IIUC, the slot would be never used, it
is created only for getting a consistent_lsn. So we do not have to keep.
Also, just before, logical replication slots for each databases are created, so
WAL records are surely reserved.

14.

```
pg_log_info("starting the subscriber");
start_standby_server(&standby, subport, server_start_log);
```

This info should be in the function.

15.
```
/*
* Create a subscription for each database.
*/
for (i = 0; i < dbarr.ndbs; i++)
```

This can be divided into a function, like create_all_subscriptions().

16.
My fault: usage() must be updated.

17. use_primary_slot_name
```
if (PQntuples(res) != 1)
{
pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
PQntuples(res), 1);
return NULL;
}
```

Error message should be changed. I think this error means the standby has wrong primary_slot_name, right?

18. misc
Sometimes the pid of pg_subscriber is referred. It can be stored as global variable.

19.
C99-style has been allowed, so loop variables like "i" can be declared in the for-statement, like

```
for (int i = 0; i < MAX; i++)
```

20.
Some comments, docs, and outputs must be fixed when the name is changed.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

#88Peter Eisentraut
peter@eisentraut.org
In reply to: Euler Taveira (#81)
Re: speed up a logical replica setup

On 24.01.24 00:44, Euler Taveira wrote:

Subscriber has a different meaning of subscription. Subscription is an SQL
object. Subscriber is the server (node in replication terminology) where the
subscription resides. Having said that pg_createsubscriber doesn't seem
a bad
name because you are creating a new subscriber. (Indeed, you are
transforming /
converting but "create" seems closer and users can infer that it is a
tool to
build a new logical replica.

That makes sense.

(Also, the problem with "convert" etc. is that "convertsubscriber" would
imply that you are converting an existing subscriber to something else.
It would need to be something like "convertbackup" then, which doesn't
seem helpful.)

I think "convert" and "transform" fit for this case. However, "create",
"convert" and "transform" have 6, 7 and 9 characters,  respectively. I
suggest
that we avoid long names (subscriber already has 10 characters). My
preference
is pg_createsubscriber.

That seems best to me.

#89Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Peter Eisentraut (#88)
RE: speed up a logical replica setup

Dear Peter,

Thanks for giving your idea!

Subscriber has a different meaning of subscription. Subscription is an SQL
object. Subscriber is the server (node in replication terminology) where the
subscription resides. Having said that pg_createsubscriber doesn't seem
a bad
name because you are creating a new subscriber. (Indeed, you are
transforming /
converting but "create" seems closer and users can infer that it is a
tool to
build a new logical replica.

That makes sense.

(Also, the problem with "convert" etc. is that "convertsubscriber" would
imply that you are converting an existing subscriber to something else.
It would need to be something like "convertbackup" then, which doesn't
seem helpful.)

I think "convert" and "transform" fit for this case. However, "create",
"convert" and "transform" have 6, 7 and 9 characters, respectively. I
suggest
that we avoid long names (subscriber already has 10 characters). My
preference
is pg_createsubscriber.

That seems best to me.

Just FYI - I'm ok to change the name to pg_createsubscriber.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED

#90Euler Taveira
euler@eulerto.com
In reply to: Euler Taveira (#83)
1 attachment(s)
Re: speed up a logical replica setup

On Tue, Jan 23, 2024, at 10:29 PM, Euler Taveira wrote:

I'll post a new one soon.

I'm attaching another patch that fixes some of the issues pointed out by
Hayato, Shlok, and Junwang.

* publication doesn't exist. The analysis [1]/messages/by-id/TY3PR01MB9889C5D55206DDD978627D07F5752@TY3PR01MB9889.jpnprd01.prod.outlook.com was done by Hayato but I didn't
use the proposed patch. Instead I refactored the code a bit [2]/messages/by-id/73ab86ca-3fd5-49b3-9c80-73d1525202f1@app.fastmail.com and call it
from a new function (setup_publisher) that is called before the promotion.
* fix wrong path name in the initial comment [3]/messages/by-id/TY3PR01MB9889678E47B918F4D83A6FD8F57B2@TY3PR01MB9889.jpnprd01.prod.outlook.com
* change terminology: logical replica -> physical replica [3]/messages/by-id/TY3PR01MB9889678E47B918F4D83A6FD8F57B2@TY3PR01MB9889.jpnprd01.prod.outlook.com
* primary / standby is ready for logical replication? setup_publisher() and
setup_subscriber() check if required GUCs are set accordingly. For primary,
it checks wal_level = logical, max_replication_slots has remain replication
slots for the proposed setup and also max_wal_senders available. For standby,
it checks max_replication_slots for replication origin and also remain number
of background workers to start the subscriber.
* retain option: I extracted this one from Hayato's patch [4]/messages/by-id/TY3PR01MB9889678E47B918F4D83A6FD8F57B2@TY3PR01MB9889.jpnprd01.prod.outlook.com
* target server must be a standby. It seems we agree that this restriction
simplifies the code a bit but can be relaxed in the future (if/when base
backup support is added.)
* recovery timeout option: I decided to include it but I think the use case is
too narrow. It helps in broken setups. However, it can be an issue in some
scenarios like time-delayed replica, large replication lag, slow hardware,
slow network. I didn't use the proposed patch [5]/messages/by-id/TY3PR01MB9889593399165B9A04106741F5662@TY3PR01MB9889.jpnprd01.prod.outlook.com. Instead, I came up with a
simple one that defaults to forever. The proposed one defaults to 60 seconds
but I'm afraid that due to one of the scenarios I said in a previous
sentence, we cancel a legitimate case. Maybe we should add a message during
dry run saying that due to a replication lag, it will take longer to run.
* refactor primary_slot_name code. With the new setup_publisher and
setup_subscriber functions, I splitted the function that detects the
primary_slot_name use into 2 pieces just to avoid extra connections to have
the job done.
* remove fallback_application_name as suggested by Hayato [5]/messages/by-id/TY3PR01MB9889593399165B9A04106741F5662@TY3PR01MB9889.jpnprd01.prod.outlook.com because logical
replication already includes one.

I'm still thinking about replacing --subscriber-conninfo with separate items
(username, port, password?, host = socket dir). Maybe it is an overengineering.
The user can always prepare the environment to avoid unwanted and/or external
connections.

I didn't change the name from pg_subscriber to pg_createsubscriber yet but if I
didn't hear objections about it, I'll do it in the next patch.

[1]: /messages/by-id/TY3PR01MB9889C5D55206DDD978627D07F5752@TY3PR01MB9889.jpnprd01.prod.outlook.com
[2]: /messages/by-id/73ab86ca-3fd5-49b3-9c80-73d1525202f1@app.fastmail.com
[3]: /messages/by-id/TY3PR01MB9889678E47B918F4D83A6FD8F57B2@TY3PR01MB9889.jpnprd01.prod.outlook.com
[4]: /messages/by-id/TY3PR01MB9889678E47B918F4D83A6FD8F57B2@TY3PR01MB9889.jpnprd01.prod.outlook.com
[5]: /messages/by-id/TY3PR01MB9889593399165B9A04106741F5662@TY3PR01MB9889.jpnprd01.prod.outlook.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v9-0001-Creates-a-new-logical-replica-from-a-standby-serv.patchtext/x-patch; name="=?UTF-8?Q?v9-0001-Creates-a-new-logical-replica-from-a-standby-serv.patc?= =?UTF-8?Q?h?="Download
From d9ef01a806c3d8697faa444283f19c2deaa58850 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v9] Creates a new logical replica from a standby server

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server. Remove
the additional replication slot that was used to get the consistent LSN.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_subscriber.sgml           |  305 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_subscriber.c         | 1805 +++++++++++++++++
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   44 +
 .../t/041_pg_subscriber_standby.pl            |  139 ++
 9 files changed, 2322 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_subscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_subscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_subscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..3862c976d7 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgSubscriber       SYSTEM "pg_subscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
new file mode 100644
index 0000000000..99d4fcee49
--- /dev/null
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -0,0 +1,305 @@
+<!--
+doc/src/sgml/ref/pg_subscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgsubscriber">
+ <indexterm zone="app-pgsubscriber">
+  <primary>pg_subscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_subscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_subscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a physical replica and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_subscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--publisher-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_subscriber</application> to output progress messages
+        and detailed information about each step.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_subscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_subscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_subscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_subscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  Another
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_subscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_subscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_subscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_subscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> removes the additional replication
+     slot that was used to get the consistent LSN on the source server.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..266f4e515a 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..0e5384a1d5 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_subscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..f6281b7676 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_subscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_subscriber: $(WIN32RES) pg_subscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_subscriber$(X) '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_subscriber$(X) pg_subscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..ccfd7bb7a5 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_subscriber_sources = files(
+  'pg_subscriber.c'
+)
+
+if host_system == 'windows'
+  pg_subscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_subscriber',
+	'--FILEDESC', 'pg_subscriber - create a new logical replica from a standby server',])
+endif
+
+pg_subscriber = executable('pg_subscriber',
+  pg_subscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_subscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_subscriber.pl',
+      't/041_pg_subscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
new file mode 100644
index 0000000000..cb97dbda5e
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -0,0 +1,1805 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+static bool retain = false;
+static int	recovery_timeout = 0;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static char temp_replslot[NAMEDATALEN] = {0};
+static bool made_transient_replslot = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_subscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+
+	if (made_transient_replslot)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+			disconnect_database(conn);
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-conninfo=CONNINFO   publisher connection string\n"));
+	printf(_(" -S, --subscriber-conninfo=CONNINFO  subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Is the source server ready for logical replication? If so, create the
+ * publications and replication slots in preparation for logical replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical
+	 * max_replication_slots >= current + number of dbs to be converted
+	 * max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
+		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
+		 * to reduce the probability of collision. By default, subscription
+		 * name is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_subscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the target server ready for logical replication?
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (transient_replslot)
+		made_transient_replslot = true;
+	else
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (recovery_timeout > 0 && timer >= recovery_timeout)
+		{
+			pg_log_error("recovery timed out");
+
+			pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+			rc = system(pg_ctl_cmd);
+			pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+			exit(1);
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_subscriber must have created this
+		 * publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_subscriber_ prefix followed by the exact
+			 * database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-conninfo", required_argument, NULL, 'P'},
+		{"subscriber-conninfo", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+	int			rc;
+
+	char	   *pg_ctl_cmd;
+
+	char	   *base_dir;
+	char	   *server_start_log;
+
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	int			i;
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_subscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&database_names, optarg))
+				{
+					simple_string_list_append(&database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				retain = true;
+				break;
+			case 't':
+				recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("directory path for subscriber is too long");
+		exit(1);
+	}
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+	{
+		pg_log_error("could not create directory \"%s\": %m", base_dir);
+		exit(1);
+	}
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!setup_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication and
+		 * create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. We cannot use the last created replication slot
+	 * because the consistent LSN should be obtained *after* the base backup
+	 * finishes (and the base backup should include the logical replication
+	 * slots).
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 *
+	 * A temporary replication slot is not used here to avoid keeping a
+	 * replication connection open (depending when base backup was taken, the
+	 * connection should be open for a few hours).
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	server_start_log = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("log file path is too long");
+		exit(1);
+	}
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create a subscription for each database.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * The transient replication slot is no longer required. Drop it.
+	 *
+	 * If the physical replication slot exists, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops the replication slot later.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+	{
+		if (primary_slot_name != NULL)
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+	else
+	{
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		if (primary_slot_name != NULL)
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
new file mode 100644
index 0000000000..4ebff76b2d
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_subscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_subscriber');
+program_version_ok('pg_subscriber');
+program_options_handling_ok('pg_subscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_subscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+		'--subscriber-conninfo', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
new file mode 100644
index 0000000000..fbcd0fc82b
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_subscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_subscriber on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_subscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
-- 
2.30.2

#91Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#87)
Re: speed up a logical replica setup

On Thu, Jan 25, 2024, at 6:05 AM, Hayato Kuroda (Fujitsu) wrote:

01.
```
/* Options */
static char *pub_conninfo_str = NULL;
static SimpleStringList database_names = {NULL, NULL};
static int wait_seconds = DEFAULT_WAIT;
static bool retain = false;
static bool dry_run = false;
```

Just to confirm - is there a policy why we store the specified options? If you
want to store as global ones, username and port should follow (my fault...).
Or, should we have a structure to store them?

It is a matter of style I would say. Check other client applications. Some of
them also use global variable. There are others that group options into a
struct. I would say that since it has a short lifetime, I don't think the
current style is harmful.

04.
```
{"dry-run", no_argument, NULL, 'n'},
```

I'm not sure why the dry_run mode exists. In terms pg_resetwal, it shows the
which value would be changed based on the input. As for the pg_upgrade, it checks
whether the node can be upgraded for now. I think, we should have the checking
feature, so it should be renamed to --check. Also, the process should exit earlier
at that time.

It is extremely useful because (a) you have a physical replication setup and
don't know if it is prepared for logical replication, (b) check GUCs (is
max_wal_senders sufficient for this pg_subscriber command? Or is
max_replication_slots sufficient to setup the logical replication even though I
already have some used replication slots?), (c) connectivity and (d)
credentials.

05.
I felt we should accept some settings from enviroment variables, like pg_upgrade.
Currently, below items should be acceted.

- data directory
- username
- port
- timeout

Maybe PGDATA.

06.
```
pg_logging_set_level(PG_LOG_WARNING);
```

If the default log level is warning, there are no ways to output debug logs.
(-v option only raises one, so INFO would be output)
I think it should be PG_LOG_INFO.

You need to specify multiple -v options.

07.
Can we combine verifications into two functions, e.g., check_primary() and check_standby/check_subscriber()?

I think v9 does it.

08.
Not sure, but if we want to record outputs by pg_subscriber, the sub-directory
should be created. The name should contain the timestamp.

The log file already contains the timestamp. Why?

09.
Not sure, but should we check max_slot_wal_keep_size of primary server? It can
avoid to fail starting of logical replicaiton.

A broken physical replication *before* running this tool is its responsibility?
Hmm. We might add another check that can be noticed during dry run mode.

10.
```
nslots_new = nslots_old + dbarr.ndbs;

if (nslots_new > max_replication_slots)
{
pg_log_error("max_replication_slots (%d) must be greater than or equal to "
"the number of replication slots required (%d)", max_replication_slots, nslots_new);
exit(1);
}
```

I think standby server must not have replication slots. Because subsequent
pg_resetwal command discards all the WAL file, so WAL records pointed by them
are removed. Currently pg_resetwal does not raise ERROR at that time.

Again, dry run mode might provide a message for it.

11.
```
/*
* Stop the subscriber if it is a standby server. Before executing the
* transformation steps, make sure the subscriber is not running because
* one of the steps is to modify some recovery parameters that require a
* restart.
*/
if (stat(pidfile, &statbuf) == 0)
```

I kept just in case, but I'm not sure it is still needed. How do you think?
Removing it can reduce an inclusion of pidfile.h.

Are you suggesting another way to check if the standby is up and running?

12.
```
pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
standby.bindir, standby.pgdata);
rc = system(pg_ctl_cmd);
pg_ctl_status(pg_ctl_cmd, rc, 0);
```

There are two places to stop the instance. Can you divide it into a function?

Yes.

13.
```
* A temporary replication slot is not used here to avoid keeping a
* replication connection open (depending when base backup was taken, the
* connection should be open for a few hours).
*/
conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
if (conn == NULL)
exit(1);
consistent_lsn = create_logical_replication_slot(conn, true,
&dbarr.perdb[0]);
```

I didn't notice the comment, but still not sure the reason. Why we must reserve
the slot until pg_subscriber finishes? IIUC, the slot would be never used, it
is created only for getting a consistent_lsn. So we do not have to keep.
Also, just before, logical replication slots for each databases are created, so
WAL records are surely reserved.

This comment needs to be updated. It was written at the time I was pursuing
base backup support too. It doesn't matter if you remove this transient
replication slot earlier because all of the replication slots created to the
subscriptions were created *before* the one for the consistent LSN. Hence, no
additional WAL retention due to this transient replication slot.

14.

```
pg_log_info("starting the subscriber");
start_standby_server(&standby, subport, server_start_log);
```

This info should be in the function.

Ok.

15.
```
/*
* Create a subscription for each database.
*/
for (i = 0; i < dbarr.ndbs; i++)
```

This can be divided into a function, like create_all_subscriptions().

Ok.

16.
My fault: usage() must be updated.

17. use_primary_slot_name
```
if (PQntuples(res) != 1)
{
pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
PQntuples(res), 1);
return NULL;
}
```

Error message should be changed. I think this error means the standby has wrong primary_slot_name, right?

I refactored this code a bit but the message is the same. It detects 2 cases:
(a) you set primary_slot_name but you don't have a replication slot with the
same name and (b) a cannot-happen bug that provides > 1 rows. It is a broken
setup so maybe a hint saying so is enough.

18. misc
Sometimes the pid of pg_subscriber is referred. It can be stored as global variable.

I prefer to keep getpid() call.

19.
C99-style has been allowed, so loop variables like "i" can be declared in the for-statement, like

```
for (int i = 0; i < MAX; i++)
```

v9 does it.

20.
Some comments, docs, and outputs must be fixed when the name is changed.

Next patch.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#92Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#91)
RE: speed up a logical replica setup

Dear Euler,

Thanks for updating the patch! Before reading yours, I wanted to reply some of comments.

I'm still thinking about replacing --subscriber-conninfo with separate items
(username, port, password?, host = socket dir). Maybe it is an overengineering.
The user can always prepare the environment to avoid unwanted and/or external
connections.

For me, required amount of fixes are not so different from current one. How about
others?

It is extremely useful because (a) you have a physical replication setup and
don't know if it is prepared for logical replication, (b) check GUCs (is
max_wal_senders sufficient for this pg_subscriber command? Or is
max_replication_slots sufficient to setup the logical replication even though I
already have some used replication slots?), (c) connectivity and (d)
credentials.

Yeah, it is useful for verification purpose, so let's keep this option.
But I still think the naming should be "--check". Also, there are many
`if (!dry_run)` but most of them can be removed if the process exits earlier.
Thought?

05.
I felt we should accept some settings from enviroment variables, like pg_upgrade.
Currently, below items should be acceted.

- data directory
- username
- port
- timeout

Maybe PGDATA.

Sorry, I cannot follow this. Did you mean that the target data directory should
be able to be specified by PGDATA? OF so, +1.

06.
```
pg_logging_set_level(PG_LOG_WARNING);
```

If the default log level is warning, there are no ways to output debug logs.
(-v option only raises one, so INFO would be output)
I think it should be PG_LOG_INFO.

You need to specify multiple -v options.

Hmm. I felt the specification was bit strange...but at least it must be
described on the documentation. pg_dump.sgml has similar lines.

08.
Not sure, but if we want to record outputs by pg_subscriber, the sub-directory
should be created. The name should contain the timestamp.

The log file already contains the timestamp. Why?

This comment assumed outputs by pg_subscriber were also recorded to a file.
In this case and if the file also has the same timestamp, I think they can be
gathered in the same place. No need if outputs are not recorded.

09.
Not sure, but should we check max_slot_wal_keep_size of primary server? It can
avoid to fail starting of logical replicaiton.

A broken physical replication *before* running this tool is its responsibility?
Hmm. We might add another check that can be noticed during dry run mode.

I thought that we should not generate any broken objects, but indeed, not sure
it is our scope. How do other think?

11.
```
/*
* Stop the subscriber if it is a standby server. Before executing the
* transformation steps, make sure the subscriber is not running because
* one of the steps is to modify some recovery parameters that require a
* restart.
*/
if (stat(pidfile, &statbuf) == 0)
```

I kept just in case, but I'm not sure it is still needed. How do you think?
Removing it can reduce an inclusion of pidfile.h.

Are you suggesting another way to check if the standby is up and running?

Running `pg_ctl stop` itself can detect whether the process has been still alive.
It would exit with 1 when the process is not there.

I didn't notice the comment, but still not sure the reason. Why we must reserve
the slot until pg_subscriber finishes? IIUC, the slot would be never used, it
is created only for getting a consistent_lsn. So we do not have to keep.
Also, just before, logical replication slots for each databases are created, so
WAL records are surely reserved.

I want to confirm the conclusion - will you remove the creation of a transient slot?

Also, not tested, I'm now considering that we can reuse the primary_conninfo value.
We are assuming that the target server is standby and the current upstream one will
convert to publisher. In this case, the connection string is already specified as
primary_conninfo so --publisher-conninfo may not be needed. The parameter does
not contain database name, so --databases is still needed. I imagine like:

1. Parse options
2. Turn on standby
3. Verify the standby
4. Turn off standby
5. Get primary_conninfo from standby
6. Connect to primary (concat of primary_conninfo and an option is used)
7. Verify the primary
...

How do you think?

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#93Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#90)
RE: speed up a logical replica setup

Dear Euler,

Again, thanks for updating the patch! There are my random comments for v9.

01.
I cannot find your replies for my comments#7 [1]/messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com but you reverted related changes.
I'm not sure you are still considering it or you decided not to include changes.
Can you clarify your opinion?
(It is needed because changes are huge so it quite affects other developments...)

02.
```
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--timeout=<replaceable class="parameter">seconds</replaceable></option></term>
```

But source codes required `--recovery-timeout`. Please update either of them,

03.
```
+ * Create a new logical replica from a standby server
```

Junwang pointed out to change here but the change was reverted [2]/messages/by-id/CAEG8a3+wL_2R8n12BmRz7yBP3EBNdHDhmdgxQFA9vS+zPR+3Kw@mail.gmail.com
Can you clarify your opinion as well?

04.
```
+/*
+ * Is the source server ready for logical replication? If so, create the
+ * publications and replication slots in preparation for logical replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
```

But this function verifies the source server. I felt they should be in the
different function.

05.
```
+/*
+ * Is the target server ready for logical replication?
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo)
````

Actually, this function does not set up subscriber. It just verifies whether the
target can become a subscriber, right? If should be renamed.

06.
```
+ atexit(cleanup_objects_atexit);
```

The registration of the cleanup function is too early. This sometimes triggers
a core-dump. E.g.,

```
$ pg_subscriber --publisher-conninfo --subscriber-conninfo 'user=postgres port=5432' --verbose --database 'postgres' --pgdata data_N2/
pg_subscriber: error: too many command-line arguments (first is "user=postgres port=5432")
pg_subscriber: hint: Try "pg_subscriber --help" for more information.
Segmentation fault (core dumped)

$ gdb ...
(gdb) bt
#0 cleanup_objects_atexit () at pg_subscriber.c:131
#1 0x00007fb982cffce9 in __run_exit_handlers () from /lib64/libc.so.6
#2 0x00007fb982cffd37 in exit () from /lib64/libc.so.6
#3 0x00000000004054e6 in main (argc=9, argv=0x7ffc59074158) at pg_subscriber.c:1500
(gdb) f 3
#3 0x00000000004054e6 in main (argc=9, argv=0x7ffc59074158) at pg_subscriber.c:1500
1500 exit(1);
(gdb) list
1495 if (optind < argc)
1496 {
1497 pg_log_error("too many command-line arguments (first is \"%s\")",
1498 argv[optind]);
1499 pg_log_error_hint("Try \"%s --help\" for more information.", progname);
1500 exit(1);
1501 }
1502
1503 /*
1504 * Required arguments
```

I still think it should be done just before the creation of objects [3]/messages/by-id/TY3PR01MB9889678E47B918F4D83A6FD8F57B2@TY3PR01MB9889.jpnprd01.prod.outlook.com.

07.
Missing a removal of publications on the standby.

08.
Missing registration of LogicalRepInfo in the typedefs.list.

09
```
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
```

Can you reply my comment#2 [4]/messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com? I think mandatory options should be written.

10.
Just to confirm - will you implement start_standby/stop_standby functions in next version?

[1]: /messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com
[2]: /messages/by-id/CAEG8a3+wL_2R8n12BmRz7yBP3EBNdHDhmdgxQFA9vS+zPR+3Kw@mail.gmail.com
[3]: /messages/by-id/TY3PR01MB9889678E47B918F4D83A6FD8F57B2@TY3PR01MB9889.jpnprd01.prod.outlook.com
[4]: /messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#94Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#93)
Re: speed up a logical replica setup

On Fri, Jan 26, 2024, at 4:55 AM, Hayato Kuroda (Fujitsu) wrote:

Again, thanks for updating the patch! There are my random comments for v9.

Thanks for checking v9. I already incorporated some of the points below into
the next patch. Give me a couple of hours to include some important points.

01.
I cannot find your replies for my comments#7 [1] but you reverted related changes.
I'm not sure you are still considering it or you decided not to include changes.
Can you clarify your opinion?
(It is needed because changes are huge so it quite affects other developments...)

It is still on my list. As I said in a previous email I'm having a hard time
reviewing pieces from your 0002 patch because you include a bunch of things
into one patch.

02.
```
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--timeout=<replaceable class="parameter">seconds</replaceable></option></term>
```

But source codes required `--recovery-timeout`. Please update either of them,

Oops. Fixed. My preference is --recovery-timeout because someone can decide to
include a --timeout option for this tool.

03.
```
+ * Create a new logical replica from a standby server
```

Junwang pointed out to change here but the change was reverted [2]
Can you clarify your opinion as well?

I'll review the documentation once I fix the code. Since the tool includes the
*create* verb into its name, it seems strange use another verb (convert) in the
description. Maybe we should just remove the word *new* and a long description
explains the action is to turn the standby (physical replica) into a logical
replica.

04.
```
+/*
+ * Is the source server ready for logical replication? If so, create the
+ * publications and replication slots in preparation for logical replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
```

But this function verifies the source server. I felt they should be in the
different function.

I split setup_publisher() into check_publisher() and setup_publisher().

05.
```
+/*
+ * Is the target server ready for logical replication?
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo)
````

Actually, this function does not set up subscriber. It just verifies whether the
target can become a subscriber, right? If should be renamed.

I renamed setup_subscriber() -> check_subscriber().

06.
```
+ atexit(cleanup_objects_atexit);
```

The registration of the cleanup function is too early. This sometimes triggers
a core-dump. E.g.,

I forgot to apply the atexit() patch.

07.
Missing a removal of publications on the standby.

It was on my list to do. It will be in the next patch.

08.
Missing registration of LogicalRepInfo in the typedefs.list.

Done.

09
```
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
```

Can you reply my comment#2 [4]? I think mandatory options should be written.

I included the mandatory options into the synopsis.

10.
Just to confirm - will you implement start_standby/stop_standby functions in next version?

It is still on my list.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#95Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#94)
4 attachment(s)
RE: speed up a logical replica setup

Dear Euler,

It is still on my list. As I said in a previous email I'm having a hard time
reviewing pieces from your 0002 patch because you include a bunch of things
...
It is still on my list.

I understood that patches we posted were bad. Sorry for inconvenience.
So I extracted changes only related with them. Can you review them and include
If it seems OK?

v10-0001: same as v9-0001.
0002: not related with our changes, but this fixed a small bug.
The third argument of getopt_long was not correct.
0003: adds start_standby/stop_standby funcitons
0004: tries to refactor some structures.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

Attachments:

v10-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchapplication/octet-stream; name=v10-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchDownload
From 8eff27951f4b778639e91e0a02f871628af60afd Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v10 1/4] Creates a new logical replica from a standby server

A new tool called pg_subscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server. Remove
the additional replication slot that was used to get the consistent LSN.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_subscriber.sgml           |  305 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_subscriber.c         | 1805 +++++++++++++++++
 src/bin/pg_basebackup/t/040_pg_subscriber.pl  |   44 +
 .../t/041_pg_subscriber_standby.pl            |  139 ++
 9 files changed, 2322 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_subscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_subscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_subscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..3862c976d7 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgSubscriber       SYSTEM "pg_subscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_subscriber.sgml b/doc/src/sgml/ref/pg_subscriber.sgml
new file mode 100644
index 0000000000..99d4fcee49
--- /dev/null
+++ b/doc/src/sgml/ref/pg_subscriber.sgml
@@ -0,0 +1,305 @@
+<!--
+doc/src/sgml/ref/pg_subscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgsubscriber">
+ <indexterm zone="app-pgsubscriber">
+  <primary>pg_subscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_subscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_subscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_subscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_subscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a physical replica and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_subscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_subscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--publisher-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">conninfo</replaceable></option></term>
+      <term><option>--subscriber-conninfo=<replaceable class="parameter">conninfo</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_subscriber</application> to output progress messages
+        and detailed information about each step.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_subscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_subscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_subscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_subscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  Another
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_subscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_subscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_subscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_subscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> removes the additional replication
+     slot that was used to get the consistent LSN on the source server.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_subscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_subscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..266f4e515a 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..0e5384a1d5 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_subscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..f6281b7676 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_subscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_subscriber: $(WIN32RES) pg_subscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_subscriber$(X) '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_subscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_subscriber$(X) pg_subscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..ccfd7bb7a5 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_subscriber_sources = files(
+  'pg_subscriber.c'
+)
+
+if host_system == 'windows'
+  pg_subscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_subscriber',
+	'--FILEDESC', 'pg_subscriber - create a new logical replica from a standby server',])
+endif
+
+pg_subscriber = executable('pg_subscriber',
+  pg_subscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_subscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_subscriber.pl',
+      't/041_pg_subscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
new file mode 100644
index 0000000000..cb97dbda5e
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -0,0 +1,1805 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_subscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_subscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+static bool retain = false;
+static int	recovery_timeout = 0;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static char temp_replslot[NAMEDATALEN] = {0};
+static bool made_transient_replslot = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_subscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+
+	if (made_transient_replslot)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+			disconnect_database(conn);
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-conninfo=CONNINFO   publisher connection string\n"));
+	printf(_(" -S, --subscriber-conninfo=CONNINFO  subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Is the source server ready for logical replication? If so, create the
+ * publications and replication slots in preparation for logical replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical
+	 * max_replication_slots >= current + number of dbs to be converted
+	 * max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
+		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
+		 * to reduce the probability of collision. By default, subscription
+		 * name is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_subscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the target server ready for logical replication?
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (transient_replslot)
+		made_transient_replslot = true;
+	else
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (recovery_timeout > 0 && timer >= recovery_timeout)
+		{
+			pg_log_error("recovery timed out");
+
+			pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+			rc = system(pg_ctl_cmd);
+			pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+			exit(1);
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_subscriber must have created this
+		 * publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_subscriber_ prefix followed by the exact
+			 * database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-conninfo", required_argument, NULL, 'P'},
+		{"subscriber-conninfo", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+	int			rc;
+
+	char	   *pg_ctl_cmd;
+
+	char	   *base_dir;
+	char	   *server_start_log;
+
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	int			i;
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_subscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_subscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&database_names, optarg))
+				{
+					simple_string_list_append(&database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				retain = true;
+				break;
+			case 't':
+				recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("directory path for subscriber is too long");
+		exit(1);
+	}
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+	{
+		pg_log_error("could not create directory \"%s\": %m", base_dir);
+		exit(1);
+	}
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!setup_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication and
+		 * create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+
+		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+		rc = system(pg_ctl_cmd);
+		pg_ctl_status(pg_ctl_cmd, rc, 0);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. We cannot use the last created replication slot
+	 * because the consistent LSN should be obtained *after* the base backup
+	 * finishes (and the base backup should include the logical replication
+	 * slots).
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 *
+	 * A temporary replication slot is not used here to avoid keeping a
+	 * replication connection open (depending when base backup was taken, the
+	 * connection should be open for a few hours).
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	server_start_log = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("log file path is too long");
+		exit(1);
+	}
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create a subscription for each database.
+	 */
+	for (i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	/*
+	 * The transient replication slot is no longer required. Drop it.
+	 *
+	 * If the physical replication slot exists, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops the replication slot later.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+	{
+		if (primary_slot_name != NULL)
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+	else
+	{
+		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		if (primary_slot_name != NULL)
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_subscriber.pl b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
new file mode 100644
index 0000000000..4ebff76b2d
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_subscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_subscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_subscriber');
+program_version_ok('pg_subscriber');
+program_options_handling_ok('pg_subscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_subscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_subscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-conninfo', 'dbname=postgres',
+		'--subscriber-conninfo', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
new file mode 100644
index 0000000000..fbcd0fc82b
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_subscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_subscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_subscriber on node S
+command_ok(
+	[
+		'pg_subscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-conninfo', $node_p->connstr('pg1'),
+		'--subscriber-conninfo', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_subscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_subscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
-- 
2.43.0

v10-0002-fix-getopt-options.patchapplication/octet-stream; name=v10-0002-fix-getopt-options.patchDownload
From 722a2ba9bb647559def5f6d826fe9886973910d4 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 29 Jan 2024 08:43:11 +0000
Subject: [PATCH v10 2/4] fix getopt options

---
 src/bin/pg_basebackup/pg_subscriber.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index cb97dbda5e..df770f0fc8 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -1448,7 +1448,7 @@ main(int argc, char **argv)
 	}
 #endif
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nv",
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
-- 
2.43.0

v10-0003-add-start_standby-stop_standby-functions.patchapplication/octet-stream; name=v10-0003-add-start_standby-stop_standby-functions.patchDownload
From e17e392b68011f9173ddaa9bc88009a8e5629c44 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 29 Jan 2024 08:27:48 +0000
Subject: [PATCH v10 3/4] add start_standby/stop_standby functions

---
 src/bin/pg_basebackup/pg_subscriber.c | 104 +++++++++++++++++---------
 1 file changed, 69 insertions(+), 35 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index df770f0fc8..f535e7160f 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -75,6 +75,9 @@ static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
 static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 
+static void start_standby(char *server_start_log);
+static void stop_standby(void);
+
 #define	USEC_PER_SEC	1000000
 #define	WAIT_INTERVAL	1		/* 1 second */
 
@@ -1363,6 +1366,68 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	destroyPQExpBuffer(str);
 }
 
+/*
+ * Start a standby server with given logfile. This also decides filename if not
+ * determined yet. If the start is failed, the process exits with error
+ * messages.
+ */
+static void
+start_standby(char *server_start_log)
+{
+	int			rc;
+	char	   *pg_ctl_cmd;
+
+	Assert(server_start_log != NULL);
+
+	if (server_start_log[0] == '\0')
+	{
+		char		timebuf[128];
+		struct timeval time;
+		time_t		tt;
+		int			len;
+
+		/* append timestamp with ISO 8601 format. */
+		gettimeofday(&time, NULL);
+		tt = (time_t) time.tv_sec;
+		strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+		snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+				 ".%03d", (int) (time.tv_usec / 1000));
+
+		len = snprintf(server_start_log, MAXPGPATH,
+					   "%s/%s/server_start_%s.log", subscriber_dir,
+					   PGS_OUTPUT_DIR, timebuf);
+		if (len >= MAXPGPATH)
+		{
+			pg_log_error("log file path is too long");
+			exit(1);
+		}
+	}
+
+	pg_log_info("starting the standby server");
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path,
+						  subscriber_dir, server_start_log);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+/*
+ * Stop a standby server. If the stop is failed, the process exits with error
+ * messages.
+ */
+static void
+stop_standby(void)
+{
+	int			rc;
+	char	   *pg_ctl_cmd;
+
+	pg_log_info("stopping the standby server");
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  subscriber_dir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
 int
 main(int argc, char **argv)
 {
@@ -1383,16 +1448,10 @@ main(int argc, char **argv)
 
 	int			c;
 	int			option_index;
-	int			rc;
-
-	char	   *pg_ctl_cmd;
 
 	char	   *base_dir;
-	char	   *server_start_log;
+	char		server_start_log[MAXPGPATH] = {0};
 
-	char		timebuf[128];
-	struct timeval time;
-	time_t		tt;
 	int			len;
 
 	char	   *pub_base_conninfo = NULL;
@@ -1638,9 +1697,7 @@ main(int argc, char **argv)
 		pg_log_info("standby is up and running");
 		pg_log_info("stopping the server to start the transformation steps");
 
-		pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
-		rc = system(pg_ctl_cmd);
-		pg_ctl_status(pg_ctl_cmd, rc, 0);
+		stop_standby();
 	}
 	else
 	{
@@ -1705,26 +1762,7 @@ main(int argc, char **argv)
 	/*
 	 * Start subscriber and wait until accepting connections.
 	 */
-	pg_log_info("starting the subscriber");
-
-	/* append timestamp with ISO 8601 format. */
-	gettimeofday(&time, NULL);
-	tt = (time_t) time.tv_sec;
-	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
-	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
-			 ".%03d", (int) (time.tv_usec / 1000));
-
-	server_start_log = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(server_start_log, MAXPGPATH, "%s/%s/server_start_%s.log", subscriber_dir, PGS_OUTPUT_DIR, timebuf);
-	if (len >= MAXPGPATH)
-	{
-		pg_log_error("log file path is too long");
-		exit(1);
-	}
-
-	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, subscriber_dir, server_start_log);
-	rc = system(pg_ctl_cmd);
-	pg_ctl_status(pg_ctl_cmd, rc, 1);
+	start_standby(server_start_log);
 
 	/*
 	 * Waiting the subscriber to be promoted.
@@ -1779,11 +1817,7 @@ main(int argc, char **argv)
 	/*
 	 * Stop the subscriber.
 	 */
-	pg_log_info("stopping the subscriber");
-
-	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
-	rc = system(pg_ctl_cmd);
-	pg_ctl_status(pg_ctl_cmd, rc, 0);
+	stop_standby();
 
 	/*
 	 * Change system identifier.
-- 
2.43.0

v10-0004-Divide-LogicalReplInfo-into-some-strcutures.patchapplication/octet-stream; name=v10-0004-Divide-LogicalReplInfo-into-some-strcutures.patchDownload
From afd8eb318c40e4602268cf5a82b209cb7fa07af2 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 29 Jan 2024 07:03:59 +0000
Subject: [PATCH v10 4/4] Divide LogicalReplInfo into some strcutures

---
 src/bin/pg_basebackup/pg_subscriber.c | 674 ++++++++++++++------------
 src/tools/pgindent/typedefs.list      |   4 +
 2 files changed, 374 insertions(+), 304 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_subscriber.c b/src/bin/pg_basebackup/pg_subscriber.c
index f535e7160f..a8df8d5135 100644
--- a/src/bin/pg_basebackup/pg_subscriber.c
+++ b/src/bin/pg_basebackup/pg_subscriber.c
@@ -32,51 +32,72 @@
 
 #define	PGS_OUTPUT_DIR	"pg_subscriber_output.d"
 
-typedef struct LogicalRepInfo
+typedef struct LogicalRepPerdbInfo
 {
-	Oid			oid;			/* database OID */
-	char	   *dbname;			/* database name */
-	char	   *pubconninfo;	/* publication connection string for logical
-								 * replication */
-	char	   *subconninfo;	/* subscription connection string for logical
-								 * replication */
-	char	   *pubname;		/* publication name */
-	char	   *subname;		/* subscription name (also replication slot
-								 * name) */
-
-	bool		made_replslot;	/* replication slot was created */
-	bool		made_publication;	/* publication was created */
-	bool		made_subscription;	/* subscription was created */
-} LogicalRepInfo;
+	Oid		oid;
+	char   *dbname;
+	bool	made_replslot;		/* replication slot was created */
+	bool	made_publication;	/* publication was created */
+	bool	made_subscription; 	/* subscription was created */
+} LogicalRepPerdbInfo;
+
+typedef struct LogicalRepPerdbInfoArr
+{
+	LogicalRepPerdbInfo    *perdb;	/* array of db infos */
+	int						ndbs;	/* number of db infos */
+} LogicalRepPerdbInfoArr;
+
+typedef struct PrimaryInfo
+{
+	char   *base_conninfo;
+	uint64	sysid;
+	bool	made_transient_replslot;
+} PrimaryInfo;
+
+typedef struct StandbyInfo
+{
+	char   *base_conninfo;
+	char   *bindir;
+	char   *pgdata;
+	char   *primary_slot_name;
+	char   *server_log;
+	uint64	sysid;
+} StandbyInfo;
 
 static void cleanup_objects_atexit(void);
 static void usage();
 static char *get_base_conninfo(char *conninfo, char *dbname,
 							   const char *noderole);
-static bool get_exec_path(const char *path);
+static bool get_exec_base_path(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
-static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
-static PGconn *connect_database(const char *conninfo);
+static void store_db_names(LogicalRepPerdbInfo **perdb, int ndbs);
+static PGconn *connect_database(const char *base_conninfo, const char*dbname);
 static void disconnect_database(PGconn *conn);
-static uint64 get_sysid_from_conn(const char *conninfo);
-static uint64 get_control_from_datadir(const char *datadir);
-static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
-static bool setup_publisher(LogicalRepInfo *dbinfo);
-static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-											 char *slot_name);
-static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void get_sysid_for_primary(PrimaryInfo *primary, char *dbname);
+static void get_sysid_for_standby(StandbyInfo *standby);
+static void modify_sysid(const char *bindir, const char *datadir);
+static bool setup_publisher(PrimaryInfo *primary, LogicalRepPerdbInfoArr *dbarr);
+static char *create_logical_replication_slot(PGconn *conn, bool temporary,
+											 LogicalRepPerdbInfo *perdb);
+static void drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+								  const char *slot_name);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
-static void wait_for_end_recovery(const char *conninfo);
-static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
-static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-
-static void start_standby(char *server_start_log);
-static void stop_standby(void);
+static void wait_for_end_recovery(StandbyInfo *standby,
+								  const char *dbname);
+static void create_publication(PGconn *conn, PrimaryInfo *primary,
+							   LogicalRepPerdbInfo *perdb);
+static void drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void create_subscription(PGconn *conn, StandbyInfo *standby,
+								PrimaryInfo *primary,
+								LogicalRepPerdbInfo *perdb);
+static void drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
+
+static void start_standby(StandbyInfo *standby);
+static void stop_standby(StandbyInfo *standby);
 
 #define	USEC_PER_SEC	1000000
 #define	WAIT_INTERVAL	1		/* 1 second */
@@ -84,25 +105,18 @@ static void stop_standby(void);
 /* Options */
 static const char *progname;
 
-static char *subscriber_dir = NULL;
 static char *pub_conninfo_str = NULL;
 static char *sub_conninfo_str = NULL;
 static SimpleStringList database_names = {NULL, NULL};
-static char *primary_slot_name = NULL;
 static bool dry_run = false;
 static bool retain = false;
 static int	recovery_timeout = 0;
 
 static bool success = false;
 
-static char *pg_ctl_path = NULL;
-static char *pg_resetwal_path = NULL;
-
-static LogicalRepInfo *dbinfo;
-static int	num_dbs = 0;
-
-static char temp_replslot[NAMEDATALEN] = {0};
-static bool made_transient_replslot = false;
+static LogicalRepPerdbInfoArr dbarr;
+static PrimaryInfo primary;
+static StandbyInfo standby;
 
 enum WaitPMResult
 {
@@ -112,6 +126,30 @@ enum WaitPMResult
 	POSTMASTER_FAILED
 };
 
+/*
+ * Build the replication slot and subscription name. The name must not exceed
+ * NAMEDATALEN - 1. This current schema uses a maximum of 36 characters
+ * (14 + 10 + 1 + 10 + '\0'). System identifier is included to reduce the
+ * probability of collision. By default, subscription name is used as
+ * replication slot name.
+ */
+static inline void
+get_subscription_name(Oid oid, int pid, char *subname, Size szsub)
+{
+	snprintf(subname, szsub, "pg_subscriber_%u_%d", oid, pid);
+}
+
+/*
+ * Build the publication name. The name must not exceed NAMEDATALEN -
+ * 1. This current schema uses a maximum of 35 characters (14 + 10 +
+ * '\0').
+ */
+static inline void
+get_publication_name(Oid oid, char *pubname, Size szpub)
+{
+	snprintf(pubname, szpub, "pg_subscriber_%u", oid);
+}
+
 
 /*
  * Cleanup objects that were created by pg_subscriber if there is an error.
@@ -129,38 +167,51 @@ cleanup_objects_atexit(void)
 	if (success)
 		return;
 
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
-		if (dbinfo[i].made_subscription)
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
+
+		if (perdb->made_subscription)
 		{
-			conn = connect_database(dbinfo[i].subconninfo);
+			conn = connect_database(standby.base_conninfo, perdb->dbname);
 			if (conn != NULL)
 			{
-				drop_subscription(conn, &dbinfo[i]);
+				drop_subscription(conn, perdb);
 				disconnect_database(conn);
 			}
 		}
 
-		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		if (perdb->made_publication || perdb->made_replslot)
 		{
-			conn = connect_database(dbinfo[i].pubconninfo);
+			conn = connect_database(primary.base_conninfo, perdb->dbname);
 			if (conn != NULL)
 			{
-				if (dbinfo[i].made_publication)
-					drop_publication(conn, &dbinfo[i]);
-				if (dbinfo[i].made_replslot)
-					drop_replication_slot(conn, &dbinfo[i], NULL);
-				disconnect_database(conn);
+				if (perdb->made_publication)
+					drop_publication(conn, perdb);
+				if (perdb->made_replslot)
+				{
+					char replslotname[NAMEDATALEN];
+
+					get_subscription_name(perdb->oid, (int) getpid(),
+										  replslotname, NAMEDATALEN);
+					drop_replication_slot(conn, perdb, replslotname);
+				}
 			}
 		}
 	}
 
-	if (made_transient_replslot)
+	if (primary.made_transient_replslot)
 	{
-		conn = connect_database(dbinfo[0].pubconninfo);
+		char transient_replslot[NAMEDATALEN];
+
+		conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
+
 		if (conn != NULL)
 		{
-			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+			snprintf(transient_replslot, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+					 (int) getpid());
+
+			drop_replication_slot(conn, &dbarr.perdb[0], transient_replslot);
 			disconnect_database(conn);
 		}
 	}
@@ -246,15 +297,16 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 }
 
 /*
- * Get the absolute path from other PostgreSQL binaries (pg_ctl and
- * pg_resetwal) that is used by it.
+ * Get the absolute binary path from another PostgreSQL binary (pg_ctl) and set
+ * to StandbyInfo.
  */
 static bool
-get_exec_path(const char *path)
+get_exec_base_path(const char *path)
 {
-	int			rc;
+	int		rc;
+	char	pg_ctl_path[MAXPGPATH];
+	char   *p;
 
-	pg_ctl_path = pg_malloc(MAXPGPATH);
 	rc = find_other_exec(path, "pg_ctl",
 						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
 						 pg_ctl_path);
@@ -279,30 +331,12 @@ get_exec_path(const char *path)
 
 	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
 
-	pg_resetwal_path = pg_malloc(MAXPGPATH);
-	rc = find_other_exec(path, "pg_resetwal",
-						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
-						 pg_resetwal_path);
-	if (rc < 0)
-	{
-		char		full_path[MAXPGPATH];
+	/* Extract the directory part from the path */
+	p = strrchr(pg_ctl_path, 'p');
+	Assert(p);
 
-		if (find_my_exec(path, full_path) < 0)
-			strlcpy(full_path, progname, sizeof(full_path));
-		if (rc == -1)
-			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
-						 "same directory as \"%s\".\n"
-						 "Check your installation.",
-						 "pg_resetwal", progname, full_path);
-		else
-			pg_log_error("The program \"%s\" was found by \"%s\"\n"
-						 "but was not the same version as %s.\n"
-						 "Check your installation.",
-						 "pg_resetwal", full_path, progname);
-		return false;
-	}
-
-	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+	*p = '\0';
+	standby.bindir = pg_strdup(pg_ctl_path);
 
 	return true;
 }
@@ -366,49 +400,35 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
 }
 
 /*
- * Store publication and subscription information.
+ * Initialize per-db structure and store the name of databases.
  */
-static LogicalRepInfo *
-store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+static void
+store_db_names(LogicalRepPerdbInfo **perdb, int ndbs)
 {
-	LogicalRepInfo *dbinfo;
-	SimpleStringListCell *cell;
-	int			i = 0;
+	SimpleStringListCell   *cell;
+	int						i = 0;
 
-	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+	dbarr.perdb = (LogicalRepPerdbInfo *) pg_malloc0(ndbs *
+											   sizeof(LogicalRepPerdbInfo));
 
 	for (cell = database_names.head; cell; cell = cell->next)
 	{
-		char	   *conninfo;
-
-		/* Publisher. */
-		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
-		dbinfo[i].pubconninfo = conninfo;
-		dbinfo[i].dbname = cell->val;
-		dbinfo[i].made_replslot = false;
-		dbinfo[i].made_publication = false;
-		dbinfo[i].made_subscription = false;
-		/* other struct fields will be filled later. */
-
-		/* Subscriber. */
-		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
-		dbinfo[i].subconninfo = conninfo;
-
+		(*perdb)[i].dbname = pg_strdup(cell->val);
 		i++;
 	}
-
-	return dbinfo;
 }
 
 static PGconn *
-connect_database(const char *conninfo)
+connect_database(const char *base_conninfo, const char*dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	const char *rconninfo;
+	char	   *rconninfo;
+	char	   *concat_conninfo = concat_conninfo_dbname(base_conninfo,
+														 dbname);
 
 	/* logical replication mode */
-	rconninfo = psprintf("%s replication=database", conninfo);
+	rconninfo = psprintf("%s replication=database", concat_conninfo);
 
 	conn = PQconnectdb(rconninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
@@ -426,6 +446,8 @@ connect_database(const char *conninfo)
 	}
 	PQclear(res);
 
+	pfree(rconninfo);
+	pfree(concat_conninfo);
 	return conn;
 }
 
@@ -438,19 +460,18 @@ disconnect_database(PGconn *conn)
 }
 
 /*
- * Obtain the system identifier using the provided connection. It will be used
- * to compare if a data directory is a clone of another one.
+ * Obtain the system identifier from the primary server. It will be used to
+ * compare if a data directory is a clone of another one.
  */
-static uint64
-get_sysid_from_conn(const char *conninfo)
+static void
+get_sysid_for_primary(PrimaryInfo *primary, char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from publisher");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(primary->base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -473,43 +494,40 @@ get_sysid_from_conn(const char *conninfo)
 		exit(1);
 	}
 
-	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+	primary->sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
 
-	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+	pg_log_info("system identifier is " UINT64_FORMAT " on publisher",
+				primary->sysid);
 
 	disconnect_database(conn);
-
-	return sysid;
 }
 
 /*
- * Obtain the system identifier from control file. It will be used to compare
- * if a data directory is a clone of another one. This routine is used locally
- * and avoids a replication connection.
+ * Obtain the system identifier from a standby server. It will be used to
+ * compare if a data directory is a clone of another one. This routine is used
+ * locally and avoids a replication connection.
  */
-static uint64
-get_control_from_datadir(const char *datadir)
+static void
+get_sysid_for_standby(StandbyInfo *standby)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from subscriber");
 
-	cf = get_controlfile(datadir, &crc_ok);
+	cf = get_controlfile(standby->pgdata, &crc_ok);
 	if (!crc_ok)
 	{
 		pg_log_error("control file appears to be corrupt");
 		exit(1);
 	}
 
-	sysid = cf->system_identifier;
+	standby->sysid = cf->system_identifier;
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+	pg_log_info("system identifier is " UINT64_FORMAT " on subscriber",
+				standby->sysid);
 
 	pfree(cf);
-
-	return sysid;
 }
 
 /*
@@ -518,7 +536,7 @@ get_control_from_datadir(const char *datadir)
  * files from one of the systems might be used in the other one.
  */
 static void
-modify_sysid(const char *pg_resetwal_path, const char *datadir)
+modify_sysid(const char *bindir, const char *datadir)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
@@ -553,7 +571,7 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
 
 	pg_log_info("running pg_resetwal on the subscriber");
 
-	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\"", bindir, datadir);
 
 	pg_log_debug("command is: %s", cmd_str);
 
@@ -574,7 +592,7 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
  * publications and replication slots in preparation for logical replication.
  */
 static bool
-setup_publisher(LogicalRepInfo *dbinfo)
+setup_publisher(PrimaryInfo *primary, LogicalRepPerdbInfoArr *dbarr)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -597,7 +615,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
 	 * max_replication_slots >= current + number of dbs to be converted
 	 * max_wal_senders >= current + number of dbs to be converted
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary->base_conninfo, dbarr->perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -635,10 +653,11 @@ setup_publisher(LogicalRepInfo *dbinfo)
 	 * use after the transformation, hence, it will be removed at the end of
 	 * this process.
 	 */
-	if (primary_slot_name)
+	if (standby.primary_slot_name)
 	{
 		appendPQExpBuffer(str,
-						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'",
+						  standby.primary_slot_name);
 
 		pg_log_debug("command is: %s", str->data);
 
@@ -653,13 +672,14 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		{
 			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
 						 PQntuples(res), 1);
-			pg_free(primary_slot_name); /* it is not being used. */
-			primary_slot_name = NULL;
+			pg_free(standby.primary_slot_name); /* it is not being used. */
+			standby.primary_slot_name = NULL;
 			return false;
 		}
 		else
 		{
-			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+			pg_log_info("primary has replication slot \"%s\"",
+						standby.primary_slot_name);
 		}
 
 		PQclear(res);
@@ -673,26 +693,29 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		return false;
 	}
 
-	if (max_repslots - cur_repslots < num_dbs)
+	if (max_repslots - cur_repslots < dbarr->ndbs)
 	{
-		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 dbarr->ndbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + dbarr->ndbs);
 		return false;
 	}
 
-	if (max_walsenders - cur_walsenders < num_dbs)
+	if (max_walsenders - cur_walsenders < dbarr->ndbs)
 	{
-		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
-		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 dbarr->ndbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + dbarr->ndbs);
 		return false;
 	}
 
-	for (int i = 0; i < num_dbs; i++)
+	for (int i = 0; i < dbarr->ndbs; i++)
 	{
-		char		pubname[NAMEDATALEN];
-		char		replslotname[NAMEDATALEN];
+		LogicalRepPerdbInfo *perdb = &dbarr->perdb[i];
 
-		conn = connect_database(dbinfo[i].pubconninfo);
+		conn = connect_database(primary->base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
@@ -712,43 +735,21 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		}
 
 		/* Remember database OID. */
-		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		perdb->oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
 
 		PQclear(res);
 
-		/*
-		 * Build the publication name. The name must not exceed NAMEDATALEN -
-		 * 1. This current schema uses a maximum of 35 characters (14 + 10 +
-		 * '\0').
-		 */
-		snprintf(pubname, sizeof(pubname), "pg_subscriber_%u", dbinfo[i].oid);
-		dbinfo[i].pubname = pg_strdup(pubname);
-
 		/*
 		 * Create publication on publisher. This step should be executed
 		 * *before* promoting the subscriber to avoid any transactions between
 		 * consistent LSN and the new publication rows (such transactions
 		 * wouldn't see the new publication rows resulting in an error).
 		 */
-		create_publication(conn, &dbinfo[i]);
-
-		/*
-		 * Build the replication slot name. The name must not exceed
-		 * NAMEDATALEN - 1. This current schema uses a maximum of 36
-		 * characters (14 + 10 + 1 + 10 + '\0'). System identifier is included
-		 * to reduce the probability of collision. By default, subscription
-		 * name is used as replication slot name.
-		 */
-		snprintf(replslotname, sizeof(replslotname),
-				 "pg_subscriber_%u_%d",
-				 dbinfo[i].oid,
-				 (int) getpid());
-		dbinfo[i].subname = pg_strdup(replslotname);
+		create_publication(conn, primary, perdb);
 
 		/* Create replication slot on publisher. */
-		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
-			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
-		else
+		if (create_logical_replication_slot(conn, false, perdb) == NULL &&
+			!dry_run)
 			return false;
 
 		disconnect_database(conn);
@@ -761,7 +762,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
  * Is the target server ready for logical replication?
  */
 static bool
-setup_subscriber(LogicalRepInfo *dbinfo)
+setup_subscriber(StandbyInfo *standby, LogicalRepPerdbInfoArr *dbarr)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -781,7 +782,7 @@ setup_subscriber(LogicalRepInfo *dbinfo)
 	 * max_logical_replication_workers >= number of dbs to be converted
 	 * max_worker_processes >= 1 + number of dbs to be converted
 	 */
-	conn = connect_database(dbinfo[0].subconninfo);
+	conn = connect_database(standby->base_conninfo, dbarr->perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -798,35 +799,41 @@ setup_subscriber(LogicalRepInfo *dbinfo)
 	max_repslots = atoi(PQgetvalue(res, 1, 0));
 	max_wprocs = atoi(PQgetvalue(res, 2, 0));
 	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
-		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+		standby->primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
 
 	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
 	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
 	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
-	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+	pg_log_debug("subscriber: primary_slot_name: %s", standby->primary_slot_name);
 
 	PQclear(res);
 
 	disconnect_database(conn);
 
-	if (max_repslots < num_dbs)
+	if (max_repslots < dbarr->ndbs)
 	{
-		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 dbarr->ndbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  dbarr->ndbs);
 		return false;
 	}
 
-	if (max_lrworkers < num_dbs)
+	if (max_lrworkers < dbarr->ndbs)
 	{
-		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
-		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 dbarr->ndbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  dbarr->ndbs);
 		return false;
 	}
 
-	if (max_wprocs < num_dbs + 1)
+	if (max_wprocs < dbarr->ndbs + 1)
 	{
-		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
-		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 dbarr->ndbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  dbarr->ndbs + 1);
 		return false;
 	}
 
@@ -841,28 +848,32 @@ setup_subscriber(LogicalRepInfo *dbinfo)
  * result set that contains the consistent LSN.
  */
 static char *
-create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-								char *slot_name)
+create_logical_replication_slot(PGconn *conn, bool temporary,
+								LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res = NULL;
 	char	   *lsn = NULL;
-	bool		transient_replslot = false;
+	char		slot_name[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
 	/*
-	 * If no slot name is informed, it is a transient replication slot used
-	 * only for catch up purposes.
+	 * Construct a name of logical replication slot. The formatting is
+	 * different depends on its persistency.
+	 *
+	 * For persistent slots: the name must be same as the subscription.
+	 * For temporary slots: OID is not needed, but another string is added.
 	 */
-	if (slot_name[0] == '\0')
-	{
+	if (temporary)
 		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
 				 (int) getpid());
-		transient_replslot = true;
-	}
+	else
+		get_subscription_name(perdb->oid, (int) getpid(), slot_name,
+							  NAMEDATALEN);
 
-	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
 	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
@@ -874,17 +885,19 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						 PQresultErrorMessage(res));
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, perdb->dbname, PQresultErrorMessage(res));
 			return lsn;
 		}
+
+		pg_log_info("create replication slot \"%s\" on publisher", slot_name);
 	}
 
 	/* for cleanup purposes */
-	if (transient_replslot)
-		made_transient_replslot = true;
+	if (temporary)
+		primary.made_transient_replslot = true;
 	else
-		dbinfo->made_replslot = true;
+		perdb->made_replslot = true;
 
 	if (!dry_run)
 	{
@@ -898,14 +911,16 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 }
 
 static void
-drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+					  const char *slot_name)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
 
@@ -915,7 +930,8 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, perdb->dbname,
 						 PQerrorMessage(conn));
 
 		PQclear(res);
@@ -968,19 +984,17 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo)
+wait_for_end_recovery(StandbyInfo *standby,
+					  const char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
 	int			status = POSTMASTER_STILL_STARTING;
 	int			timer = 0;
 
-	char	   *pg_ctl_cmd;
-	int			rc;
-
 	pg_log_info("waiting the postmaster to reach the consistent state");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(standby->base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -1021,9 +1035,13 @@ wait_for_end_recovery(const char *conninfo)
 		 */
 		if (recovery_timeout > 0 && timer >= recovery_timeout)
 		{
+			char *pg_ctl_cmd;
+			int	 rc;
+
 			pg_log_error("recovery timed out");
 
-			pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, subscriber_dir);
+			pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s",
+								  standby->bindir, standby->pgdata);
 			rc = system(pg_ctl_cmd);
 			pg_ctl_status(pg_ctl_cmd, rc, 0);
 
@@ -1051,17 +1069,22 @@ wait_for_end_recovery(const char *conninfo)
  * Create a publication that includes all tables in the database.
  */
 static void
-create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+create_publication(PGconn *conn, PrimaryInfo *primary,
+				   LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
+
 	/* Check if the publication needs to be created. */
 	appendPQExpBuffer(str,
 					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
-					  dbinfo->pubname);
+					  pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
@@ -1081,7 +1104,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		 */
 		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
 		{
-			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			pg_log_info("publication \"%s\" already exists", pubname);
 			return;
 		}
 		else
@@ -1094,7 +1117,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 			 * database oid in which puballtables is false.
 			 */
 			pg_log_error("publication \"%s\" does not replicate changes for all tables",
-						 dbinfo->pubname);
+						 pubname);
 			pg_log_error_hint("Consider renaming this publication.");
 			PQclear(res);
 			PQfinish(conn);
@@ -1105,9 +1128,9 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	PQclear(res);
 	resetPQExpBuffer(str);
 
-	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("creating publication \"%s\" on database \"%s\"", pubname, perdb->dbname);
 
-	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1117,14 +1140,14 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
-						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+						 pubname, perdb->dbname, PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
 		}
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_publication = true;
+	perdb->made_publication = true;
 
 	if (!dry_run)
 		PQclear(res);
@@ -1136,24 +1159,28 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove publication if it couldn't finish all steps.
  */
 static void
-drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
 
-	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				pubname, perdb->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", pubname);
 
-	pg_log_debug("command is: %s", str->data);
 
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 pubname, perdb->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1174,19 +1201,30 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * initial location.
  */
 static void
-create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+create_subscription(PGconn *conn, StandbyInfo *standby,
+					PrimaryInfo *primary,
+					LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
 					  "WITH (create_slot = false, copy_data = false, enabled = false)",
-					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+					  subname,
+					  concat_conninfo_dbname(primary->base_conninfo,
+											 perdb->dbname),
+					  pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1196,14 +1234,14 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+						 subname, perdb->dbname, PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
 		}
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_subscription = true;
+	perdb->made_subscription = true;
 
 	if (!dry_run)
 		PQclear(res);
@@ -1215,16 +1253,20 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove subscription if it couldn't finish all steps.
  */
 static void
-drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
+				subname, perdb->dbname);
 
-	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1232,7 +1274,8 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
+						 subname, perdb->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1251,18 +1294,23 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * printing purposes.
  */
 static void
-set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb,
+						 const char *lsn)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 	Oid			suboid;
 	char		originname[NAMEDATALEN];
 	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
 	appendPQExpBuffer(str,
-					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'",
+					  subname);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1303,7 +1351,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	PQclear(res);
 
 	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
-				originname, lsnstr, dbinfo->dbname);
+				originname, lsnstr, perdb->dbname);
 
 	resetPQExpBuffer(str);
 	appendPQExpBuffer(str,
@@ -1317,7 +1365,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
 			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
-						 dbinfo->subname, PQresultErrorMessage(res));
+						 subname, PQresultErrorMessage(res));
 			PQfinish(conn);
 			exit(1);
 		}
@@ -1336,16 +1384,19 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
  * of this setup.
  */
 static void
-enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
 
-	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1354,7 +1405,7 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
-			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+			pg_log_error("could not enable subscription \"%s\": %s", subname,
 						 PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
@@ -1372,20 +1423,20 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * messages.
  */
 static void
-start_standby(char *server_start_log)
+start_standby(StandbyInfo *standby)
 {
 	int			rc;
 	char	   *pg_ctl_cmd;
 
-	Assert(server_start_log != NULL);
-
-	if (server_start_log[0] == '\0')
+	if (standby->server_log == NULL)
 	{
 		char		timebuf[128];
 		struct timeval time;
 		time_t		tt;
 		int			len;
 
+		standby->server_log = (char *) pg_malloc0(MAXPGPATH);
+
 		/* append timestamp with ISO 8601 format. */
 		gettimeofday(&time, NULL);
 		tt = (time_t) time.tv_sec;
@@ -1393,8 +1444,8 @@ start_standby(char *server_start_log)
 		snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
 				 ".%03d", (int) (time.tv_usec / 1000));
 
-		len = snprintf(server_start_log, MAXPGPATH,
-					   "%s/%s/server_start_%s.log", subscriber_dir,
+		len = snprintf(standby->server_log, MAXPGPATH,
+					   "%s/%s/server_start_%s.log", standby->pgdata,
 					   PGS_OUTPUT_DIR, timebuf);
 		if (len >= MAXPGPATH)
 		{
@@ -1404,8 +1455,8 @@ start_standby(char *server_start_log)
 	}
 
 	pg_log_info("starting the standby server");
-	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path,
-						  subscriber_dir, server_start_log);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"",
+						  standby->bindir, standby->pgdata, standby->server_log);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 1);
 }
@@ -1415,15 +1466,15 @@ start_standby(char *server_start_log)
  * messages.
  */
 static void
-stop_standby(void)
+stop_standby(StandbyInfo *standby)
 {
 	int			rc;
 	char	   *pg_ctl_cmd;
 
 	pg_log_info("stopping the standby server");
 
-	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
-						  subscriber_dir);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", standby->bindir,
+						  standby->pgdata);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 0);
 }
@@ -1454,12 +1505,8 @@ main(int argc, char **argv)
 
 	int			len;
 
-	char	   *pub_base_conninfo = NULL;
-	char	   *sub_base_conninfo = NULL;
 	char	   *dbname_conninfo = NULL;
 
-	uint64		pub_sysid;
-	uint64		sub_sysid;
 	struct stat statbuf;
 
 	PGconn	   *conn;
@@ -1513,7 +1560,7 @@ main(int argc, char **argv)
 		switch (c)
 		{
 			case 'D':
-				subscriber_dir = pg_strdup(optarg);
+				standby.pgdata = pg_strdup(optarg);
 				break;
 			case 'P':
 				pub_conninfo_str = pg_strdup(optarg);
@@ -1526,7 +1573,7 @@ main(int argc, char **argv)
 				if (!simple_string_list_member(&database_names, optarg))
 				{
 					simple_string_list_append(&database_names, optarg);
-					num_dbs++;
+					dbarr.ndbs++;
 				}
 				break;
 			case 'n':
@@ -1562,7 +1609,7 @@ main(int argc, char **argv)
 	/*
 	 * Required arguments
 	 */
-	if (subscriber_dir == NULL)
+	if (standby.pgdata == NULL)
 	{
 		pg_log_error("no subscriber data directory specified");
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -1585,9 +1632,10 @@ main(int argc, char **argv)
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 		exit(1);
 	}
-	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
-										  "publisher");
-	if (pub_base_conninfo == NULL)
+	primary.base_conninfo = get_base_conninfo(pub_conninfo_str,
+											  dbname_conninfo,
+											  "publisher");
+	if (primary.base_conninfo == NULL)
 		exit(1);
 
 	if (sub_conninfo_str == NULL)
@@ -1596,8 +1644,12 @@ main(int argc, char **argv)
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 		exit(1);
 	}
-	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
-	if (sub_base_conninfo == NULL)
+
+	standby.base_conninfo = get_base_conninfo(sub_conninfo_str, NULL,
+											  "subscriber");
+
+
+	if (standby.base_conninfo == NULL)
 		exit(1);
 
 	if (database_names.head == NULL)
@@ -1612,7 +1664,7 @@ main(int argc, char **argv)
 		if (dbname_conninfo)
 		{
 			simple_string_list_append(&database_names, dbname_conninfo);
-			num_dbs++;
+			dbarr.ndbs++;
 
 			pg_log_info("database \"%s\" was extracted from the publisher connection string",
 						dbname_conninfo);
@@ -1628,23 +1680,23 @@ main(int argc, char **argv)
 	/*
 	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
 	 */
-	if (!get_exec_path(argv[0]))
+	if (!get_exec_base_path(argv[0]))
 		exit(1);
 
 	/* rudimentary check for a data directory. */
-	if (!check_data_directory(subscriber_dir))
+	if (!check_data_directory(standby.pgdata))
 		exit(1);
 
-	/* Store database information for publisher and subscriber. */
-	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+	/* Store database information to dbarr */
+	store_db_names(&dbarr.perdb, dbarr.ndbs);
 
 	/*
 	 * Check if the subscriber data directory has the same system identifier
 	 * than the publisher data directory.
 	 */
-	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
-	sub_sysid = get_control_from_datadir(subscriber_dir);
-	if (pub_sysid != sub_sysid)
+	get_sysid_for_primary(&primary, dbarr.perdb[0].dbname);
+	get_sysid_for_standby(&standby);
+	if (primary.sysid != standby.sysid)
 	{
 		pg_log_error("subscriber data directory is not a copy of the source database cluster");
 		exit(1);
@@ -1654,7 +1706,7 @@ main(int argc, char **argv)
 	 * Create the output directory to store any data generated by this tool.
 	 */
 	base_dir = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", standby.pgdata, PGS_OUTPUT_DIR);
 	if (len >= MAXPGPATH)
 	{
 		pg_log_error("directory path for subscriber is too long");
@@ -1668,7 +1720,7 @@ main(int argc, char **argv)
 	}
 
 	/* subscriber PID file. */
-	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", standby.pgdata);
 
 	/*
 	 * The standby server must be running. That's because some checks will be
@@ -1681,7 +1733,7 @@ main(int argc, char **argv)
 		/*
 		 * Check if the standby server is ready for logical replication.
 		 */
-		if (!setup_subscriber(dbinfo))
+		if (!setup_subscriber(&standby, &dbarr))
 			exit(1);
 
 		/*
@@ -1691,13 +1743,13 @@ main(int argc, char **argv)
 		 * if the primary slot is in use. We could use an extra connection for
 		 * it but it doesn't seem worth.
 		 */
-		if (!setup_publisher(dbinfo))
+		if (!setup_publisher(&primary, &dbarr))
 			exit(1);
 
 		pg_log_info("standby is up and running");
 		pg_log_info("stopping the server to start the transformation steps");
 
-		stop_standby();
+		stop_standby(&standby);
 	}
 	else
 	{
@@ -1723,11 +1775,11 @@ main(int argc, char **argv)
 	 * replication connection open (depending when base backup was taken, the
 	 * connection should be open for a few hours).
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
-	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
-													 temp_replslot);
+	consistent_lsn = create_logical_replication_slot(conn, true, &dbarr.perdb[0]);
+
 
 	/*
 	 * Write recovery parameters.
@@ -1753,7 +1805,7 @@ main(int argc, char **argv)
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  consistent_lsn);
-		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+		WriteRecoveryConfig(conn, standby.pgdata, recoveryconfcontents);
 	}
 	disconnect_database(conn);
 
@@ -1762,32 +1814,32 @@ main(int argc, char **argv)
 	/*
 	 * Start subscriber and wait until accepting connections.
 	 */
-	start_standby(server_start_log);
+	start_standby(&standby);
 
 	/*
 	 * Waiting the subscriber to be promoted.
 	 */
-	wait_for_end_recovery(dbinfo[0].subconninfo);
+	wait_for_end_recovery(&standby, dbarr.perdb[0].dbname);
 
 	/*
 	 * Create a subscription for each database.
 	 */
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
+
 		/* Connect to subscriber. */
-		conn = connect_database(dbinfo[i].subconninfo);
+		conn = connect_database(standby.base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
-		create_subscription(conn, &dbinfo[i]);
+		create_subscription(conn, &standby, &primary, perdb);
 
 		/* Set the replication progress to the correct LSN. */
-		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+		set_replication_progress(conn, perdb, consistent_lsn);
 
 		/* Enable subscription. */
-		enable_subscription(conn, &dbinfo[i]);
-
-		disconnect_database(conn);
+		enable_subscription(conn, perdb);
 	}
 
 	/*
@@ -1798,31 +1850,45 @@ main(int argc, char **argv)
 	 * XXX we might not fail here. Instead, we provide a warning so the user
 	 * eventually drops the replication slot later.
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
 	if (conn == NULL)
 	{
-		if (primary_slot_name != NULL)
-			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
-		pg_log_warning("could not drop transient replication slot \"%s\" on publisher", temp_replslot);
+		char transient_replslot[NAMEDATALEN];
+
+		snprintf(transient_replslot, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+				 (int) getpid());
+
+		if (standby.primary_slot_name != NULL)
+			pg_log_warning("could not drop replication slot \"%s\" on primary",
+						   standby.primary_slot_name);
+		pg_log_warning("could not drop transient replication slot \"%s\" on publisher",
+					   transient_replslot);
 		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
 	}
 	else
 	{
-		drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[0];
+		char *primary_slot_name = standby.primary_slot_name;
+		char transient_replslot[NAMEDATALEN];
+
 		if (primary_slot_name != NULL)
-			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+			drop_replication_slot(conn, perdb, primary_slot_name);
+
+		snprintf(transient_replslot, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+				 (int) getpid());
+		drop_replication_slot(conn, perdb, transient_replslot);
 		disconnect_database(conn);
 	}
 
 	/*
 	 * Stop the subscriber.
 	 */
-	stop_standby();
+	stop_standby(&standby);
 
 	/*
 	 * Change system identifier.
 	 */
-	modify_sysid(pg_resetwal_path, subscriber_dir);
+	modify_sysid(standby.bindir, standby.pgdata);
 
 	/*
 	 * The log file is kept if retain option is specified or this tool does
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7e866e3c3d..e9e3db9ffc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1506,6 +1506,8 @@ LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
 LogicalRepMsgType
 LogicalRepPartMapEntry
+LogicalRepPerdbInfo
+LogicalRepPerdbInfoArr
 LogicalRepPreparedTxnData
 LogicalRepRelId
 LogicalRepRelMapEntry
@@ -1884,6 +1886,7 @@ PREDICATELOCK
 PREDICATELOCKTAG
 PREDICATELOCKTARGET
 PREDICATELOCKTARGETTAG
+PrimaryInfo
 PROCESS_INFORMATION
 PROCLOCK
 PROCLOCKTAG
@@ -2460,6 +2463,7 @@ SQLValueFunctionOp
 SSL
 SSLExtensionInfoContext
 SSL_CTX
+StandbyInfo
 STARTUPINFO
 STRLEN
 SV
-- 
2.43.0

#96Euler Taveira
euler@eulerto.com
In reply to: Euler Taveira (#94)
1 attachment(s)
Re: speed up a logical replica setup

On Sun, Jan 28, 2024, at 10:10 PM, Euler Taveira wrote:

On Fri, Jan 26, 2024, at 4:55 AM, Hayato Kuroda (Fujitsu) wrote:

Again, thanks for updating the patch! There are my random comments for v9.

Thanks for checking v9. I already incorporated some of the points below into
the next patch. Give me a couple of hours to include some important points.

Here it is another patch that includes the following changes:

* rename the tool to pg_createsubscriber: it was the name with the most votes
[1]: /messages/by-id/b315c7da-7ab1-4014-a2a9-8ab6ae26017c@app.fastmail.com
* fix recovery-timeout option [2]/messages/by-id/TY3PR01MB98895A551923953B3DA3C7C8F5792@TY3PR01MB9889.jpnprd01.prod.outlook.com
* refactor: split setup_publisher into check_publisher (that contains only GUC
checks) and setup_publisher (that does what the name suggests) [2]/messages/by-id/TY3PR01MB98895A551923953B3DA3C7C8F5792@TY3PR01MB9889.jpnprd01.prod.outlook.com
* doc: verbose option can be specified multiple times [2]/messages/by-id/TY3PR01MB98895A551923953B3DA3C7C8F5792@TY3PR01MB9889.jpnprd01.prod.outlook.com
* typedefs.list: add LogicalRepInfo [2]/messages/by-id/TY3PR01MB98895A551923953B3DA3C7C8F5792@TY3PR01MB9889.jpnprd01.prod.outlook.com
* fix: register cleanup routine after the data structure (dbinfo) is assigned
[2]: /messages/by-id/TY3PR01MB98895A551923953B3DA3C7C8F5792@TY3PR01MB9889.jpnprd01.prod.outlook.com
* doc: add mandatory options to synopsis [3]/messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com
* refactor: rename some options (such as publisher-conninfo to
publisher-server) to use the same pattern as pg_rewind.
* feat: remove publications from subscriber [2]/messages/by-id/TY3PR01MB98895A551923953B3DA3C7C8F5792@TY3PR01MB9889.jpnprd01.prod.outlook.com
* feat: use temporary replication slot to get consistent LSN [3]/messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com
* refactor: move subscriber setup to its own function
* refactor: stop standby server to its own function [2]/messages/by-id/TY3PR01MB98895A551923953B3DA3C7C8F5792@TY3PR01MB9889.jpnprd01.prod.outlook.com
* refactor: start standby server to its own function [2]/messages/by-id/TY3PR01MB98895A551923953B3DA3C7C8F5792@TY3PR01MB9889.jpnprd01.prod.outlook.com
* fix: getopt options [4]/messages/by-id/TY3PR01MB98891E7735141FE8760CEC4AF57E2@TY3PR01MB9889.jpnprd01.prod.outlook.com

There is a few open items in my list. I included v10-0002. I had already
included a refactor to include start/stop functions so I didn't include
v10-0003. I'll check v10-0004 tomorrow.

One open item that is worrying me is how to handle the pg_ctl timeout. This
patch does nothing and the user should use PGCTLTIMEOUT environment variable to
avoid that the execution is canceled after 60 seconds (default for pg_ctl).
Even if you set a high value, it might not be enough for cases like
time-delayed replica. Maybe pg_ctl should accept no timeout as --timeout
option. I'll include this caveat into the documentation but I'm afraid it is
not sufficient and we should provide a better way to handle this situation.

[1]: /messages/by-id/b315c7da-7ab1-4014-a2a9-8ab6ae26017c@app.fastmail.com
[2]: /messages/by-id/TY3PR01MB98895A551923953B3DA3C7C8F5792@TY3PR01MB9889.jpnprd01.prod.outlook.com
[3]: /messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com
[4]: /messages/by-id/TY3PR01MB98891E7735141FE8760CEC4AF57E2@TY3PR01MB9889.jpnprd01.prod.outlook.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v11-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchtext/x-patch; name="=?UTF-8?Q?v11-0001-Creates-a-new-logical-replica-from-a-standby-ser.patc?= =?UTF-8?Q?h?="Download
From 67c6e63cf34885bdd687c06e1e071d15f42cdf2a Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v11] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica
into a logical replica. It runs on the target server and should be able
to connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.  Stop the
target server. Change the system identifier from the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  322 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1852 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   44 +
 .../t/041_pg_createsubscriber_standby.pl      |  139 ++
 src/tools/pgindent/typedefs.list              |    1 +
 10 files changed, 2387 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..1c78ff92e0
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,322 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_createsubscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a physical replica and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..478560b3e4
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1852 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *server_logfile_name(const char *datadir);
+static void start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+static bool retain = false;
+static int	recovery_timeout = 0;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical
+	 * max_replication_slots >= current + number of dbs to be converted
+	 * max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical replication and
+ * enable the subscriptions. That's the last step for logical repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	if (transient_replslot)
+		appendPQExpBufferStr(str, " TEMPORARY");
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (!transient_replslot)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+static char *
+server_logfile_name(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *filename;
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("log file path is too long");
+		exit(1);
+	}
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (recovery_timeout > 0 && timer >= recovery_timeout)
+		{
+			pg_log_error("recovery timed out");
+			stop_standby_server(pg_ctl_path, subscriber_dir);
+			exit(1);
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+
+	char	   *base_dir;
+	char	   *server_start_log;
+	int			len;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+	char		temp_replslot[NAMEDATALEN] = {0};
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&database_names, optarg))
+				{
+					simple_string_list_append(&database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				retain = true;
+				break;
+			case 't':
+				recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure. */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("directory path for subscriber is too long");
+		exit(1);
+	}
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+	{
+		pg_log_error("could not create directory \"%s\": %m", base_dir);
+		exit(1);
+	}
+
+	server_start_log = server_logfile_name(subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server. */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	start_standby_server(pg_ctl_path, subscriber_dir, server_start_log);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, subscriber_dir);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..0f02b1bfac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..534bc53a76
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90b37b919c..2c5725e18d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1505,6 +1505,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.30.2

#97Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#96)
RE: speed up a logical replica setup

Dear Euler,

Thanks for updating the patch!

One open item that is worrying me is how to handle the pg_ctl timeout. This
patch does nothing and the user should use PGCTLTIMEOUT environment variable to
avoid that the execution is canceled after 60 seconds (default for pg_ctl).
Even if you set a high value, it might not be enough for cases like
time-delayed replica. Maybe pg_ctl should accept no timeout as --timeout
option. I'll include this caveat into the documentation but I'm afraid it is
not sufficient and we should provide a better way to handle this situation.

I felt you might be confused a bit. Even if the recovery_min_apply_delay is set,
e.g., 10h., the pg_ctl can start and stop the server. This is because the
walreceiver serialize changes as soon as they received. The delay is done by the
startup process. There are no unflushed data, so server instance can be turned off.
If you meant the combination of recovery-timeout and time-delayed replica, yes,
it would be likely to occur. But in the case, using like --no-timeout option is
dangerous. I think we should overwrite recovery_min_apply_delay to zero. Thought?

Below part contains my comments for v11-0001. Note that the ordering is random.

01. doc
```
<group choice="req">
<arg choice="plain"><option>-D</option> </arg>
<arg choice="plain"><option>--pgdata</option></arg>
</group>
```

According to other documentation like pg_upgrade, we do not write both longer
and shorter options in the synopsis section.

02. doc
```
<para>
<application>pg_createsubscriber</application> takes the publisher and subscriber
connection strings, a cluster directory from a physical replica and a list of
database names and it sets up a new logical replica using the physical
recovery process.
</para>

```

I found that you did not include my suggestion without saying [1]/messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com. Do you dislike
the comment or still considering?

03. doc
```
<term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
```

Too many blank after -P.

04. doc
```
<para>
<application>pg_createsubscriber</application> checks if the target data
directory is used by a physical replica. Stop the physical replica if it is
running. One of the next steps is to add some recovery parameters that
requires a server start. This step avoids an error.
</para>
```

I think doc is not updated. Currently (not sure it is good) the physical replica
must be running before doing pg_createsubscriber.

05. doc
```
each specified database on the source server. The replication slot name
contains a <literal>pg_createsubscriber</literal> prefix. These replication
slots will be used by the subscriptions in a future step. A temporary
```

According to the "Replication Slot Management" subsection in logical-replication.sgml,
the format of names should be described expressly, e.g.:

```
These replication slots have generated names:"pg_createsubscriber_%u_%d" (parameters: Subscription oid, Process id pid).
```

Publications, a temporary slot, and subscriptions should be described as well.

06. doc
```
Next, <application>pg_createsubscriber</application> creates one publication
for each specified database on the source server. Each publication
replicates changes for all tables in the database. The publication name

```

Missing update. Publications are created before creating replication slots.

07. general
I think there are some commenting conversions in PG, but this file breaks it.

* Comments must be one-line as much as possible
* Periods must not be added when the comment is one-line.
* Initial character must be capital.

08. general
Some pg_log_error() + exit(1) can be replaced with pg_fatal().

09. LogicalRepInfo
```
char *subconninfo; /* subscription connection string for logical
* replication */
```

As I posted in comment#8[2]/messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com, I don't think it is "subscription connection". Also,
"for logical replication" is bit misreading because it would not be passed to
workers.

10. get_base_conninfo
```
static char *
get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
...
/*
* If --database option is not provided, try to obtain the dbname from
* the publisher conninfo. If dbname parameter is not available, error
* out.
*/

```

I'm not sure getting dbname from the conninfo improves user-experience. I felt
it may trigger an unintended targeting.
(I still think the publisher-server should be removed)

11. check_data_directory
```
/*
* Is it a cluster directory? These are preliminary checks. It is far from
* making an accurate check. If it is not a clone from the publisher, it will
* eventually fail in a future step.
*/
static bool
check_data_directory(const char *datadir)
```

We shoud also check whether pg_createsubscriber can create a file and a directory.
GetDataDirectoryCreatePerm() verifies it.

12. main
```
/* Register a function to clean up objects in case of failure. */
atexit(cleanup_objects_atexit);
```

According to the manpage, callback functions would not be called when it exits
due to signals:

Functions registered using atexit() (and on_exit(3)) are not called if a
process terminates abnormally because of the delivery of a signal.

Do you have a good way to handle the case? One solution is to output created
objects in any log level, but the consideration may be too much. Thought?

13, main
```
/*
* Create a temporary logical replication slot to get a consistent LSN.
```

Just to clarify - I still think the process exits before here in case of dry run.
In case of pg_resetwal, the process exits before doing actual works like
RewriteControlFile().

14. main
```
* XXX we might not fail here. Instead, we provide a warning so the user
* eventually drops this replication slot later.
```

But there are possibilities to exit(1) in drop_replication_slot(). Is it acceptable?

15. wait_for_end_recovery
```
/*
* Bail out after recovery_timeout seconds if this option is set.
*/
if (recovery_timeout > 0 && timer >= recovery_timeout)
```

Hmm, IIUC, it should be enabled by default [3]/messages/by-id/CAA4eK1JRgnhv_ySzuFjN7UaX9qxz5Hqcwew7Fk=+AbJbu0Kd9w@mail.gmail.com. Do you have anything in your mind?

16. main
```
/*
* Create the subscription for each database on subscriber. It does not
* enable it immediately because it needs to adjust the logical
* replication start point to the LSN reported by consistent_lsn (see
* set_replication_progress). It also cleans up publications created by
* this tool and replication to the standby.
*/
if (!setup_subscriber(dbinfo, consistent_lsn))
```

Subscriptions would be created and replication origin would be moved forward here,
but latter one can be done only by the superuser. I felt that this should be
checked in check_subscriber().

17. main
```
/*
* Change system identifier.
*/
modify_sysid(pg_resetwal_path, subscriber_dir);
```

Even if I executed without -v option, an output from pg_resetwal command appears.
It seems bit strange.

```
$ pg_createsubscriber -D data_N2/ -P "port=5431 user=postgres" -S "port=5432 user=postgres" -d postgres
Write-ahead log reset
$
```

[1]: /messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com
[2]: /messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com
[3]: /messages/by-id/CAA4eK1JRgnhv_ySzuFjN7UaX9qxz5Hqcwew7Fk=+AbJbu0Kd9w@mail.gmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#98Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#97)
5 attachment(s)
RE: speed up a logical replica setup

Attachments:

v12-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchapplication/octet-stream; name=v12-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchDownload
From 14e74d8df45fcc25d0ba9d8a8417a1c9bb95b6aa Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v12 1/5] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica
into a logical replica. It runs on the target server and should be able
to connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.  Stop the
target server. Change the system identifier from the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  322 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1852 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   44 +
 .../t/041_pg_createsubscriber_standby.pl      |  139 ++
 src/tools/pgindent/typedefs.list              |    1 +
 10 files changed, 2387 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..1c78ff92e0
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,322 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_createsubscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a physical replica and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..478560b3e4
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1852 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *server_logfile_name(const char *datadir);
+static void start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+static bool retain = false;
+static int	recovery_timeout = 0;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical
+	 * max_replication_slots >= current + number of dbs to be converted
+	 * max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical replication and
+ * enable the subscriptions. That's the last step for logical repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	if (transient_replslot)
+		appendPQExpBufferStr(str, " TEMPORARY");
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (!transient_replslot)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+static char *
+server_logfile_name(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *filename;
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("log file path is too long");
+		exit(1);
+	}
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (recovery_timeout > 0 && timer >= recovery_timeout)
+		{
+			pg_log_error("recovery timed out");
+			stop_standby_server(pg_ctl_path, subscriber_dir);
+			exit(1);
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+
+	char	   *base_dir;
+	char	   *server_start_log;
+	int			len;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+	char		temp_replslot[NAMEDATALEN] = {0};
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&database_names, optarg))
+				{
+					simple_string_list_append(&database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				retain = true;
+				break;
+			case 't':
+				recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure. */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("directory path for subscriber is too long");
+		exit(1);
+	}
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+	{
+		pg_log_error("could not create directory \"%s\": %m", base_dir);
+		exit(1);
+	}
+
+	server_start_log = server_logfile_name(subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server. */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	start_standby_server(pg_ctl_path, subscriber_dir, server_start_log);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, subscriber_dir);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..0f02b1bfac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..534bc53a76
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91433d439b..f51f1ff23f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1505,6 +1505,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.43.0

v12-0002-Avoid-to-use-replication-connections.patchapplication/octet-stream; name=v12-0002-Avoid-to-use-replication-connections.patchDownload
From 36e626128b40a03ab7c50702fa91cb8acdd90e2f Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 31 Jan 2024 07:39:35 +0000
Subject: [PATCH v12 2/5] Avoid to use replication connections

---
 doc/src/sgml/ref/pg_createsubscriber.sgml   |  1 -
 src/bin/pg_basebackup/pg_createsubscriber.c | 27 +++++++++------------
 2 files changed, 11 insertions(+), 17 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 1c78ff92e0..53b42e6161 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -61,7 +61,6 @@ PostgreSQL documentation
    The <application>pg_createsubscriber</application> should be run at the target
    server. The source server (known as publisher server) should accept logical
    replication connections from the target server (known as subscriber server).
-   The target server should accept local logical replication connection.
   </para>
  </refsect1>
 
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 478560b3e4..47970b10d6 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -397,12 +397,8 @@ connect_database(const char *conninfo)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	const char *rconninfo;
 
-	/* logical replication mode */
-	rconninfo = psprintf("%s replication=database", conninfo);
-
-	conn = PQconnectdb(rconninfo);
+	conn = PQconnectdb(conninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
 	{
 		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
@@ -446,26 +442,26 @@ get_sysid_from_conn(const char *conninfo)
 	if (conn == NULL)
 		exit(1);
 
-	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	res = PQexec(conn, "SELECT * FROM pg_control_system();");
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not send replication command \"%s\": %s",
+		pg_log_error("could not send command \"%s\": %s",
 					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
 		PQclear(res);
 		disconnect_database(conn);
 		exit(1);
 	}
-	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	if (PQntuples(res) != 1 || PQnfields(res) < 4)
 	{
 		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
-					 PQntuples(res), PQnfields(res), 1, 3);
+					 PQntuples(res), PQnfields(res), 1, 4);
 
 		PQclear(res);
 		disconnect_database(conn);
 		exit(1);
 	}
 
-	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+	sysid = strtou64(PQgetvalue(res, 0, 2), NULL, 10);
 
 	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
 
@@ -477,7 +473,7 @@ get_sysid_from_conn(const char *conninfo)
 /*
  * Obtain the system identifier from control file. It will be used to compare
  * if a data directory is a clone of another one. This routine is used locally
- * and avoids a replication connection.
+ * and avoids a connection establishment.
  */
 static uint64
 get_control_from_datadir(const char *datadir)
@@ -905,10 +901,8 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 
 	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
 
-	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
-	if (transient_replslot)
-		appendPQExpBufferStr(str, " TEMPORARY");
-	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+	appendPQExpBuffer(str, "SELECT * FROM pg_create_logical_replication_slot('%s', 'pgoutput', %s, false, false);",
+					  slot_name, transient_replslot ? "true" : "false");
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -948,7 +942,8 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 
 	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
 
-	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBuffer(str, "SELECT * FROM pg_drop_replication_slot('%s');",
+					  slot_name);
 
 	pg_log_debug("command is: %s", str->data);
 
-- 
2.43.0

v12-0003-Remove-P-and-use-primary_conninfo-instead.patchapplication/octet-stream; name=v12-0003-Remove-P-and-use-primary_conninfo-instead.patchDownload
From 1315038f73179760000137cd34575645dc8fe86a Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 31 Jan 2024 09:20:54 +0000
Subject: [PATCH v12 3/5] Remove -P and use primary_conninfo instead

XXX: This may be a problematic when the OS user who started target instance is
not the current OS user and PGPASSWORD environment variable was used for
connecting to the primary server. In this case, the password would not be
written in the primary_conninfo and the PGPASSWORD variable might not be set.
This may lead an connection error. Is this a real issue? Note that using
PGPASSWORD is not recommended.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     | 17 +---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 98 ++++++++++++-------
 .../t/040_pg_createsubscriber.pl              |  8 --
 .../t/041_pg_createsubscriber_standby.pl      |  5 +-
 4 files changed, 65 insertions(+), 63 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 53b42e6161..0abe1f6f28 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -29,11 +29,6 @@ PostgreSQL documentation
      <arg choice="plain"><option>--pgdata</option></arg>
     </group>
     <replaceable>datadir</replaceable>
-    <group choice="req">
-     <arg choice="plain"><option>-P</option></arg>
-     <arg choice="plain"><option>--publisher-server</option></arg>
-    </group>
-    <replaceable>connstr</replaceable>
     <group choice="req">
      <arg choice="plain"><option>-S</option></arg>
      <arg choice="plain"><option>--subscriber-server</option></arg>
@@ -83,16 +78,6 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
-     <varlistentry>
-      <term><option>-P  <replaceable class="parameter">connstr</replaceable></option></term>
-      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
-      <listitem>
-       <para>
-        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry>
       <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
       <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
@@ -304,7 +289,7 @@ PostgreSQL documentation
    To create a logical replica for databases <literal>hr</literal> and
    <literal>finance</literal> from a physical replica at <literal>foo</literal>:
 <screen>
-<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -S "host=localhost" -d hr -d finance</userinput>
 </screen>
   </para>
 
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 47970b10d6..f0e9db7793 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -51,10 +51,10 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname,
-							   const char *noderole);
+static char *get_base_conninfo(char *conninfo, char *dbname);
 static bool get_exec_path(const char *path);
 static bool check_data_directory(const char *datadir);
+static char *get_primary_conninfo(const char *base_conninfo);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
 static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
 static PGconn *connect_database(const char *conninfo);
@@ -88,7 +88,6 @@ static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static const char *progname;
 
 static char *subscriber_dir = NULL;
-static char *pub_conninfo_str = NULL;
 static char *sub_conninfo_str = NULL;
 static SimpleStringList database_names = {NULL, NULL};
 static char *primary_slot_name = NULL;
@@ -167,7 +166,6 @@ usage(void)
 	printf(_("  %s [OPTION]...\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
-	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
@@ -192,7 +190,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+get_base_conninfo(char *conninfo, char *dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -201,7 +199,7 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 	char	   *ret;
 	int			i;
 
-	pg_log_info("validating connection string on %s", noderole);
+	pg_log_info("validating connection string on subscriber");
 
 	conn_opts = PQconninfoParse(conninfo, &errmsg);
 	if (conn_opts == NULL)
@@ -425,6 +423,58 @@ disconnect_database(PGconn *conn)
 	PQfinish(conn);
 }
 
+/*
+ * Obtain primary_conninfo from the target server. The value would be used for
+ * connecting from the pg_createsubscriber itself and logical replication apply
+ * worker.
+ */
+static char *
+get_primary_conninfo(const char *base_conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	char	   *conninfo;
+	char	   *primaryconninfo;
+
+	pg_log_info("getting primary_conninfo from standby");
+
+	/*
+	 * Construct a connection string to the target instance. Since dbinfo has
+	 * not stored infomation yet, we must directly get the first element of the
+	 * database list.
+	 */
+	conninfo = concat_conninfo_dbname(base_conninfo, database_names.head->val);
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SHOW primary_conninfo;");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send command \"%s\": %s",
+					 "SHOW primary_conninfo;", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	primaryconninfo = pg_strdup(PQgetvalue(res, 0, 0));
+
+	if (strlen(primaryconninfo) == 0)
+	{
+		pg_log_error("primary_conninfo is empty");
+		pg_log_error_hint("Check whether the target server is really a physical standby.");
+		exit(1);
+	}
+
+	pg_free(conninfo);
+	PQclear(res);
+	disconnect_database(conn);
+
+	return primaryconninfo;
+}
+
 /*
  * Obtain the system identifier using the provided connection. It will be used
  * to compare if a data directory is a clone of another one.
@@ -1452,7 +1502,6 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
-		{"publisher-server", required_argument, NULL, 'P'},
 		{"subscriber-server", required_argument, NULL, 'S'},
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
@@ -1519,7 +1568,7 @@ main(int argc, char **argv)
 	}
 #endif
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+	while ((c = getopt_long(argc, argv, "D:S:d:nrt:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1527,9 +1576,6 @@ main(int argc, char **argv)
 			case 'D':
 				subscriber_dir = pg_strdup(optarg);
 				break;
-			case 'P':
-				pub_conninfo_str = pg_strdup(optarg);
-				break;
 			case 'S':
 				sub_conninfo_str = pg_strdup(optarg);
 				break;
@@ -1581,34 +1627,13 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
-	/*
-	 * Parse connection string. Build a base connection string that might be
-	 * reused by multiple databases.
-	 */
-	if (pub_conninfo_str == NULL)
-	{
-		/*
-		 * TODO use primary_conninfo (if available) from subscriber and
-		 * extract publisher connection string. Assume that there are
-		 * identical entries for physical and logical replication. If there is
-		 * not, we would fail anyway.
-		 */
-		pg_log_error("no publisher connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
-										  "publisher");
-	if (pub_base_conninfo == NULL)
-		exit(1);
-
 	if (sub_conninfo_str == NULL)
 	{
 		pg_log_error("no subscriber connection string specified");
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 		exit(1);
 	}
-	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, dbname_conninfo);
 	if (sub_base_conninfo == NULL)
 		exit(1);
 
@@ -1618,7 +1643,7 @@ main(int argc, char **argv)
 
 		/*
 		 * If --database option is not provided, try to obtain the dbname from
-		 * the publisher conninfo. If dbname parameter is not available, error
+		 * the subscriber conninfo. If dbname parameter is not available, error
 		 * out.
 		 */
 		if (dbname_conninfo)
@@ -1626,7 +1651,7 @@ main(int argc, char **argv)
 			simple_string_list_append(&database_names, dbname_conninfo);
 			num_dbs++;
 
-			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+			pg_log_info("database \"%s\" was extracted from the subscriber connection string",
 						dbname_conninfo);
 		}
 		else
@@ -1637,6 +1662,9 @@ main(int argc, char **argv)
 		}
 	}
 
+	/* Obtain a connection string from the target */
+	pub_base_conninfo = get_primary_conninfo(sub_base_conninfo);
+
 	/*
 	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
 	 */
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 0f02b1bfac..5c240a5417 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -17,18 +17,11 @@ my $datadir = PostgreSQL::Test::Utils::tempdir;
 
 command_fails(['pg_createsubscriber'],
 	'no subscriber data directory specified');
-command_fails(
-	[
-		'pg_createsubscriber',
-		'--pgdata', $datadir
-	],
-	'no publisher connection string specified');
 command_fails(
 	[
 		'pg_createsubscriber',
 		'--dry-run',
 		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres'
 	],
 	'no subscriber connection string specified');
 command_fails(
@@ -36,7 +29,6 @@ command_fails(
 		'pg_createsubscriber',
 		'--verbose',
 		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres',
 		'--subscriber-server', 'dbname=postgres'
 	],
 	'no database name specified');
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 534bc53a76..a9d03acc87 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -56,19 +56,17 @@ command_fails(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_f->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
-	'subscriber data directory is not a copy of the source database cluster');
+	'target database is not a physical standby');
 
 # dry run mode on node S
 command_ok(
 	[
 		'pg_createsubscriber', '--verbose', '--dry-run',
 		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
@@ -88,7 +86,6 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
-- 
2.43.0

v12-0004-Exit-earlier-when-we-are-in-the-dry_run-mode.patchapplication/octet-stream; name=v12-0004-Exit-earlier-when-we-are-in-the-dry_run-mode.patchDownload
From c1aa11c772925044318fc0288e5a16e6bc66f977 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 31 Jan 2024 10:45:21 +0000
Subject: [PATCH v12 4/5] Exit earlier when we are in the dry_run mode

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 198 ++++++++------------
 1 file changed, 75 insertions(+), 123 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index f0e9db7793..6f832f7551 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -584,8 +584,7 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
 	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
 	cf->system_identifier |= getpid() & 0xFFF;
 
-	if (!dry_run)
-		update_controlfile(datadir, cf, true);
+	update_controlfile(datadir, cf, true);
 
 	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
 
@@ -595,14 +594,12 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
 
 	pg_log_debug("command is: %s", cmd_str);
 
-	if (!dry_run)
-	{
-		rc = system(cmd_str);
-		if (rc == 0)
-			pg_log_info("subscriber successfully changed the system identifier");
-		else
-			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
-	}
+	rc = system(cmd_str);
+
+	if (rc == 0)
+		pg_log_info("subscriber successfully changed the system identifier");
+	else
+		pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
 
 	pfree(cf);
 }
@@ -676,7 +673,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		dbinfo[i].subname = pg_strdup(replslotname);
 
 		/* Create replication slot on publisher. */
-		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL)
 			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
 		else
 			return false;
@@ -956,26 +953,20 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 
 	pg_log_debug("command is: %s", str->data);
 
-	if (!dry_run)
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_TUPLES_OK)
-		{
-			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						 PQresultErrorMessage(res));
-			return lsn;
-		}
+		pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						PQresultErrorMessage(res));
+		return lsn;
 	}
 
 	/* for cleanup purposes */
 	if (!transient_replslot)
 		dbinfo->made_replslot = true;
 
-	if (!dry_run)
-	{
-		lsn = pg_strdup(PQgetvalue(res, 0, 1));
-		PQclear(res);
-	}
+	lsn = pg_strdup(PQgetvalue(res, 0, 1));
+	PQclear(res);
 
 	destroyPQExpBuffer(str);
 
@@ -997,15 +988,12 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 
 	pg_log_debug("command is: %s", str->data);
 
-	if (!dry_run)
-	{
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						 PQerrorMessage(conn));
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						PQerrorMessage(conn));
 
-		PQclear(res);
-	}
+	PQclear(res);
 
 	destroyPQExpBuffer(str);
 }
@@ -1138,11 +1126,8 @@ wait_for_end_recovery(const char *conninfo)
 
 		PQclear(res);
 
-		/*
-		 * Does the recovery process finish? In dry run mode, there is no
-		 * recovery mode. Bail out as the recovery process has ended.
-		 */
-		if (!in_recovery || dry_run)
+		/* Does the recovery process finish? */
+		if (!in_recovery)
 		{
 			status = POSTMASTER_READY;
 			break;
@@ -1239,24 +1224,19 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	pg_log_debug("command is: %s", str->data);
 
-	if (!dry_run)
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
 	{
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
-						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
-			PQfinish(conn);
-			exit(1);
-		}
+		pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		PQfinish(conn);
+		exit(1);
 	}
 
 	/* for cleanup purposes */
 	dbinfo->made_publication = true;
 
-	if (!dry_run)
-		PQclear(res);
-
+	PQclear(res);
 	destroyPQExpBuffer(str);
 }
 
@@ -1277,14 +1257,11 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	pg_log_debug("command is: %s", str->data);
 
-	if (!dry_run)
-	{
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
 
-		PQclear(res);
-	}
+	PQclear(res);
 
 	destroyPQExpBuffer(str);
 }
@@ -1318,24 +1295,19 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	pg_log_debug("command is: %s", str->data);
 
-	if (!dry_run)
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
 	{
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
-			PQfinish(conn);
-			exit(1);
-		}
+		pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		PQfinish(conn);
+		exit(1);
 	}
 
 	/* for cleanup purposes */
 	dbinfo->made_subscription = true;
 
-	if (!dry_run)
-		PQclear(res);
-
+	PQclear(res);
 	destroyPQExpBuffer(str);
 }
 
@@ -1356,14 +1328,11 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	pg_log_debug("command is: %s", str->data);
 
-	if (!dry_run)
-	{
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
 
-		PQclear(res);
-	}
+	PQclear(res);
 
 	destroyPQExpBuffer(str);
 }
@@ -1402,7 +1371,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 		exit(1);
 	}
 
-	if (PQntuples(res) != 1 && !dry_run)
+	if (PQntuples(res) != 1)
 	{
 		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
 					 PQntuples(res), 1);
@@ -1411,16 +1380,8 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 		exit(1);
 	}
 
-	if (dry_run)
-	{
-		suboid = InvalidOid;
-		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
-	}
-	else
-	{
-		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
-		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
-	}
+	suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+	snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
 
 	/*
 	 * The origin name is defined as pg_%u. %u is the subscription OID. See
@@ -1439,20 +1400,16 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 
 	pg_log_debug("command is: %s", str->data);
 
-	if (!dry_run)
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_TUPLES_OK)
-		{
-			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
-						 dbinfo->subname, PQresultErrorMessage(res));
-			PQfinish(conn);
-			exit(1);
-		}
-
-		PQclear(res);
+		pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						dbinfo->subname, PQresultErrorMessage(res));
+		PQfinish(conn);
+		exit(1);
 	}
 
+	PQclear(res);
 	destroyPQExpBuffer(str);
 }
 
@@ -1477,20 +1434,16 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	pg_log_debug("command is: %s", str->data);
 
-	if (!dry_run)
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
 	{
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
-						 PQerrorMessage(conn));
-			PQfinish(conn);
-			exit(1);
-		}
-
-		PQclear(res);
+		pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						PQerrorMessage(conn));
+		PQfinish(conn);
+		exit(1);
 	}
 
+	PQclear(res);
 	destroyPQExpBuffer(str);
 }
 
@@ -1759,6 +1712,14 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
+	/*
+	 * Exit earlier when we are in the dry_run mode.
+	 *
+	 * XXX: Should we keep turning on the standby server in case of dry_run?
+	 */
+	if (dry_run)
+		goto cleanup;
+
 	/*
 	 * Create a temporary logical replication slot to get a consistent LSN.
 	 *
@@ -1783,26 +1744,16 @@ main(int argc, char **argv)
 	 * Despite of the recovery parameters will be written to the subscriber,
 	 * use a publisher connection for the follwing recovery functions. The
 	 * connection is only used to check the current server version (physical
-	 * replica, same server version). The subscriber is not running yet. In
-	 * dry run mode, the recovery parameters *won't* be written. An invalid
-	 * LSN is used for printing purposes.
+	 * replica, same server version). The subscriber is not running yet.
 	 */
 	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
 
-	if (dry_run)
-	{
-		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
-		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
-						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
-	}
-	else
-	{
-		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
-						  consistent_lsn);
-		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
-	}
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						consistent_lsn);
+	WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+
 	disconnect_database(conn);
 
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
@@ -1860,6 +1811,7 @@ main(int argc, char **argv)
 	 */
 	modify_sysid(pg_resetwal_path, subscriber_dir);
 
+cleanup:
 	/*
 	 * The log file is kept if retain option is specified or this tool does
 	 * not run successfully. Otherwise, log file is removed.
-- 
2.43.0

v12-0005-Divide-LogicalReplInfo-into-some-strcutures.patchapplication/octet-stream; name=v12-0005-Divide-LogicalReplInfo-into-some-strcutures.patchDownload
From dd7be7f34e1ac68c2b6fd193dec2aa6b2832e1b9 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 29 Jan 2024 07:03:59 +0000
Subject: [PATCH v12 5/5] Divide LogicalReplInfo into some strcutures

---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 660 ++++++++++--------
 .../t/041_pg_createsubscriber_standby.pl      |   2 +-
 src/tools/pgindent/typedefs.list              |   5 +-
 3 files changed, 365 insertions(+), 302 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 6f832f7551..898fa3f114 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -32,54 +32,78 @@
 
 #define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
 
-typedef struct LogicalRepInfo
+typedef struct LogicalRepPerdbInfo
 {
-	Oid			oid;			/* database OID */
-	char	   *dbname;			/* database name */
-	char	   *pubconninfo;	/* publication connection string for logical
-								 * replication */
-	char	   *subconninfo;	/* subscription connection string for logical
-								 * replication */
-	char	   *pubname;		/* publication name */
-	char	   *subname;		/* subscription name (also replication slot
-								 * name) */
-
-	bool		made_replslot;	/* replication slot was created */
-	bool		made_publication;	/* publication was created */
-	bool		made_subscription;	/* subscription was created */
-} LogicalRepInfo;
+	Oid		oid;
+	char   *dbname;
+	bool	made_replslot;		/* replication slot was created */
+	bool	made_publication;	/* publication was created */
+	bool	made_subscription; 	/* subscription was created */
+} LogicalRepPerdbInfo;
+
+typedef struct LogicalRepPerdbInfoArr
+{
+	LogicalRepPerdbInfo    *perdb;	/* array of db infos */
+	int						ndbs;	/* number of db infos */
+} LogicalRepPerdbInfoArr;
+
+typedef struct PrimaryInfo
+{
+	char   *base_conninfo;
+	uint64	sysid;
+	bool	made_transient_replslot;
+} PrimaryInfo;
+
+typedef struct StandbyInfo
+{
+	char   *base_conninfo;
+	char   *bindir;
+	char   *pgdata;
+	char   *primary_slot_name;
+	char   *server_log;
+	uint64	sysid;
+} StandbyInfo;
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname);
-static bool get_exec_path(const char *path);
+static bool get_exec_base_path(const char *path);
 static bool check_data_directory(const char *datadir);
-static char *get_primary_conninfo(const char *base_conninfo);
+static char *get_primary_conninfo(StandbyInfo *standby);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
-static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
-static PGconn *connect_database(const char *conninfo);
+static void store_db_names(LogicalRepPerdbInfo **perdb, int ndbs);
+static PGconn *connect_database(const char *base_conninfo, const char*dbname);
 static void disconnect_database(PGconn *conn);
-static uint64 get_sysid_from_conn(const char *conninfo);
-static uint64 get_control_from_datadir(const char *datadir);
-static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
-static bool check_publisher(LogicalRepInfo *dbinfo);
-static bool setup_publisher(LogicalRepInfo *dbinfo);
-static bool check_subscriber(LogicalRepInfo *dbinfo);
-static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
-static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-											 char *slot_name);
-static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void get_sysid_for_primary(PrimaryInfo *primary, char *dbname);
+static void get_sysid_for_standby(StandbyInfo *standby);
+static void modify_sysid(const char *bindir, const char *datadir);
+static bool check_publisher(PrimaryInfo *primary, LogicalRepPerdbInfoArr *dbarr);
+static bool setup_publisher(PrimaryInfo *primary, LogicalRepPerdbInfoArr *dbarr);
+static bool check_subscriber(StandbyInfo *standby, LogicalRepPerdbInfoArr *dbarr);
+static bool setup_subscriber(StandbyInfo *standby, PrimaryInfo *primary,
+							 LogicalRepPerdbInfoArr *dbarr,
+							 const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, bool temporary,
+											 LogicalRepPerdbInfo *perdb);
+static void drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+								  const char *slot_name);
 static char *server_logfile_name(const char *datadir);
-static void start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile);
-static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void start_standby_server(StandbyInfo *standby);
+static void stop_standby_server(StandbyInfo *standby);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
-static void wait_for_end_recovery(const char *conninfo);
-static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
-static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+
+static void wait_for_end_recovery(StandbyInfo *standby,
+								  const char *dbname);
+static void create_publication(PGconn *conn, PrimaryInfo *primary,
+							   LogicalRepPerdbInfo *perdb);
+static void drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void create_subscription(PGconn *conn, StandbyInfo *standby,
+								PrimaryInfo *primary,
+								LogicalRepPerdbInfo *perdb);
+static void drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
 
 #define	USEC_PER_SEC	1000000
 #define	WAIT_INTERVAL	1		/* 1 second */
@@ -87,21 +111,17 @@ static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 /* Options */
 static const char *progname;
 
-static char *subscriber_dir = NULL;
 static char *sub_conninfo_str = NULL;
 static SimpleStringList database_names = {NULL, NULL};
-static char *primary_slot_name = NULL;
 static bool dry_run = false;
 static bool retain = false;
 static int	recovery_timeout = 0;
 
 static bool success = false;
 
-static char *pg_ctl_path = NULL;
-static char *pg_resetwal_path = NULL;
-
-static LogicalRepInfo *dbinfo;
-static int	num_dbs = 0;
+static LogicalRepPerdbInfoArr dbarr;
+static PrimaryInfo primary;
+static StandbyInfo standby;
 
 enum WaitPMResult
 {
@@ -111,6 +131,30 @@ enum WaitPMResult
 	POSTMASTER_FAILED
 };
 
+/*
+ * Build the replication slot name. The name must not exceed
+ * NAMEDATALEN - 1. This current schema uses a maximum of 42
+ * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+ * probability of collision. By default, subscription name is used as
+ * replication slot name.
+ */
+static inline void
+get_subscription_name(Oid oid, int pid, char *subname, Size szsub)
+{
+	snprintf(subname, szsub, "pg_createsubscriber_%u_%d", oid, pid);
+}
+
+/*
+ * Build the publication name. The name must not exceed NAMEDATALEN -
+ * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+ * '\0').
+ */
+static inline void
+get_publication_name(Oid oid, char *pubname, Size szpub)
+{
+	snprintf(pubname, szpub, "pg_createsubscriber_%u", oid);
+}
+
 
 /*
  * Cleanup objects that were created by pg_createsubscriber if there is an error.
@@ -128,33 +172,54 @@ cleanup_objects_atexit(void)
 	if (success)
 		return;
 
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
-		if (dbinfo[i].made_subscription)
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
+
+		if (perdb->made_subscription)
 		{
-			conn = connect_database(dbinfo[i].subconninfo);
+			conn = connect_database(standby.base_conninfo, perdb->dbname);
 			if (conn != NULL)
 			{
-				drop_subscription(conn, &dbinfo[i]);
-				if (dbinfo[i].made_publication)
-					drop_publication(conn, &dbinfo[i]);
+				drop_subscription(conn, perdb);
 				disconnect_database(conn);
 			}
 		}
 
-		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		if (perdb->made_publication || perdb->made_replslot)
 		{
-			conn = connect_database(dbinfo[i].pubconninfo);
+			conn = connect_database(primary.base_conninfo, perdb->dbname);
 			if (conn != NULL)
 			{
-				if (dbinfo[i].made_publication)
-					drop_publication(conn, &dbinfo[i]);
-				if (dbinfo[i].made_replslot)
-					drop_replication_slot(conn, &dbinfo[i], NULL);
-				disconnect_database(conn);
+				if (perdb->made_publication)
+					drop_publication(conn, perdb);
+				if (perdb->made_replslot)
+				{
+					char replslotname[NAMEDATALEN];
+
+					get_subscription_name(perdb->oid, (int) getpid(),
+										  replslotname, NAMEDATALEN);
+					drop_replication_slot(conn, perdb, replslotname);
+				}
 			}
 		}
 	}
+
+	if (primary.made_transient_replslot)
+	{
+		char transient_replslot[NAMEDATALEN];
+
+		conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
+
+		if (conn != NULL)
+		{
+			snprintf(transient_replslot, NAMEDATALEN, "pg_subscriber_%d_startpoint",
+					 (int) getpid());
+
+			drop_replication_slot(conn, &dbarr.perdb[0], transient_replslot);
+			disconnect_database(conn);
+		}
+	}
 }
 
 static void
@@ -236,15 +301,16 @@ get_base_conninfo(char *conninfo, char *dbname)
 }
 
 /*
- * Get the absolute path from other PostgreSQL binaries (pg_ctl and
- * pg_resetwal) that is used by it.
+ * Get the absolute binary path from another PostgreSQL binary (pg_ctl) and set
+ * to StandbyInfo.
  */
 static bool
-get_exec_path(const char *path)
+get_exec_base_path(const char *path)
 {
-	int			rc;
+	int		rc;
+	char	pg_ctl_path[MAXPGPATH];
+	char   *p;
 
-	pg_ctl_path = pg_malloc(MAXPGPATH);
 	rc = find_other_exec(path, "pg_ctl",
 						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
 						 pg_ctl_path);
@@ -269,30 +335,12 @@ get_exec_path(const char *path)
 
 	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
 
-	pg_resetwal_path = pg_malloc(MAXPGPATH);
-	rc = find_other_exec(path, "pg_resetwal",
-						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
-						 pg_resetwal_path);
-	if (rc < 0)
-	{
-		char		full_path[MAXPGPATH];
-
-		if (find_my_exec(path, full_path) < 0)
-			strlcpy(full_path, progname, sizeof(full_path));
-		if (rc == -1)
-			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
-						 "same directory as \"%s\".\n"
-						 "Check your installation.",
-						 "pg_resetwal", progname, full_path);
-		else
-			pg_log_error("The program \"%s\" was found by \"%s\"\n"
-						 "but was not the same version as %s.\n"
-						 "Check your installation.",
-						 "pg_resetwal", full_path, progname);
-		return false;
-	}
+	/* Extract the directory part from the path */
+	p = strrchr(pg_ctl_path, 'p');
+	Assert(p);
 
-	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+	*p = '\0';
+	standby.bindir = pg_strdup(pg_ctl_path);
 
 	return true;
 }
@@ -356,45 +404,31 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
 }
 
 /*
- * Store publication and subscription information.
+ * Initialize per-db structure and store the name of databases.
  */
-static LogicalRepInfo *
-store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+static void
+store_db_names(LogicalRepPerdbInfo **perdb, int ndbs)
 {
-	LogicalRepInfo *dbinfo;
-	SimpleStringListCell *cell;
-	int			i = 0;
+	SimpleStringListCell   *cell;
+	int						i = 0;
 
-	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+	dbarr.perdb = (LogicalRepPerdbInfo *) pg_malloc0(ndbs *
+											   sizeof(LogicalRepPerdbInfo));
 
 	for (cell = database_names.head; cell; cell = cell->next)
 	{
-		char	   *conninfo;
-
-		/* Publisher. */
-		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
-		dbinfo[i].pubconninfo = conninfo;
-		dbinfo[i].dbname = cell->val;
-		dbinfo[i].made_replslot = false;
-		dbinfo[i].made_publication = false;
-		dbinfo[i].made_subscription = false;
-		/* other struct fields will be filled later. */
-
-		/* Subscriber. */
-		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
-		dbinfo[i].subconninfo = conninfo;
-
+		(*perdb)[i].dbname = pg_strdup(cell->val);
 		i++;
 	}
-
-	return dbinfo;
 }
 
 static PGconn *
-connect_database(const char *conninfo)
+connect_database(const char *base_conninfo, const char*dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
+	char	   *conninfo = concat_conninfo_dbname(base_conninfo,
+														 dbname);
 
 	conn = PQconnectdb(conninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
@@ -412,6 +446,7 @@ connect_database(const char *conninfo)
 	}
 	PQclear(res);
 
+	pfree(conninfo);
 	return conn;
 }
 
@@ -429,11 +464,10 @@ disconnect_database(PGconn *conn)
  * worker.
  */
 static char *
-get_primary_conninfo(const char *base_conninfo)
+get_primary_conninfo(StandbyInfo *standby)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	char	   *conninfo;
 	char	   *primaryconninfo;
 
 	pg_log_info("getting primary_conninfo from standby");
@@ -443,9 +477,7 @@ get_primary_conninfo(const char *base_conninfo)
 	 * not stored infomation yet, we must directly get the first element of the
 	 * database list.
 	 */
-	conninfo = concat_conninfo_dbname(base_conninfo, database_names.head->val);
-
-	conn = connect_database(conninfo);
+	conn = connect_database(standby->base_conninfo, database_names.head->val);
 	if (conn == NULL)
 		exit(1);
 
@@ -468,7 +500,6 @@ get_primary_conninfo(const char *base_conninfo)
 		exit(1);
 	}
 
-	pg_free(conninfo);
 	PQclear(res);
 	disconnect_database(conn);
 
@@ -476,19 +507,18 @@ get_primary_conninfo(const char *base_conninfo)
 }
 
 /*
- * Obtain the system identifier using the provided connection. It will be used
- * to compare if a data directory is a clone of another one.
+ * Obtain the system identifier from the primary server. It will be used to
+ * compare if a data directory is a clone of another one.
  */
-static uint64
-get_sysid_from_conn(const char *conninfo)
+static void
+get_sysid_for_primary(PrimaryInfo *primary, char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from publisher");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(primary->base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -511,13 +541,12 @@ get_sysid_from_conn(const char *conninfo)
 		exit(1);
 	}
 
-	sysid = strtou64(PQgetvalue(res, 0, 2), NULL, 10);
+	primary->sysid = strtou64(PQgetvalue(res, 0, 2), NULL, 10);
 
-	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) primary->sysid);
 
 	disconnect_database(conn);
-
-	return sysid;
 }
 
 /*
@@ -525,29 +554,26 @@ get_sysid_from_conn(const char *conninfo)
  * if a data directory is a clone of another one. This routine is used locally
  * and avoids a connection establishment.
  */
-static uint64
-get_control_from_datadir(const char *datadir)
+static void
+get_sysid_for_standby(StandbyInfo *standby)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from subscriber");
 
-	cf = get_controlfile(datadir, &crc_ok);
+	cf = get_controlfile(standby->pgdata, &crc_ok);
 	if (!crc_ok)
 	{
 		pg_log_error("control file appears to be corrupt");
 		exit(1);
 	}
 
-	sysid = cf->system_identifier;
+	standby->sysid = cf->system_identifier;
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) standby->sysid);
 
 	pfree(cf);
-
-	return sysid;
 }
 
 /*
@@ -556,7 +582,7 @@ get_control_from_datadir(const char *datadir)
  * files from one of the systems might be used in the other one.
  */
 static void
-modify_sysid(const char *pg_resetwal_path, const char *datadir)
+modify_sysid(const char *bindir, const char *datadir)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
@@ -590,7 +616,7 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
 
 	pg_log_info("running pg_resetwal on the subscriber");
 
-	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\"", bindir, datadir);
 
 	pg_log_debug("command is: %s", cmd_str);
 
@@ -609,17 +635,16 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
  * replication.
  */
 static bool
-setup_publisher(LogicalRepInfo *dbinfo)
+setup_publisher(PrimaryInfo *primary, LogicalRepPerdbInfoArr *dbarr)
 {
 	PGconn	   *conn;
 	PGresult   *res;
 
-	for (int i = 0; i < num_dbs; i++)
+	for (int i = 0; i < dbarr->ndbs; i++)
 	{
-		char		pubname[NAMEDATALEN];
-		char		replslotname[NAMEDATALEN];
+		LogicalRepPerdbInfo *perdb = &dbarr->perdb[i];
 
-		conn = connect_database(dbinfo[i].pubconninfo);
+		conn = connect_database(primary->base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
@@ -639,43 +664,20 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		}
 
 		/* Remember database OID. */
-		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		perdb->oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
 
 		PQclear(res);
 
-		/*
-		 * Build the publication name. The name must not exceed NAMEDATALEN -
-		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
-		 * '\0').
-		 */
-		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
-		dbinfo[i].pubname = pg_strdup(pubname);
-
 		/*
 		 * Create publication on publisher. This step should be executed
 		 * *before* promoting the subscriber to avoid any transactions between
 		 * consistent LSN and the new publication rows (such transactions
 		 * wouldn't see the new publication rows resulting in an error).
 		 */
-		create_publication(conn, &dbinfo[i]);
-
-		/*
-		 * Build the replication slot name. The name must not exceed
-		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
-		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
-		 * probability of collision. By default, subscription name is used as
-		 * replication slot name.
-		 */
-		snprintf(replslotname, sizeof(replslotname),
-				 "pg_createsubscriber_%u_%d",
-				 dbinfo[i].oid,
-				 (int) getpid());
-		dbinfo[i].subname = pg_strdup(replslotname);
+		create_publication(conn, primary, perdb);
 
 		/* Create replication slot on publisher. */
-		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL)
-			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
-		else
+		if (create_logical_replication_slot(conn, false, perdb) == NULL)
 			return false;
 
 		disconnect_database(conn);
@@ -688,7 +690,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
  * Is the primary server ready for logical replication?
  */
 static bool
-check_publisher(LogicalRepInfo *dbinfo)
+check_publisher(PrimaryInfo *primary, LogicalRepPerdbInfoArr *dbarr)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -711,7 +713,7 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * max_replication_slots >= current + number of dbs to be converted
 	 * max_wal_senders >= current + number of dbs to be converted
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary->base_conninfo, dbarr->perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -749,10 +751,11 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * use after the transformation, hence, it will be removed at the end of
 	 * this process.
 	 */
-	if (primary_slot_name)
+	if (standby.primary_slot_name)
 	{
 		appendPQExpBuffer(str,
-						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'",
+						  standby.primary_slot_name);
 
 		pg_log_debug("command is: %s", str->data);
 
@@ -767,13 +770,14 @@ check_publisher(LogicalRepInfo *dbinfo)
 		{
 			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
 						 PQntuples(res), 1);
-			pg_free(primary_slot_name); /* it is not being used. */
-			primary_slot_name = NULL;
+			pg_free(standby.primary_slot_name); /* it is not being used. */
+			standby.primary_slot_name = NULL;
 			return false;
 		}
 		else
 		{
-			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+			pg_log_info("primary has replication slot \"%s\"",
+						standby.primary_slot_name);
 		}
 
 		PQclear(res);
@@ -787,17 +791,21 @@ check_publisher(LogicalRepInfo *dbinfo)
 		return false;
 	}
 
-	if (max_repslots - cur_repslots < num_dbs)
+	if (max_repslots - cur_repslots < dbarr->ndbs)
 	{
-		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 dbarr->ndbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + dbarr->ndbs);
 		return false;
 	}
 
-	if (max_walsenders - cur_walsenders < num_dbs)
+	if (max_walsenders - cur_walsenders < dbarr->ndbs)
 	{
-		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
-		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 dbarr->ndbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + dbarr->ndbs);
 		return false;
 	}
 
@@ -808,7 +816,7 @@ check_publisher(LogicalRepInfo *dbinfo)
  * Is the standby server ready for logical replication?
  */
 static bool
-check_subscriber(LogicalRepInfo *dbinfo)
+check_subscriber(StandbyInfo *standby, LogicalRepPerdbInfoArr *dbarr)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -828,7 +836,7 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	 * max_logical_replication_workers >= number of dbs to be converted
 	 * max_worker_processes >= 1 + number of dbs to be converted
 	 */
-	conn = connect_database(dbinfo[0].subconninfo);
+	conn = connect_database(standby->base_conninfo, dbarr->perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -845,35 +853,41 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	max_repslots = atoi(PQgetvalue(res, 1, 0));
 	max_wprocs = atoi(PQgetvalue(res, 2, 0));
 	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
-		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+		standby->primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
 
 	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
 	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
 	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
-	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+	pg_log_debug("subscriber: primary_slot_name: %s", standby->primary_slot_name);
 
 	PQclear(res);
 
 	disconnect_database(conn);
 
-	if (max_repslots < num_dbs)
+	if (max_repslots < dbarr->ndbs)
 	{
-		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 dbarr->ndbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  dbarr->ndbs);
 		return false;
 	}
 
-	if (max_lrworkers < num_dbs)
+	if (max_lrworkers < dbarr->ndbs)
 	{
-		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
-		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 dbarr->ndbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  dbarr->ndbs);
 		return false;
 	}
 
-	if (max_wprocs < num_dbs + 1)
+	if (max_wprocs < dbarr->ndbs + 1)
 	{
-		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
-		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 dbarr->ndbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  dbarr->ndbs + 1);
 		return false;
 	}
 
@@ -885,14 +899,17 @@ check_subscriber(LogicalRepInfo *dbinfo)
  * enable the subscriptions. That's the last step for logical repliation setup.
  */
 static bool
-setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+setup_subscriber(StandbyInfo *standby, PrimaryInfo *primary,
+				 LogicalRepPerdbInfoArr *dbarr, const char *consistent_lsn)
 {
 	PGconn	   *conn;
 
-	for (int i = 0; i < num_dbs; i++)
+	for (int i = 0; i < dbarr->ndbs; i++)
 	{
+		LogicalRepPerdbInfo *perdb = &dbarr->perdb[i];
+
 		/* Connect to subscriber. */
-		conn = connect_database(dbinfo[i].subconninfo);
+		conn = connect_database(standby->base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
@@ -901,15 +918,15 @@ setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
 		 * available on the subscriber when the physical replica is promoted.
 		 * Remove publications from the subscriber because it has no use.
 		 */
-		drop_publication(conn, &dbinfo[i]);
+		drop_publication(conn, perdb);
 
-		create_subscription(conn, &dbinfo[i]);
+		create_subscription(conn, standby, primary, perdb);
 
 		/* Set the replication progress to the correct LSN. */
-		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+		set_replication_progress(conn, perdb, consistent_lsn);
 
 		/* Enable subscription. */
-		enable_subscription(conn, &dbinfo[i]);
+		enable_subscription(conn, perdb);
 
 		disconnect_database(conn);
 	}
@@ -925,45 +942,55 @@ setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
  * result set that contains the consistent LSN.
  */
 static char *
-create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-								char *slot_name)
+create_logical_replication_slot(PGconn *conn, bool temporary,
+								LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res = NULL;
 	char	   *lsn = NULL;
-	bool		transient_replslot = false;
+	char		slot_name[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
 	/*
-	 * If no slot name is informed, it is a transient replication slot used
-	 * only for catch up purposes.
+	 * Construct a name of logical replication slot. The formatting is
+	 * different depends on its persistency.
+	 *
+	 * For persistent slots: the name must be same as the subscription.
+	 * For temporary slots: OID is not needed, but another string is added.
 	 */
-	if (slot_name[0] == '\0')
-	{
-		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+	if (temporary)
+		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
 				 (int) getpid());
-		transient_replslot = true;
-	}
+	else
+		get_subscription_name(perdb->oid, (int) getpid(), slot_name,
+							  NAMEDATALEN);
 
-	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "SELECT * FROM pg_create_logical_replication_slot('%s', 'pgoutput', %s, false, false);",
-					  slot_name, transient_replslot ? "true" : "false");
+					  slot_name, temporary ? "true" : "false");
 
 	pg_log_debug("command is: %s", str->data);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						PQresultErrorMessage(res));
+		pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+					 slot_name, perdb->dbname, PQresultErrorMessage(res));
+
 		return lsn;
 	}
 
+	pg_log_info("create replication slot \"%s\" on publisher", slot_name);
+
 	/* for cleanup purposes */
-	if (!transient_replslot)
-		dbinfo->made_replslot = true;
+	if (temporary)
+		primary.made_transient_replslot = true;
+	else
+		perdb->made_replslot = true;
+
 
 	lsn = pg_strdup(PQgetvalue(res, 0, 1));
 	PQclear(res);
@@ -974,14 +1001,16 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 }
 
 static void
-drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+					  const char *slot_name)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "SELECT * FROM pg_drop_replication_slot('%s');",
 					  slot_name);
@@ -990,8 +1019,9 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						PQerrorMessage(conn));
+		pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+					 slot_name, perdb->dbname,
+					 PQerrorMessage(conn));
 
 	PQclear(res);
 
@@ -1026,23 +1056,25 @@ server_logfile_name(const char *datadir)
 }
 
 static void
-start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile)
+start_standby_server(StandbyInfo *standby)
 {
 	char	   *pg_ctl_cmd;
 	int			rc;
 
-	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, datadir, logfile);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"",
+						  standby->bindir, standby->pgdata, standby->server_log);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 1);
 }
 
 static void
-stop_standby_server(const char *pg_ctl_path, const char *datadir)
+stop_standby_server(StandbyInfo *standby)
 {
 	char	   *pg_ctl_cmd;
 	int			rc;
 
-	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, datadir);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", standby->bindir,
+						  standby->pgdata);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 0);
 }
@@ -1091,7 +1123,7 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo)
+wait_for_end_recovery(StandbyInfo *standby, const char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -1100,7 +1132,7 @@ wait_for_end_recovery(const char *conninfo)
 
 	pg_log_info("waiting the postmaster to reach the consistent state");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(standby->base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -1139,7 +1171,7 @@ wait_for_end_recovery(const char *conninfo)
 		if (recovery_timeout > 0 && timer >= recovery_timeout)
 		{
 			pg_log_error("recovery timed out");
-			stop_standby_server(pg_ctl_path, subscriber_dir);
+			stop_standby_server(standby);
 			exit(1);
 		}
 
@@ -1164,17 +1196,21 @@ wait_for_end_recovery(const char *conninfo)
  * Create a publication that includes all tables in the database.
  */
 static void
-create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+create_publication(PGconn *conn, PrimaryInfo *primary,
+				   LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
 	/* Check if the publication needs to be created. */
 	appendPQExpBuffer(str,
 					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
-					  dbinfo->pubname);
+					  pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
@@ -1194,7 +1230,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		 */
 		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
 		{
-			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			pg_log_info("publication \"%s\" already exists", pubname);
 			return;
 		}
 		else
@@ -1207,7 +1243,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 			 * exact database oid in which puballtables is false.
 			 */
 			pg_log_error("publication \"%s\" does not replicate changes for all tables",
-						 dbinfo->pubname);
+						 pubname);
 			pg_log_error_hint("Consider renaming this publication.");
 			PQclear(res);
 			PQfinish(conn);
@@ -1218,9 +1254,9 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	PQclear(res);
 	resetPQExpBuffer(str);
 
-	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("creating publication \"%s\" on database \"%s\"", pubname, perdb->dbname);
 
-	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1228,13 +1264,13 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (PQresultStatus(res) != PGRES_COMMAND_OK)
 	{
 		pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
-						dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+					 pubname, perdb->dbname, PQerrorMessage(conn));;
 		PQfinish(conn);
 		exit(1);
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_publication = true;
+	perdb->made_publication = true;
 
 	PQclear(res);
 	destroyPQExpBuffer(str);
@@ -1244,25 +1280,29 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove publication if it couldn't finish all steps.
  */
 static void
-drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				pubname, perdb->dbname);
 
-	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+					 pubname, perdb->dbname, PQerrorMessage(conn));
 
 	PQclear(res);
-
 	destroyPQExpBuffer(str);
 }
 
@@ -1279,19 +1319,30 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * initial location.
  */
 static void
-create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+create_subscription(PGconn *conn, StandbyInfo *standby,
+					PrimaryInfo *primary,
+					LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
 					  "WITH (create_slot = false, copy_data = false, enabled = false)",
-					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+					  subname,
+					  concat_conninfo_dbname(primary->base_conninfo,
+											 perdb->dbname),
+					  pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1299,13 +1350,13 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (PQresultStatus(res) != PGRES_COMMAND_OK)
 	{
 		pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
-						dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+					 subname, perdb->dbname, PQerrorMessage(conn));
 		PQfinish(conn);
 		exit(1);
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_subscription = true;
+	perdb->made_subscription = true;
 
 	PQclear(res);
 	destroyPQExpBuffer(str);
@@ -1315,22 +1366,27 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove subscription if it couldn't finish all steps.
  */
 static void
-drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
+				subname, perdb->dbname);
 
-	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
+					 subname, perdb->dbname, PQerrorMessage(conn));
 
 	PQclear(res);
 
@@ -1348,18 +1404,23 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * printing purposes.
  */
 static void
-set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb,
+						 const char *lsn)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 	Oid			suboid;
 	char		originname[NAMEDATALEN];
 	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
 	appendPQExpBuffer(str,
-					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'",
+					  subname);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1392,7 +1453,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	PQclear(res);
 
 	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
-				originname, lsnstr, dbinfo->dbname);
+				originname, lsnstr, perdb->dbname);
 
 	resetPQExpBuffer(str);
 	appendPQExpBuffer(str,
@@ -1404,7 +1465,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
 		pg_log_error("could not set replication progress for the subscription \"%s\": %s",
-						dbinfo->subname, PQresultErrorMessage(res));
+					 subname, PQresultErrorMessage(res));
 		PQfinish(conn);
 		exit(1);
 	}
@@ -1421,24 +1482,27 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
  * of this setup.
  */
 static void
-enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
 
-	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_COMMAND_OK)
 	{
-		pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
-						PQerrorMessage(conn));
+		pg_log_error("could not enable subscription \"%s\": %s", subname,
+					 PQerrorMessage(conn));
 		PQfinish(conn);
 		exit(1);
 	}
@@ -1468,16 +1532,10 @@ main(int argc, char **argv)
 	int			option_index;
 
 	char	   *base_dir;
-	char	   *server_start_log;
 	int			len;
 
-	char	   *pub_base_conninfo = NULL;
-	char	   *sub_base_conninfo = NULL;
 	char	   *dbname_conninfo = NULL;
-	char		temp_replslot[NAMEDATALEN] = {0};
 
-	uint64		pub_sysid;
-	uint64		sub_sysid;
 	struct stat statbuf;
 
 	PGconn	   *conn;
@@ -1527,7 +1585,7 @@ main(int argc, char **argv)
 		switch (c)
 		{
 			case 'D':
-				subscriber_dir = pg_strdup(optarg);
+				standby.pgdata = pg_strdup(optarg);
 				break;
 			case 'S':
 				sub_conninfo_str = pg_strdup(optarg);
@@ -1537,7 +1595,7 @@ main(int argc, char **argv)
 				if (!simple_string_list_member(&database_names, optarg))
 				{
 					simple_string_list_append(&database_names, optarg);
-					num_dbs++;
+					dbarr.ndbs++;
 				}
 				break;
 			case 'n':
@@ -1573,7 +1631,7 @@ main(int argc, char **argv)
 	/*
 	 * Required arguments
 	 */
-	if (subscriber_dir == NULL)
+	if (standby.pgdata == NULL)
 	{
 		pg_log_error("no subscriber data directory specified");
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -1586,8 +1644,8 @@ main(int argc, char **argv)
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 		exit(1);
 	}
-	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, dbname_conninfo);
-	if (sub_base_conninfo == NULL)
+	standby.base_conninfo = get_base_conninfo(sub_conninfo_str, dbname_conninfo);
+	if (standby.base_conninfo == NULL)
 		exit(1);
 
 	if (database_names.head == NULL)
@@ -1602,7 +1660,7 @@ main(int argc, char **argv)
 		if (dbname_conninfo)
 		{
 			simple_string_list_append(&database_names, dbname_conninfo);
-			num_dbs++;
+			dbarr.ndbs++;
 
 			pg_log_info("database \"%s\" was extracted from the subscriber connection string",
 						dbname_conninfo);
@@ -1616,20 +1674,20 @@ main(int argc, char **argv)
 	}
 
 	/* Obtain a connection string from the target */
-	pub_base_conninfo = get_primary_conninfo(sub_base_conninfo);
+	primary.base_conninfo = get_primary_conninfo(&standby);
 
 	/*
 	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
 	 */
-	if (!get_exec_path(argv[0]))
+	if (!get_exec_base_path(argv[0]))
 		exit(1);
 
 	/* rudimentary check for a data directory. */
-	if (!check_data_directory(subscriber_dir))
+	if (!check_data_directory(standby.pgdata))
 		exit(1);
 
-	/* Store database information for publisher and subscriber. */
-	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+	/* Store database information to dbarr */
+	store_db_names(&dbarr.perdb, dbarr.ndbs);
 
 	/* Register a function to clean up objects in case of failure. */
 	atexit(cleanup_objects_atexit);
@@ -1638,9 +1696,9 @@ main(int argc, char **argv)
 	 * Check if the subscriber data directory has the same system identifier
 	 * than the publisher data directory.
 	 */
-	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
-	sub_sysid = get_control_from_datadir(subscriber_dir);
-	if (pub_sysid != sub_sysid)
+	get_sysid_for_primary(&primary, dbarr.perdb[0].dbname);
+	get_sysid_for_standby(&standby);
+	if (primary.sysid != standby.sysid)
 	{
 		pg_log_error("subscriber data directory is not a copy of the source database cluster");
 		exit(1);
@@ -1650,7 +1708,7 @@ main(int argc, char **argv)
 	 * Create the output directory to store any data generated by this tool.
 	 */
 	base_dir = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", standby.pgdata, PGS_OUTPUT_DIR);
 	if (len >= MAXPGPATH)
 	{
 		pg_log_error("directory path for subscriber is too long");
@@ -1663,10 +1721,10 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
-	server_start_log = server_logfile_name(subscriber_dir);
+	standby.server_log = server_logfile_name(standby.pgdata);
 
 	/* subscriber PID file. */
-	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", standby.pgdata);
 
 	/*
 	 * The standby server must be running. That's because some checks will be
@@ -1679,7 +1737,7 @@ main(int argc, char **argv)
 		/*
 		 * Check if the standby server is ready for logical replication.
 		 */
-		if (!check_subscriber(dbinfo))
+		if (!check_subscriber(&standby, &dbarr))
 			exit(1);
 
 		/*
@@ -1688,7 +1746,7 @@ main(int argc, char **argv)
 		 * relies on check_subscriber() to obtain the primary_slot_name.
 		 * That's why it is called after it.
 		 */
-		if (!check_publisher(dbinfo))
+		if (!check_publisher(&primary, &dbarr))
 			exit(1);
 
 		/*
@@ -1697,13 +1755,13 @@ main(int argc, char **argv)
 		 * if the primary slot is in use. We could use an extra connection for
 		 * it but it doesn't seem worth.
 		 */
-		if (!setup_publisher(dbinfo))
+		if (!setup_publisher(&primary, &dbarr))
 			exit(1);
 
 		/* Stop the standby server. */
 		pg_log_info("standby is up and running");
 		pg_log_info("stopping the server to start the transformation steps");
-		stop_standby_server(pg_ctl_path, subscriber_dir);
+		stop_standby_server(&standby);
 	}
 	else
 	{
@@ -1732,11 +1790,10 @@ main(int argc, char **argv)
 	 * consistent LSN but it should be changed after adding pg_basebackup
 	 * support.
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
-	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
-													 temp_replslot);
+	consistent_lsn = create_logical_replication_slot(conn, true, &dbarr.perdb[0]);
 
 	/*
 	 * Write recovery parameters.
@@ -1752,7 +1809,7 @@ main(int argc, char **argv)
 
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						consistent_lsn);
-	WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	WriteRecoveryConfig(conn, standby.pgdata, recoveryconfcontents);
 
 	disconnect_database(conn);
 
@@ -1762,12 +1819,12 @@ main(int argc, char **argv)
 	 * Start subscriber and wait until accepting connections.
 	 */
 	pg_log_info("starting the subscriber");
-	start_standby_server(pg_ctl_path, subscriber_dir, server_start_log);
+	start_standby_server(&standby);
 
 	/*
 	 * Waiting the subscriber to be promoted.
 	 */
-	wait_for_end_recovery(dbinfo[0].subconninfo);
+	wait_for_end_recovery(&standby, dbarr.perdb[0].dbname);
 
 	/*
 	 * Create the subscription for each database on subscriber. It does not
@@ -1776,7 +1833,7 @@ main(int argc, char **argv)
 	 * set_replication_progress). It also cleans up publications created by
 	 * this tool and replication to the standby.
 	 */
-	if (!setup_subscriber(dbinfo, consistent_lsn))
+	if (!setup_subscriber(&standby, &primary, &dbarr, consistent_lsn))
 		exit(1);
 
 	/*
@@ -1785,12 +1842,15 @@ main(int argc, char **argv)
 	 * XXX we might not fail here. Instead, we provide a warning so the user
 	 * eventually drops this replication slot later.
 	 */
-	if (primary_slot_name != NULL)
+	if (standby.primary_slot_name != NULL)
 	{
-		conn = connect_database(dbinfo[0].pubconninfo);
+		char *primary_slot_name = standby.primary_slot_name;
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[0];
+
+		conn = connect_database(primary.base_conninfo, perdb->dbname);
 		if (conn != NULL)
 		{
-			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+			drop_replication_slot(conn, perdb, primary_slot_name);
 		}
 		else
 		{
@@ -1804,12 +1864,12 @@ main(int argc, char **argv)
 	 * Stop the subscriber.
 	 */
 	pg_log_info("stopping the subscriber");
-	stop_standby_server(pg_ctl_path, subscriber_dir);
+	stop_standby_server(&standby);
 
 	/*
 	 * Change system identifier.
 	 */
-	modify_sysid(pg_resetwal_path, subscriber_dir);
+	modify_sysid(standby.bindir, standby.pgdata);
 
 cleanup:
 	/*
@@ -1817,7 +1877,7 @@ cleanup:
 	 * not run successfully. Otherwise, log file is removed.
 	 */
 	if (!retain)
-		unlink(server_start_log);
+		unlink(standby.server_log);
 
 	success = true;
 
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index a9d03acc87..856bf0de3c 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -88,7 +88,7 @@ command_ok(
 		'--pgdata', $node_s->data_dir,
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
-		'--database', 'pg2'
+		'--database', 'pg2', '-r'
 	],
 	'run pg_createsubscriber on node S');
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index f51f1ff23f..3b1ec3fce1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1505,9 +1505,10 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
-LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
+LogicalRepPerdbInfo
+LogicalRepPerdbInfoArr
 LogicalRepPreparedTxnData
 LogicalRepRelId
 LogicalRepRelMapEntry
@@ -1886,6 +1887,7 @@ PREDICATELOCK
 PREDICATELOCKTAG
 PREDICATELOCKTARGET
 PREDICATELOCKTARGETTAG
+PrimaryInfo
 PROCESS_INFORMATION
 PROCLOCK
 PROCLOCKTAG
@@ -2461,6 +2463,7 @@ SQLValueFunctionOp
 SSL
 SSLExtensionInfoContext
 SSL_CTX
+StandbyInfo
 STARTUPINFO
 STRLEN
 SV
-- 
2.43.0

#99Fabrízio de Royes Mello
fabriziomello@gmail.com
In reply to: Peter Eisentraut (#71)
Re: speed up a logical replica setup

On Thu, Jan 18, 2024 at 6:19 AM Peter Eisentraut <peter@eisentraut.org>
wrote:

Very early in this thread, someone mentioned the name
pg_create_subscriber, and of course there is pglogical_create_subscriber
as the historical predecessor. Something along those lines seems better
to me. Maybe there are other ideas.

I've mentioned it upthread because of this pet project [1]https://github.com/fabriziomello/pg_create_subscriber that is one of
the motivations behind upstream this facility.

[1]: https://github.com/fabriziomello/pg_create_subscriber

--
Fabrízio de Royes Mello

#100Fabrízio de Royes Mello
fabriziomello@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#98)
Re: speed up a logical replica setup

On Wed, Jan 31, 2024 at 9:52 AM Hayato Kuroda (Fujitsu) <
kuroda.hayato@fujitsu.com> wrote:

Dear Euler,

I extracted some review comments which may require many efforts. I hope

this makes them

easy to review.

0001: not changed from yours.
0002: avoid to use replication connections. Source: comment #3[1]
0003: Remove -P option and use primary_conninfo instead. Source: [2]
0004: Exit earlier when dry_run is specified. Source: [3]
0005: Refactor data structures. Source: [4]

[1]:

/messages/by-id/TY3PR01MB9889593399165B9A04106741F5662@TY3PR01MB9889.jpnprd01.prod.outlook.com

[2]:

/messages/by-id/TY3PR01MB98897C85700C6DF942D2D0A3F5792@TY3PR01MB9889.jpnprd01.prod.outlook.com

[3]:

/messages/by-id/TY3PR01MB98897C85700C6DF942D2D0A3F5792@TY3PR01MB9889.jpnprd01.prod.outlook.com

[4]:

/messages/by-id/TY3PR01MB9889C362FF76102C88FA1C29F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com

Hey folks,

Jumping into this a bit late here... I'm trying a simple
pg_createsubscriber but getting an error:

~/pgsql took 19s
✦ ➜ pg_createsubscriber -d fabrizio -r -D /tmp/replica5434 -S 'host=/tmp
port=5434'
pg_createsubscriber: error: could not create subscription
"pg_createsubscriber_16384_695617" on database "fabrizio": ERROR: syntax
error at or near "/"
LINE 1: ..._16384_695617 CONNECTION 'user=fabrizio passfile='/home/fabr...
^
pg_createsubscriber: error: could not drop replication slot
"pg_createsubscriber_16384_695617" on database "fabrizio":
pg_createsubscriber: error: could not drop replication slot
"pg_subscriber_695617_startpoint" on database "fabrizio": ERROR:
replication slot "pg_subscriber_695617_startpoint" does not exist

And the LOG contains the following:

~/pgsql took 12s
✦ ➜ cat
/tmp/replica5434/pg_createsubscriber_output.d/server_start_20240131T110318.730.log

2024-01-31 11:03:19.138 -03 [695632] LOG: starting PostgreSQL 17devel on
x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0,
64-bit
2024-01-31 11:03:19.138 -03 [695632] LOG: listening on IPv6 address "::1",
port 5434
2024-01-31 11:03:19.138 -03 [695632] LOG: listening on IPv4 address
"127.0.0.1", port 5434
2024-01-31 11:03:19.158 -03 [695632] LOG: listening on Unix socket
"/tmp/.s.PGSQL.5434"
2024-01-31 11:03:19.179 -03 [695645] LOG: database system was shut down in
recovery at 2024-01-31 11:03:18 -03
2024-01-31 11:03:19.180 -03 [695645] LOG: entering standby mode
2024-01-31 11:03:19.192 -03 [695645] LOG: redo starts at 0/4000028
2024-01-31 11:03:19.198 -03 [695645] LOG: consistent recovery state
reached at 0/504DB08
2024-01-31 11:03:19.198 -03 [695645] LOG: invalid record length at
0/504DB08: expected at least 24, got 0
2024-01-31 11:03:19.198 -03 [695632] LOG: database system is ready to
accept read-only connections
2024-01-31 11:03:19.215 -03 [695646] LOG: started streaming WAL from
primary at 0/5000000 on timeline 1
2024-01-31 11:03:29.587 -03 [695645] LOG: recovery stopping after WAL
location (LSN) "0/504F260"
2024-01-31 11:03:29.587 -03 [695645] LOG: redo done at 0/504F260 system
usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 10.39 s
2024-01-31 11:03:29.587 -03 [695645] LOG: last completed transaction was
at log time 2024-01-31 11:03:18.761544-03
2024-01-31 11:03:29.587 -03 [695646] FATAL: terminating walreceiver
process due to administrator command
2024-01-31 11:03:29.598 -03 [695645] LOG: selected new timeline ID: 2
2024-01-31 11:03:29.680 -03 [695645] LOG: archive recovery complete
2024-01-31 11:03:29.690 -03 [695643] LOG: checkpoint starting:
end-of-recovery immediate wait
2024-01-31 11:03:29.795 -03 [695643] LOG: checkpoint complete: wrote 51
buffers (0.3%); 0 WAL file(s) added, 0 removed, 1 recycled; write=0.021 s,
sync=0.034 s, total=0.115 s; sync files=17, longest=0.011 s, average=0.002
s; distance=16700 kB, estimate=16700 kB; lsn=0/504F298, redo lsn=0/504F298
2024-01-31 11:03:29.805 -03 [695632] LOG: database system is ready to
accept connections
2024-01-31 11:03:30.332 -03 [695658] ERROR: syntax error at or near "/" at
character 90
2024-01-31 11:03:30.332 -03 [695658] STATEMENT: CREATE SUBSCRIPTION
pg_createsubscriber_16384_695617 CONNECTION 'user=fabrizio
passfile='/home/fabrizio/.pgpass' channel_binding=prefer host=localhost
port=5432 sslmode=prefer sslcompression=0 sslcertmode=allow sslsni=1
ssl_min_protocol_version=TLSv1.2 gssencmode=disable krbsrvname=postgres
gssdelegation=0 target_session_attrs=any load_balance_hosts=disable
dbname=fabrizio' PUBLICATION pg_createsubscriber_16384 WITH (create_slot =
false, copy_data = false, enabled = false)

Seems we need to escape connection params similar we do in dblink [1]https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=contrib/dblink/dblink.c;h=19a362526d21dff5d8b1cdc68b15afebe7d40249;hb=HEAD#l2882

Regards,

[1]: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=contrib/dblink/dblink.c;h=19a362526d21dff5d8b1cdc68b15afebe7d40249;hb=HEAD#l2882
https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=contrib/dblink/dblink.c;h=19a362526d21dff5d8b1cdc68b15afebe7d40249;hb=HEAD#l2882

--
Fabrízio de Royes Mello

#101Euler Taveira
euler@eulerto.com
In reply to: Fabrízio de Royes Mello (#100)
Re: speed up a logical replica setup

On Wed, Jan 31, 2024, at 11:25 AM, Fabrízio de Royes Mello wrote:

Jumping into this a bit late here... I'm trying a simple pg_createsubscriber but getting an error:

Try v11. It seems v12-0002 is not correct.

Seems we need to escape connection params similar we do in dblink [1]

I think it is a consequence of v12-0003. I didn't review v12 yet but although I
have added a comment saying it might be possible to use primary_conninfo, I'm
not 100% convinced that's the right direction.

/*
* TODO use primary_conninfo (if available) from subscriber and
* extract publisher connection string. Assume that there are
* identical entries for physical and logical replication. If there is
* not, we would fail anyway.
*/

--
Euler Taveira
EDB https://www.enterprisedb.com/

#102Fabrízio de Royes Mello
fabriziomello@gmail.com
In reply to: Euler Taveira (#101)
1 attachment(s)
Re: speed up a logical replica setup

On Wed, Jan 31, 2024 at 11:35 AM Euler Taveira <euler@eulerto.com> wrote:

On Wed, Jan 31, 2024, at 11:25 AM, Fabrízio de Royes Mello wrote:

Jumping into this a bit late here... I'm trying a simple

pg_createsubscriber but getting an error:

Try v11. It seems v12-0002 is not correct.

Using v11 I'm getting this error:

~/pgsql took 22s
✦ ➜ pg_createsubscriber -d fabrizio -r -D /tmp/replica5434 -S 'host=/tmp
port=5434' -P 'host=/tmp port=5432'
NOTICE: changed the failover state of replication slot
"pg_createsubscriber_16384_706609" on publisher to false
pg_createsubscriber: error: could not drop replication slot
"pg_createsubscriber_706609_startpoint" on database "fabrizio": ERROR:
replication slot "pg_createsubscriber_706609_startpoint" does not exist
Write-ahead log reset

Attached the output log.

Regards,

--
Fabrízio de Royes Mello

Attachments:

server_start_20240131T115324.735.logtext/x-log; charset=US-ASCII; name=server_start_20240131T115324.735.logDownload
#103Euler Taveira
euler@eulerto.com
In reply to: Fabrízio de Royes Mello (#102)
Re: speed up a logical replica setup

On Wed, Jan 31, 2024, at 11:55 AM, Fabrízio de Royes Mello wrote:

On Wed, Jan 31, 2024 at 11:35 AM Euler Taveira <euler@eulerto.com> wrote:

On Wed, Jan 31, 2024, at 11:25 AM, Fabrízio de Royes Mello wrote:

Jumping into this a bit late here... I'm trying a simple pg_createsubscriber but getting an error:

Try v11. It seems v12-0002 is not correct.

Using v11 I'm getting this error:

~/pgsql took 22s
✦ ➜ pg_createsubscriber -d fabrizio -r -D /tmp/replica5434 -S 'host=/tmp port=5434' -P 'host=/tmp port=5432'
NOTICE: changed the failover state of replication slot "pg_createsubscriber_16384_706609" on publisher to false
pg_createsubscriber: error: could not drop replication slot "pg_createsubscriber_706609_startpoint" on database "fabrizio": ERROR: replication slot "pg_createsubscriber_706609_startpoint" does not exist
Write-ahead log reset

Hmm. I didn't try it with the failover patch that was recently applied. Did you
have any special configuration on primary?

--
Euler Taveira
EDB https://www.enterprisedb.com/

#104Fabrízio de Royes Mello
fabriziomello@gmail.com
In reply to: Euler Taveira (#103)
Re: speed up a logical replica setup

On Wed, Jan 31, 2024 at 12:38 PM Euler Taveira <euler@eulerto.com> wrote:

Hmm. I didn't try it with the failover patch that was recently applied.

Did you

have any special configuration on primary?

Nothing special, here the configurations I've changed after bootstrap:

port = '5432'
wal_level = 'logical'
max_wal_senders = '8'
max_replication_slots = '6'
hot_standby_feedback = 'on'
max_prepared_transactions = '10'
max_locks_per_transaction = '512'

Regards,

--
Fabrízio de Royes Mello

#105Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#97)
Re: speed up a logical replica setup

On Tue, Jan 30, 2024, at 6:26 AM, Hayato Kuroda (Fujitsu) wrote:

One open item that is worrying me is how to handle the pg_ctl timeout. This
patch does nothing and the user should use PGCTLTIMEOUT environment variable to
avoid that the execution is canceled after 60 seconds (default for pg_ctl).
Even if you set a high value, it might not be enough for cases like
time-delayed replica. Maybe pg_ctl should accept no timeout as --timeout
option. I'll include this caveat into the documentation but I'm afraid it is
not sufficient and we should provide a better way to handle this situation.

I felt you might be confused a bit. Even if the recovery_min_apply_delay is set,
e.g., 10h., the pg_ctl can start and stop the server. This is because the
walreceiver serialize changes as soon as they received. The delay is done by the
startup process. There are no unflushed data, so server instance can be turned off.
If you meant the combination of recovery-timeout and time-delayed replica, yes,
it would be likely to occur. But in the case, using like --no-timeout option is
dangerous. I think we should overwrite recovery_min_apply_delay to zero. Thought?

I didn't provide the whole explanation. I'm envisioning the use case that pg_ctl
doesn't reach the consistent state and the timeout is reached (the consequence
is that pg_createsubscriber aborts the execution). It might occur on a busy
server. The probability that it occurs with the current code is low (LSN gap
for recovery is small). Maybe I'm anticipating issues when the base backup
support is added but better to raise concerns during development.

Below part contains my comments for v11-0001. Note that the ordering is random.

Hayato, thanks for reviewing v11.

01. doc
```
<group choice="req">
<arg choice="plain"><option>-D</option> </arg>
<arg choice="plain"><option>--pgdata</option></arg>
</group>
```

According to other documentation like pg_upgrade, we do not write both longer
and shorter options in the synopsis section.

pg_upgrade doesn't but others do like pg_rewind, pg_resetwal, pg_controldata,
pg_checksums. It seems newer tools tend to provide short and long options.

02. doc
```
<para>
<application>pg_createsubscriber</application> takes the publisher and subscriber
connection strings, a cluster directory from a physical replica and a list of
database names and it sets up a new logical replica using the physical
recovery process.
</para>

```

I found that you did not include my suggestion without saying [1]. Do you dislike
the comment or still considering?

Documentation is on my list. I didn't fix the documentation since some design
decisions were changed. I'm still working on it.

03. doc
```
<term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
```

Too many blank after -P.

Fixed.

[documentation related items will be addressed later...]

07. general
I think there are some commenting conversions in PG, but this file breaks it.

It is on my list.

08. general
Some pg_log_error() + exit(1) can be replaced with pg_fatal().

Done. I kept a few pg_log_error() + exit() because there is no
pg_fatal_and_hint() function.

09. LogicalRepInfo
```
char *subconninfo; /* subscription connection string for logical
* replication */
```

As I posted in comment#8[2], I don't think it is "subscription connection". Also,
"for logical replication" is bit misreading because it would not be passed to
workers.

Done.

s/publication/publisher/
s/subscription/subscriber/

10. get_base_conninfo
```
static char *
get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
...
/*
* If --database option is not provided, try to obtain the dbname from
* the publisher conninfo. If dbname parameter is not available, error
* out.
*/

```

I'm not sure getting dbname from the conninfo improves user-experience. I felt
it may trigger an unintended targeting.
(I still think the publisher-server should be removed)

Why not? Unique database is a common setup. It is unintended if you don't
document it accordingly. I'll make sure it is advertised in the --database and
the --publisher-server options.

11. check_data_directory
```
/*
* Is it a cluster directory? These are preliminary checks. It is far from
* making an accurate check. If it is not a clone from the publisher, it will
* eventually fail in a future step.
*/
static bool
check_data_directory(const char *datadir)
```

We shoud also check whether pg_createsubscriber can create a file and a directory.
GetDataDirectoryCreatePerm() verifies it.

Good point. It is included in the next patch.

12. main
```
/* Register a function to clean up objects in case of failure. */
atexit(cleanup_objects_atexit);
```

According to the manpage, callback functions would not be called when it exits
due to signals:

Functions registered using atexit() (and on_exit(3)) are not called if a
process terminates abnormally because of the delivery of a signal.

Do you have a good way to handle the case? One solution is to output created
objects in any log level, but the consideration may be too much. Thought?

Nothing? If you interrupt the execution, there will be objects left behind and
you, as someone that decided to do it, have to clean things up. What do you
expect this tool to do? The documentation will provide some guidance informing
the object name patterns this tool uses and you can check for leftover objects.
Someone can argue that is a valid feature request but IMO it is not one in the
top of the list.

13, main
```
/*
* Create a temporary logical replication slot to get a consistent LSN.
```

Just to clarify - I still think the process exits before here in case of dry run.
In case of pg_resetwal, the process exits before doing actual works like
RewriteControlFile().

Why? Are you suggesting that the dry run mode covers just the verification
part? If so, it is not a dry run mode. I would expect it to run until the end
(or until it accomplish its goal) but *does not* modify data. For pg_resetwal,
the modification is one of the last steps and the other ones (KillFoo
functions) that are skipped modify data. It ends the dry run mode when it
accomplish its goal (obtain the new control data values). If we stop earlier,
some of the additional steps won't be covered by the dry run mode and a failure
can happen but could be detected if you run a few more steps.

14. main
```
* XXX we might not fail here. Instead, we provide a warning so the user
* eventually drops this replication slot later.
```

But there are possibilities to exit(1) in drop_replication_slot(). Is it acceptable?

No, there isn't.

15. wait_for_end_recovery
```
/*
* Bail out after recovery_timeout seconds if this option is set.
*/
if (recovery_timeout > 0 && timer >= recovery_timeout)
```

Hmm, IIUC, it should be enabled by default [3]. Do you have anything in your mind?

Why? See [1]/messages/by-id/b315c7da-7ab1-4014-a2a9-8ab6ae26017c@app.fastmail.com. I prefer the kind mode (always wait until the recovery ends) but
you and Amit are proposing a more aggressive mode. The proposal (-t 60) seems
ok right now, however, if the goal is to provide base backup support in the
future, you certainly should have to add the --recovery-timeout in big clusters
or those with high workloads because base backup is run between replication slot
creation and consistent LSN. Of course, we can change the default when base
backup support is added.

16. main
```
/*
* Create the subscription for each database on subscriber. It does not
* enable it immediately because it needs to adjust the logical
* replication start point to the LSN reported by consistent_lsn (see
* set_replication_progress). It also cleans up publications created by
* this tool and replication to the standby.
*/
if (!setup_subscriber(dbinfo, consistent_lsn))
```

Subscriptions would be created and replication origin would be moved forward here,
but latter one can be done only by the superuser. I felt that this should be
checked in check_subscriber().

Good point. I included a check for pg_create_subscription role and CREATE
privilege on the specified database.

17. main
```
/*
* Change system identifier.
*/
modify_sysid(pg_resetwal_path, subscriber_dir);
```

Even if I executed without -v option, an output from pg_resetwal command appears.
It seems bit strange.

The pg_resetwal is using a printf and there is no prefix that identifies that
message is from pg_resetwal. That's message has been bothering me for a while
so let's send it to /dev/null. I'll include it in the next patch.

RewriteControlFile();
KillExistingXLOG();
KillExistingArchiveStatus();
KillExistingWALSummaries();
WriteEmptyXLOG();

printf(_("Write-ahead log reset\n"));
return 0;

[1]: /messages/by-id/b315c7da-7ab1-4014-a2a9-8ab6ae26017c@app.fastmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

#106Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Fabrízio de Royes Mello (#104)
RE: speed up a logical replica setup

Dear Fabrízio,

Thanks for reporting. I understood that the issue occurred on v11 and v12.
I will try to reproduce and check the reason.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#107Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#105)
RE: speed up a logical replica setup

Dear Euler,

Thanks for giving comments! I want to reply some of them.

I didn't provide the whole explanation. I'm envisioning the use case that pg_ctl
doesn't reach the consistent state and the timeout is reached (the consequence
is that pg_createsubscriber aborts the execution). It might occur on a busy
server. The probability that it occurs with the current code is low (LSN gap
for recovery is small). Maybe I'm anticipating issues when the base backup
support is added but better to raise concerns during development.

Hmm, actually I didn't know the case. Thanks for explanation. I want to see
how you describe on the doc.

pg_upgrade doesn't but others do like pg_rewind, pg_resetwal, pg_controldata,
pg_checksums. It seems newer tools tend to provide short and long options.

Oh, you are right.

Nothing? If you interrupt the execution, there will be objects left behind and
you, as someone that decided to do it, have to clean things up. What do you
expect this tool to do? The documentation will provide some guidance informing
the object name patterns this tool uses and you can check for leftover objects.
Someone can argue that is a valid feature request but IMO it is not one in the
top of the list.

OK, so let's keep current style.

Why? Are you suggesting that the dry run mode covers just the verification
part? If so, it is not a dry run mode. I would expect it to run until the end
(or until it accomplish its goal) but *does not* modify data. For pg_resetwal,
the modification is one of the last steps and the other ones (KillFoo
functions) that are skipped modify data. It ends the dry run mode when it
accomplish its goal (obtain the new control data values). If we stop earlier,
some of the additional steps won't be covered by the dry run mode and a failure
can happen but could be detected if you run a few more steps.

Yes, it was my expectation. I'm still not sure which operations can detect by the
dry_run, but we can keep it for now.

Why? See [1]https://www.postgresql.org/docs/devel/functions-admin.html#FUNCTIONS-REPLICATION. I prefer the kind mode (always wait until the recovery ends) but
you and Amit are proposing a more aggressive mode. The proposal (-t 60) seems
ok right now, however, if the goal is to provide base backup support in the
future, you certainly should have to add the --recovery-timeout in big clusters
or those with high workloads because base backup is run between replication slot
creation and consistent LSN. Of course, we can change the default when base
backup support is added.

Sorry, I was missing your previous post. Let's keep yours.

Good point. I included a check for pg_create_subscription role and CREATE
privilege on the specified database.

Not sure, but can we do the replication origin functions by these privilege?
According to the doc[1]https://www.postgresql.org/docs/devel/functions-admin.html#FUNCTIONS-REPLICATION, these ones seem not to be related.

[1]: https://www.postgresql.org/docs/devel/functions-admin.html#FUNCTIONS-REPLICATION

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#108Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#107)
Re: speed up a logical replica setup

On Wed, Jan 31, 2024, at 11:09 PM, Hayato Kuroda (Fujitsu) wrote:

Why? Are you suggesting that the dry run mode covers just the verification
part? If so, it is not a dry run mode. I would expect it to run until the end
(or until it accomplish its goal) but *does not* modify data. For pg_resetwal,
the modification is one of the last steps and the other ones (KillFoo
functions) that are skipped modify data. It ends the dry run mode when it
accomplish its goal (obtain the new control data values). If we stop earlier,
some of the additional steps won't be covered by the dry run mode and a failure
can happen but could be detected if you run a few more steps.

Yes, it was my expectation. I'm still not sure which operations can detect by the
dry_run, but we can keep it for now.

The main goal is to have information for troubleshooting.

Good point. I included a check for pg_create_subscription role and CREATE
privilege on the specified database.

Not sure, but can we do the replication origin functions by these privilege?
According to the doc[1], these ones seem not to be related.

Hmm. No. :( Better add this check too.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#109Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#106)
1 attachment(s)
RE: speed up a logical replica setup

Dear Fabrízio, Euler,

I think you set the primary_slot_name to the standby server, right?
While reading codes, I found below line in v11-0001.
```
if (primary_slot_name != NULL)
{
conn = connect_database(dbinfo[0].pubconninfo);
if (conn != NULL)
{
drop_replication_slot(conn, &dbinfo[0], temp_replslot);
}
```

Now the temp_replslot is temporary one, so it would be removed automatically.
This function will cause the error: replication slot "pg_createsubscriber_%u_startpoint" does not exist.
Also, the physical slot is still remained on the primary.
In short, "temp_replslot" should be "primary_slot_name".

PSA a script file for reproducing.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

test_0201.shapplication/octet-stream; name=test_0201.shDownload
#110Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#109)
5 attachment(s)
RE: speed up a logical replica setup

Dear Fabrízio, Euler,

I made fix patches to solve reported issues by Fabrízio.

* v13-0001: Same as v11-0001 made by Euler.
* v13-0002: Fixes ERRORs while dropping replication slots [1]/messages/by-id/CAFcNs+rSG9DcEewsoA=85DXhSRh+nyKrrcr64FEDytcZf6QaEQ@mail.gmail.com.
If you want to see codes which we get agreement, please apply until 0002.
=== experimental patches ===
* v13-0003: Avoids to use replication connections. The issue [2]/messages/by-id/CAFcNs+pPtw+y7Be00BK0MBpHhLk2s66tLM286g=k5rew8kUxjg@mail.gmail.com was solved on my env.
* v13-0004: Removes -P option and use primary_conninfo instead.
* v13-0005: Refactors data structures

[1]: /messages/by-id/CAFcNs+rSG9DcEewsoA=85DXhSRh+nyKrrcr64FEDytcZf6QaEQ@mail.gmail.com
[2]: /messages/by-id/CAFcNs+pPtw+y7Be00BK0MBpHhLk2s66tLM286g=k5rew8kUxjg@mail.gmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

v13-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchapplication/octet-stream; name=v13-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchDownload
From f1c8a538404193986881bd420f7502eb4d1052d3 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v13 1/5] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica
into a logical replica. It runs on the target server and should be able
to connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.  Stop the
target server. Change the system identifier from the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  322 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1852 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   44 +
 .../t/041_pg_createsubscriber_standby.pl      |  139 ++
 src/tools/pgindent/typedefs.list              |    1 +
 10 files changed, 2387 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..1c78ff92e0
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,322 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+   <application>pg_createsubscriber</application> takes the publisher and subscriber
+   connection strings, a cluster directory from a physical replica and a list of
+   database names and it sets up a new logical replica using the physical
+   recovery process.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P  <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..478560b3e4
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1852 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publication connection string for logical
+								 * replication */
+	char	   *subconninfo;	/* subscription connection string for logical
+								 * replication */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_sysid_from_conn(const char *conninfo);
+static uint64 get_control_from_datadir(const char *datadir);
+static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *server_logfile_name(const char *datadir);
+static void start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *subscriber_dir = NULL;
+static char *pub_conninfo_str = NULL;
+static char *sub_conninfo_str = NULL;
+static SimpleStringList database_names = {NULL, NULL};
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+static bool retain = false;
+static int	recovery_timeout = 0;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], NULL);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	const char *rconninfo;
+
+	/* logical replication mode */
+	rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_sysid_from_conn(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send replication command \"%s\": %s",
+					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	{
+		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
+					 PQntuples(res), PQnfields(res), 1, 3);
+
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a replication connection.
+ */
+static uint64
+get_control_from_datadir(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_sysid(const char *pg_resetwal_path, const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+	{
+		pg_log_error("control file appears to be corrupt");
+		exit(1);
+	}
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(datadir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_log_error("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical
+	 * max_replication_slots >= current + number of dbs to be converted
+	 * max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical replication and
+ * enable the subscriptions. That's the last step for logical repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
+	if (transient_replslot)
+		appendPQExpBufferStr(str, " TEMPORARY");
+	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (!transient_replslot)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 1));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+static char *
+server_logfile_name(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *filename;
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("log file path is too long");
+		exit(1);
+	}
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain recovery progress");
+			exit(1);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("unexpected result from pg_is_in_recovery function");
+			exit(1);
+		}
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (recovery_timeout > 0 && timer >= recovery_timeout)
+		{
+			pg_log_error("recovery timed out");
+			stop_standby_server(pg_ctl_path, subscriber_dir);
+			exit(1);
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+	{
+		pg_log_error("server did not end recovery");
+		exit(1);
+	}
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+						 PQerrorMessage(conn));
+			PQfinish(conn);
+			exit(1);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	int			c;
+	int			option_index;
+
+	char	   *base_dir;
+	char	   *server_start_log;
+	int			len;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+	char		temp_replslot[NAMEDATALEN] = {0};
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&database_names, optarg))
+				{
+					simple_string_list_append(&database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				retain = true;
+				break;
+			case 't':
+				recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure. */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
+	sub_sysid = get_control_from_datadir(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+	{
+		pg_log_error("subscriber data directory is not a copy of the source database cluster");
+		exit(1);
+	}
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+	{
+		pg_log_error("directory path for subscriber is too long");
+		exit(1);
+	}
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+	{
+		pg_log_error("could not create directory \"%s\": %m", base_dir);
+		exit(1);
+	}
+
+	server_start_log = server_logfile_name(subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server. */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	start_standby_server(pg_ctl_path, subscriber_dir, server_start_log);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, subscriber_dir);
+
+	/*
+	 * Change system identifier.
+	 */
+	modify_sysid(pg_resetwal_path, subscriber_dir);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..0f02b1bfac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..534bc53a76
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91433d439b..f51f1ff23f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1505,6 +1505,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.43.0

v13-0002-Fix-wrong-argument-for-drop_replication_slot.patchapplication/octet-stream; name=v13-0002-Fix-wrong-argument-for-drop_replication_slot.patchDownload
From ae33a892dc743787132156df27eb5f91be2ebc62 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 1 Feb 2024 06:13:16 +0000
Subject: [PATCH v13 2/5] Fix wrong argument for drop_replication_slot()

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 478560b3e4..0c0f31d86d 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -151,7 +151,7 @@ cleanup_objects_atexit(void)
 				if (dbinfo[i].made_publication)
 					drop_publication(conn, &dbinfo[i]);
 				if (dbinfo[i].made_replslot)
-					drop_replication_slot(conn, &dbinfo[i], NULL);
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
 				disconnect_database(conn);
 			}
 		}
@@ -1816,7 +1816,7 @@ main(int argc, char **argv)
 		conn = connect_database(dbinfo[0].pubconninfo);
 		if (conn != NULL)
 		{
-			drop_replication_slot(conn, &dbinfo[0], temp_replslot);
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
 		}
 		else
 		{
-- 
2.43.0

v13-0003-Avoid-to-use-replication-connections.patchapplication/octet-stream; name=v13-0003-Avoid-to-use-replication-connections.patchDownload
From 6afd73774079f4a14406cc025a2d25db959db68d Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 31 Jan 2024 07:39:35 +0000
Subject: [PATCH v13 3/5] Avoid to use replication connections

---
 doc/src/sgml/ref/pg_createsubscriber.sgml   |  1 -
 src/bin/pg_basebackup/pg_createsubscriber.c | 29 +++++++++------------
 2 files changed, 12 insertions(+), 18 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 1c78ff92e0..53b42e6161 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -61,7 +61,6 @@ PostgreSQL documentation
    The <application>pg_createsubscriber</application> should be run at the target
    server. The source server (known as publisher server) should accept logical
    replication connections from the target server (known as subscriber server).
-   The target server should accept local logical replication connection.
   </para>
  </refsect1>
 
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 0c0f31d86d..9c16533458 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -397,12 +397,8 @@ connect_database(const char *conninfo)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	const char *rconninfo;
 
-	/* logical replication mode */
-	rconninfo = psprintf("%s replication=database", conninfo);
-
-	conn = PQconnectdb(rconninfo);
+	conn = PQconnectdb(conninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
 	{
 		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
@@ -446,26 +442,26 @@ get_sysid_from_conn(const char *conninfo)
 	if (conn == NULL)
 		exit(1);
 
-	res = PQexec(conn, "IDENTIFY_SYSTEM");
+	res = PQexec(conn, "SELECT * FROM pg_control_system();");
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not send replication command \"%s\": %s",
+		pg_log_error("could not send command \"%s\": %s",
 					 "IDENTIFY_SYSTEM", PQresultErrorMessage(res));
 		PQclear(res);
 		disconnect_database(conn);
 		exit(1);
 	}
-	if (PQntuples(res) != 1 || PQnfields(res) < 3)
+	if (PQntuples(res) != 1 || PQnfields(res) < 4)
 	{
 		pg_log_error("could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields",
-					 PQntuples(res), PQnfields(res), 1, 3);
+					 PQntuples(res), PQnfields(res), 1, 4);
 
 		PQclear(res);
 		disconnect_database(conn);
 		exit(1);
 	}
 
-	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+	sysid = strtou64(PQgetvalue(res, 0, 2), NULL, 10);
 
 	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
 
@@ -477,7 +473,7 @@ get_sysid_from_conn(const char *conninfo)
 /*
  * Obtain the system identifier from control file. It will be used to compare
  * if a data directory is a clone of another one. This routine is used locally
- * and avoids a replication connection.
+ * and avoids a connection establishment.
  */
 static uint64
 get_control_from_datadir(const char *datadir)
@@ -905,10 +901,8 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 
 	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
 
-	appendPQExpBuffer(str, "CREATE_REPLICATION_SLOT \"%s\"", slot_name);
-	if (transient_replslot)
-		appendPQExpBufferStr(str, " TEMPORARY");
-	appendPQExpBufferStr(str, " LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT");
+	appendPQExpBuffer(str, "SELECT * FROM pg_create_logical_replication_slot('%s', 'pgoutput', %s, false, false);",
+					  slot_name, transient_replslot ? "true" : "false");
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -948,14 +942,15 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 
 	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
 
-	appendPQExpBuffer(str, "DROP_REPLICATION_SLOT \"%s\"", slot_name);
+	appendPQExpBuffer(str, "SELECT * FROM pg_drop_replication_slot('%s');",
+					  slot_name);
 
 	pg_log_debug("command is: %s", str->data);
 
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
 						 PQerrorMessage(conn));
 
-- 
2.43.0

v13-0004-Remove-P-and-use-primary_conninfo-instead.patchapplication/octet-stream; name=v13-0004-Remove-P-and-use-primary_conninfo-instead.patchDownload
From 9106f71fe1fedc8c3b73e7df3e80343fbb2dc7b6 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 31 Jan 2024 09:20:54 +0000
Subject: [PATCH v13 4/5] Remove -P and use primary_conninfo instead

XXX: This may be a problematic when the OS user who started target instance is
not the current OS user and PGPASSWORD environment variable was used for
connecting to the primary server. In this case, the password would not be
written in the primary_conninfo and the PGPASSWORD variable might not be set.
This may lead an connection error. Is this a real issue? Note that using
PGPASSWORD is not recommended.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  17 +--
 src/bin/pg_basebackup/pg_createsubscriber.c   | 105 ++++++++++++------
 .../t/040_pg_createsubscriber.pl              |   8 --
 .../t/041_pg_createsubscriber_standby.pl      |   5 +-
 4 files changed, 71 insertions(+), 64 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 53b42e6161..0abe1f6f28 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -29,11 +29,6 @@ PostgreSQL documentation
      <arg choice="plain"><option>--pgdata</option></arg>
     </group>
     <replaceable>datadir</replaceable>
-    <group choice="req">
-     <arg choice="plain"><option>-P</option></arg>
-     <arg choice="plain"><option>--publisher-server</option></arg>
-    </group>
-    <replaceable>connstr</replaceable>
     <group choice="req">
      <arg choice="plain"><option>-S</option></arg>
      <arg choice="plain"><option>--subscriber-server</option></arg>
@@ -83,16 +78,6 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
-     <varlistentry>
-      <term><option>-P  <replaceable class="parameter">connstr</replaceable></option></term>
-      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
-      <listitem>
-       <para>
-        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry>
       <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
       <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
@@ -304,7 +289,7 @@ PostgreSQL documentation
    To create a logical replica for databases <literal>hr</literal> and
    <literal>finance</literal> from a physical replica at <literal>foo</literal>:
 <screen>
-<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -S "host=localhost" -d hr -d finance</userinput>
 </screen>
   </para>
 
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 9c16533458..02291ba505 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -28,6 +28,7 @@
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
+#include "port.h"
 #include "utils/pidfile.h"
 
 #define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
@@ -51,10 +52,10 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname,
-							   const char *noderole);
+static char *get_base_conninfo(char *conninfo, char *dbname);
 static bool get_exec_path(const char *path);
 static bool check_data_directory(const char *datadir);
+static char *get_primary_conninfo(const char *base_conninfo);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
 static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
 static PGconn *connect_database(const char *conninfo);
@@ -88,7 +89,6 @@ static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static const char *progname;
 
 static char *subscriber_dir = NULL;
-static char *pub_conninfo_str = NULL;
 static char *sub_conninfo_str = NULL;
 static SimpleStringList database_names = {NULL, NULL};
 static char *primary_slot_name = NULL;
@@ -167,7 +167,6 @@ usage(void)
 	printf(_("  %s [OPTION]...\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
-	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
@@ -192,7 +191,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+get_base_conninfo(char *conninfo, char *dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -201,7 +200,7 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 	char	   *ret;
 	int			i;
 
-	pg_log_info("validating connection string on %s", noderole);
+	pg_log_info("validating connection string on subscriber");
 
 	conn_opts = PQconninfoParse(conninfo, &errmsg);
 	if (conn_opts == NULL)
@@ -425,6 +424,58 @@ disconnect_database(PGconn *conn)
 	PQfinish(conn);
 }
 
+/*
+ * Obtain primary_conninfo from the target server. The value would be used for
+ * connecting from the pg_createsubscriber itself and logical replication apply
+ * worker.
+ */
+static char *
+get_primary_conninfo(const char *base_conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	char	   *conninfo;
+	char	   *primaryconninfo;
+
+	pg_log_info("getting primary_conninfo from standby");
+
+	/*
+	 * Construct a connection string to the target instance. Since dbinfo has
+	 * not stored infomation yet, we must directly get the first element of the
+	 * database list.
+	 */
+	conninfo = concat_conninfo_dbname(base_conninfo, database_names.head->val);
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SHOW primary_conninfo;");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send command \"%s\": %s",
+					 "SHOW primary_conninfo;", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	primaryconninfo = pg_strdup(PQgetvalue(res, 0, 0));
+
+	if (strlen(primaryconninfo) == 0)
+	{
+		pg_log_error("primary_conninfo is empty");
+		pg_log_error_hint("Check whether the target server is really a physical standby.");
+		exit(1);
+	}
+
+	pg_free(conninfo);
+	PQclear(res);
+	disconnect_database(conn);
+
+	return primaryconninfo;
+}
+
 /*
  * Obtain the system identifier using the provided connection. It will be used
  * to compare if a data directory is a clone of another one.
@@ -1256,15 +1307,18 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char	   *conninfo;
 
 	Assert(conn != NULL);
 
 	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
 
+	conninfo = escape_single_quotes_ascii(dbinfo->pubconninfo);
+
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
 					  "WITH (create_slot = false, copy_data = false, enabled = false)",
-					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+					  dbinfo->subname, conninfo, dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1286,6 +1340,7 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 		PQclear(res);
 
+	pg_free(conninfo);
 	destroyPQExpBuffer(str);
 }
 
@@ -1452,7 +1507,6 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
-		{"publisher-server", required_argument, NULL, 'P'},
 		{"subscriber-server", required_argument, NULL, 'S'},
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
@@ -1519,7 +1573,7 @@ main(int argc, char **argv)
 	}
 #endif
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+	while ((c = getopt_long(argc, argv, "D:S:d:nrt:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1527,9 +1581,6 @@ main(int argc, char **argv)
 			case 'D':
 				subscriber_dir = pg_strdup(optarg);
 				break;
-			case 'P':
-				pub_conninfo_str = pg_strdup(optarg);
-				break;
 			case 'S':
 				sub_conninfo_str = pg_strdup(optarg);
 				break;
@@ -1581,34 +1632,13 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
-	/*
-	 * Parse connection string. Build a base connection string that might be
-	 * reused by multiple databases.
-	 */
-	if (pub_conninfo_str == NULL)
-	{
-		/*
-		 * TODO use primary_conninfo (if available) from subscriber and
-		 * extract publisher connection string. Assume that there are
-		 * identical entries for physical and logical replication. If there is
-		 * not, we would fail anyway.
-		 */
-		pg_log_error("no publisher connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	pub_base_conninfo = get_base_conninfo(pub_conninfo_str, dbname_conninfo,
-										  "publisher");
-	if (pub_base_conninfo == NULL)
-		exit(1);
-
 	if (sub_conninfo_str == NULL)
 	{
 		pg_log_error("no subscriber connection string specified");
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 		exit(1);
 	}
-	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, NULL, "subscriber");
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, dbname_conninfo);
 	if (sub_base_conninfo == NULL)
 		exit(1);
 
@@ -1618,7 +1648,7 @@ main(int argc, char **argv)
 
 		/*
 		 * If --database option is not provided, try to obtain the dbname from
-		 * the publisher conninfo. If dbname parameter is not available, error
+		 * the subscriber conninfo. If dbname parameter is not available, error
 		 * out.
 		 */
 		if (dbname_conninfo)
@@ -1626,7 +1656,7 @@ main(int argc, char **argv)
 			simple_string_list_append(&database_names, dbname_conninfo);
 			num_dbs++;
 
-			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+			pg_log_info("database \"%s\" was extracted from the subscriber connection string",
 						dbname_conninfo);
 		}
 		else
@@ -1637,6 +1667,9 @@ main(int argc, char **argv)
 		}
 	}
 
+	/* Obtain a connection string from the target */
+	pub_base_conninfo = get_primary_conninfo(sub_base_conninfo);
+
 	/*
 	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
 	 */
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 0f02b1bfac..5c240a5417 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -17,18 +17,11 @@ my $datadir = PostgreSQL::Test::Utils::tempdir;
 
 command_fails(['pg_createsubscriber'],
 	'no subscriber data directory specified');
-command_fails(
-	[
-		'pg_createsubscriber',
-		'--pgdata', $datadir
-	],
-	'no publisher connection string specified');
 command_fails(
 	[
 		'pg_createsubscriber',
 		'--dry-run',
 		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres'
 	],
 	'no subscriber connection string specified');
 command_fails(
@@ -36,7 +29,6 @@ command_fails(
 		'pg_createsubscriber',
 		'--verbose',
 		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres',
 		'--subscriber-server', 'dbname=postgres'
 	],
 	'no database name specified');
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 534bc53a76..a9d03acc87 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -56,19 +56,17 @@ command_fails(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_f->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
-	'subscriber data directory is not a copy of the source database cluster');
+	'target database is not a physical standby');
 
 # dry run mode on node S
 command_ok(
 	[
 		'pg_createsubscriber', '--verbose', '--dry-run',
 		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
@@ -88,7 +86,6 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
-- 
2.43.0

v13-0005-Divide-LogicalReplInfo-into-some-strcutures.patchapplication/octet-stream; name=v13-0005-Divide-LogicalReplInfo-into-some-strcutures.patchDownload
From 6ad9ce2ebc5c0fecf9412e66bf7ec2fc62d76213 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 29 Jan 2024 07:03:59 +0000
Subject: [PATCH v13 5/5] Divide LogicalReplInfo into some strcutures

---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 648 ++++++++++--------
 .../t/041_pg_createsubscriber_standby.pl      |   3 +-
 src/tools/pgindent/typedefs.list              |   5 +-
 3 files changed, 351 insertions(+), 305 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 02291ba505..bd55639251 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -33,54 +33,78 @@
 
 #define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
 
-typedef struct LogicalRepInfo
+typedef struct LogicalRepPerdbInfo
 {
-	Oid			oid;			/* database OID */
-	char	   *dbname;			/* database name */
-	char	   *pubconninfo;	/* publication connection string for logical
-								 * replication */
-	char	   *subconninfo;	/* subscription connection string for logical
-								 * replication */
-	char	   *pubname;		/* publication name */
-	char	   *subname;		/* subscription name (also replication slot
-								 * name) */
-
-	bool		made_replslot;	/* replication slot was created */
-	bool		made_publication;	/* publication was created */
-	bool		made_subscription;	/* subscription was created */
-} LogicalRepInfo;
+	Oid		oid;
+	char   *dbname;
+	bool	made_replslot;		/* replication slot was created */
+	bool	made_publication;	/* publication was created */
+	bool	made_subscription; 	/* subscription was created */
+} LogicalRepPerdbInfo;
+
+typedef struct LogicalRepPerdbInfoArr
+{
+	LogicalRepPerdbInfo    *perdb;	/* array of db infos */
+	int						ndbs;	/* number of db infos */
+} LogicalRepPerdbInfoArr;
+
+typedef struct PrimaryInfo
+{
+	char   *base_conninfo;
+	uint64	sysid;
+	bool	made_transient_replslot;
+} PrimaryInfo;
+
+typedef struct StandbyInfo
+{
+	char   *base_conninfo;
+	char   *bindir;
+	char   *pgdata;
+	char   *primary_slot_name;
+	char   *server_log;
+	uint64	sysid;
+} StandbyInfo;
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname);
-static bool get_exec_path(const char *path);
+static bool get_exec_base_path(const char *path);
 static bool check_data_directory(const char *datadir);
-static char *get_primary_conninfo(const char *base_conninfo);
+static char *get_primary_conninfo(StandbyInfo *standby);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
-static LogicalRepInfo *store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo);
-static PGconn *connect_database(const char *conninfo);
+static void store_db_names(LogicalRepPerdbInfo **perdb, int ndbs);
+static PGconn *connect_database(const char *base_conninfo, const char*dbname);
 static void disconnect_database(PGconn *conn);
-static uint64 get_sysid_from_conn(const char *conninfo);
-static uint64 get_control_from_datadir(const char *datadir);
-static void modify_sysid(const char *pg_resetwal_path, const char *datadir);
-static bool check_publisher(LogicalRepInfo *dbinfo);
-static bool setup_publisher(LogicalRepInfo *dbinfo);
-static bool check_subscriber(LogicalRepInfo *dbinfo);
-static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
-static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-											 char *slot_name);
-static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void get_sysid_for_primary(PrimaryInfo *primary, char *dbname);
+static void get_sysid_for_standby(StandbyInfo *standby);
+static void modify_sysid(const char *bindir, const char *datadir);
+static bool check_publisher(PrimaryInfo *primary, LogicalRepPerdbInfoArr *dbarr);
+static bool setup_publisher(PrimaryInfo *primary, LogicalRepPerdbInfoArr *dbarr);
+static bool check_subscriber(StandbyInfo *standby, LogicalRepPerdbInfoArr *dbarr);
+static bool setup_subscriber(StandbyInfo *standby, PrimaryInfo *primary,
+							 LogicalRepPerdbInfoArr *dbarr,
+							 const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, bool temporary,
+											 LogicalRepPerdbInfo *perdb);
+static void drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+								  const char *slot_name);
 static char *server_logfile_name(const char *datadir);
-static void start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile);
-static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void start_standby_server(StandbyInfo *standby);
+static void stop_standby_server(StandbyInfo *standby);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
-static void wait_for_end_recovery(const char *conninfo);
-static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
-static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
-static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+
+static void wait_for_end_recovery(StandbyInfo *standby,
+								  const char *dbname);
+static void create_publication(PGconn *conn, PrimaryInfo *primary,
+							   LogicalRepPerdbInfo *perdb);
+static void drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void create_subscription(PGconn *conn, StandbyInfo *standby,
+								PrimaryInfo *primary,
+								LogicalRepPerdbInfo *perdb);
+static void drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
+static void set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb);
 
 #define	USEC_PER_SEC	1000000
 #define	WAIT_INTERVAL	1		/* 1 second */
@@ -88,21 +112,17 @@ static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 /* Options */
 static const char *progname;
 
-static char *subscriber_dir = NULL;
 static char *sub_conninfo_str = NULL;
 static SimpleStringList database_names = {NULL, NULL};
-static char *primary_slot_name = NULL;
 static bool dry_run = false;
 static bool retain = false;
 static int	recovery_timeout = 0;
 
 static bool success = false;
 
-static char *pg_ctl_path = NULL;
-static char *pg_resetwal_path = NULL;
-
-static LogicalRepInfo *dbinfo;
-static int	num_dbs = 0;
+static LogicalRepPerdbInfoArr dbarr;
+static PrimaryInfo primary;
+static StandbyInfo standby;
 
 enum WaitPMResult
 {
@@ -112,6 +132,30 @@ enum WaitPMResult
 	POSTMASTER_FAILED
 };
 
+/*
+ * Build the replication slot name. The name must not exceed
+ * NAMEDATALEN - 1. This current schema uses a maximum of 42
+ * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+ * probability of collision. By default, subscription name is used as
+ * replication slot name.
+ */
+static inline void
+get_subscription_name(Oid oid, int pid, char *subname, Size szsub)
+{
+	snprintf(subname, szsub, "pg_createsubscriber_%u_%d", oid, pid);
+}
+
+/*
+ * Build the publication name. The name must not exceed NAMEDATALEN -
+ * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+ * '\0').
+ */
+static inline void
+get_publication_name(Oid oid, char *pubname, Size szpub)
+{
+	snprintf(pubname, szpub, "pg_createsubscriber_%u", oid);
+}
+
 
 /*
  * Cleanup objects that were created by pg_createsubscriber if there is an error.
@@ -129,30 +173,35 @@ cleanup_objects_atexit(void)
 	if (success)
 		return;
 
-	for (i = 0; i < num_dbs; i++)
+	for (i = 0; i < dbarr.ndbs; i++)
 	{
-		if (dbinfo[i].made_subscription)
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[i];
+
+		if (perdb->made_subscription)
 		{
-			conn = connect_database(dbinfo[i].subconninfo);
+
+			conn = connect_database(standby.base_conninfo, perdb->dbname);
 			if (conn != NULL)
 			{
-				drop_subscription(conn, &dbinfo[i]);
-				if (dbinfo[i].made_publication)
-					drop_publication(conn, &dbinfo[i]);
+				drop_subscription(conn, perdb);
+
+				if (perdb->made_publication)
+					drop_publication(conn, perdb);
 				disconnect_database(conn);
 			}
 		}
 
-		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		if (perdb->made_publication || perdb->made_replslot)
 		{
-			conn = connect_database(dbinfo[i].pubconninfo);
-			if (conn != NULL)
+			if (perdb->made_publication)
+				drop_publication(conn, perdb);
+			if (perdb->made_replslot)
 			{
-				if (dbinfo[i].made_publication)
-					drop_publication(conn, &dbinfo[i]);
-				if (dbinfo[i].made_replslot)
-					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
-				disconnect_database(conn);
+				char replslotname[NAMEDATALEN];
+
+				get_subscription_name(perdb->oid, (int) getpid(),
+									  replslotname, NAMEDATALEN);
+				drop_replication_slot(conn, perdb, replslotname);
 			}
 		}
 	}
@@ -237,15 +286,16 @@ get_base_conninfo(char *conninfo, char *dbname)
 }
 
 /*
- * Get the absolute path from other PostgreSQL binaries (pg_ctl and
- * pg_resetwal) that is used by it.
+ * Get the absolute binary path from another PostgreSQL binary (pg_ctl) and set
+ * to StandbyInfo.
  */
 static bool
-get_exec_path(const char *path)
+get_exec_base_path(const char *path)
 {
-	int			rc;
+	int		rc;
+	char	pg_ctl_path[MAXPGPATH];
+	char   *p;
 
-	pg_ctl_path = pg_malloc(MAXPGPATH);
 	rc = find_other_exec(path, "pg_ctl",
 						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
 						 pg_ctl_path);
@@ -270,30 +320,12 @@ get_exec_path(const char *path)
 
 	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
 
-	pg_resetwal_path = pg_malloc(MAXPGPATH);
-	rc = find_other_exec(path, "pg_resetwal",
-						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
-						 pg_resetwal_path);
-	if (rc < 0)
-	{
-		char		full_path[MAXPGPATH];
+	/* Extract the directory part from the path */
+	p = strrchr(pg_ctl_path, 'p');
+	Assert(p);
 
-		if (find_my_exec(path, full_path) < 0)
-			strlcpy(full_path, progname, sizeof(full_path));
-		if (rc == -1)
-			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
-						 "same directory as \"%s\".\n"
-						 "Check your installation.",
-						 "pg_resetwal", progname, full_path);
-		else
-			pg_log_error("The program \"%s\" was found by \"%s\"\n"
-						 "but was not the same version as %s.\n"
-						 "Check your installation.",
-						 "pg_resetwal", full_path, progname);
-		return false;
-	}
-
-	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+	*p = '\0';
+	standby.bindir = pg_strdup(pg_ctl_path);
 
 	return true;
 }
@@ -357,45 +389,31 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
 }
 
 /*
- * Store publication and subscription information.
+ * Initialize per-db structure and store the name of databases.
  */
-static LogicalRepInfo *
-store_pub_sub_info(const char *pub_base_conninfo, const char *sub_base_conninfo)
+static void
+store_db_names(LogicalRepPerdbInfo **perdb, int ndbs)
 {
-	LogicalRepInfo *dbinfo;
-	SimpleStringListCell *cell;
-	int			i = 0;
+	SimpleStringListCell   *cell;
+	int						i = 0;
 
-	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+	dbarr.perdb = (LogicalRepPerdbInfo *) pg_malloc0(ndbs *
+											   sizeof(LogicalRepPerdbInfo));
 
 	for (cell = database_names.head; cell; cell = cell->next)
 	{
-		char	   *conninfo;
-
-		/* Publisher. */
-		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
-		dbinfo[i].pubconninfo = conninfo;
-		dbinfo[i].dbname = cell->val;
-		dbinfo[i].made_replslot = false;
-		dbinfo[i].made_publication = false;
-		dbinfo[i].made_subscription = false;
-		/* other struct fields will be filled later. */
-
-		/* Subscriber. */
-		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
-		dbinfo[i].subconninfo = conninfo;
-
+		(*perdb)[i].dbname = pg_strdup(cell->val);
 		i++;
 	}
-
-	return dbinfo;
 }
 
 static PGconn *
-connect_database(const char *conninfo)
+connect_database(const char *base_conninfo, const char*dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
+	char	   *conninfo = concat_conninfo_dbname(base_conninfo,
+														 dbname);
 
 	conn = PQconnectdb(conninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
@@ -413,6 +431,7 @@ connect_database(const char *conninfo)
 	}
 	PQclear(res);
 
+	pfree(conninfo);
 	return conn;
 }
 
@@ -430,11 +449,10 @@ disconnect_database(PGconn *conn)
  * worker.
  */
 static char *
-get_primary_conninfo(const char *base_conninfo)
+get_primary_conninfo(StandbyInfo *standby)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	char	   *conninfo;
 	char	   *primaryconninfo;
 
 	pg_log_info("getting primary_conninfo from standby");
@@ -444,9 +462,7 @@ get_primary_conninfo(const char *base_conninfo)
 	 * not stored infomation yet, we must directly get the first element of the
 	 * database list.
 	 */
-	conninfo = concat_conninfo_dbname(base_conninfo, database_names.head->val);
-
-	conn = connect_database(conninfo);
+	conn = connect_database(standby->base_conninfo, database_names.head->val);
 	if (conn == NULL)
 		exit(1);
 
@@ -469,7 +485,6 @@ get_primary_conninfo(const char *base_conninfo)
 		exit(1);
 	}
 
-	pg_free(conninfo);
 	PQclear(res);
 	disconnect_database(conn);
 
@@ -477,19 +492,18 @@ get_primary_conninfo(const char *base_conninfo)
 }
 
 /*
- * Obtain the system identifier using the provided connection. It will be used
- * to compare if a data directory is a clone of another one.
+ * Obtain the system identifier from the primary server. It will be used to
+ * compare if a data directory is a clone of another one.
  */
-static uint64
-get_sysid_from_conn(const char *conninfo)
+static void
+get_sysid_for_primary(PrimaryInfo *primary, char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from publisher");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(primary->base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -512,13 +526,12 @@ get_sysid_from_conn(const char *conninfo)
 		exit(1);
 	}
 
-	sysid = strtou64(PQgetvalue(res, 0, 2), NULL, 10);
+	primary->sysid = strtou64(PQgetvalue(res, 0, 2), NULL, 10);
 
-	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) primary->sysid);
 
 	disconnect_database(conn);
-
-	return sysid;
 }
 
 /*
@@ -526,29 +539,26 @@ get_sysid_from_conn(const char *conninfo)
  * if a data directory is a clone of another one. This routine is used locally
  * and avoids a connection establishment.
  */
-static uint64
-get_control_from_datadir(const char *datadir)
+static void
+get_sysid_for_standby(StandbyInfo *standby)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
-	uint64		sysid;
 
 	pg_log_info("getting system identifier from subscriber");
 
-	cf = get_controlfile(datadir, &crc_ok);
+	cf = get_controlfile(standby->pgdata, &crc_ok);
 	if (!crc_ok)
 	{
 		pg_log_error("control file appears to be corrupt");
 		exit(1);
 	}
 
-	sysid = cf->system_identifier;
+	standby->sysid = cf->system_identifier;
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) standby->sysid);
 
 	pfree(cf);
-
-	return sysid;
 }
 
 /*
@@ -557,7 +567,7 @@ get_control_from_datadir(const char *datadir)
  * files from one of the systems might be used in the other one.
  */
 static void
-modify_sysid(const char *pg_resetwal_path, const char *datadir)
+modify_sysid(const char *bindir, const char *datadir)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
@@ -592,7 +602,7 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
 
 	pg_log_info("running pg_resetwal on the subscriber");
 
-	cmd_str = psprintf("\"%s\" -D \"%s\"", pg_resetwal_path, datadir);
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\"", bindir, datadir);
 
 	pg_log_debug("command is: %s", cmd_str);
 
@@ -613,17 +623,16 @@ modify_sysid(const char *pg_resetwal_path, const char *datadir)
  * replication.
  */
 static bool
-setup_publisher(LogicalRepInfo *dbinfo)
+setup_publisher(PrimaryInfo *primary, LogicalRepPerdbInfoArr *dbarr)
 {
 	PGconn	   *conn;
 	PGresult   *res;
 
-	for (int i = 0; i < num_dbs; i++)
+	for (int i = 0; i < dbarr->ndbs; i++)
 	{
-		char		pubname[NAMEDATALEN];
-		char		replslotname[NAMEDATALEN];
+		LogicalRepPerdbInfo *perdb = &dbarr->perdb[i];
 
-		conn = connect_database(dbinfo[i].pubconninfo);
+		conn = connect_database(primary->base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
@@ -643,43 +652,20 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		}
 
 		/* Remember database OID. */
-		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		perdb->oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
 
 		PQclear(res);
 
-		/*
-		 * Build the publication name. The name must not exceed NAMEDATALEN -
-		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
-		 * '\0').
-		 */
-		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
-		dbinfo[i].pubname = pg_strdup(pubname);
-
 		/*
 		 * Create publication on publisher. This step should be executed
 		 * *before* promoting the subscriber to avoid any transactions between
 		 * consistent LSN and the new publication rows (such transactions
 		 * wouldn't see the new publication rows resulting in an error).
 		 */
-		create_publication(conn, &dbinfo[i]);
-
-		/*
-		 * Build the replication slot name. The name must not exceed
-		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
-		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
-		 * probability of collision. By default, subscription name is used as
-		 * replication slot name.
-		 */
-		snprintf(replslotname, sizeof(replslotname),
-				 "pg_createsubscriber_%u_%d",
-				 dbinfo[i].oid,
-				 (int) getpid());
-		dbinfo[i].subname = pg_strdup(replslotname);
+		create_publication(conn, primary, perdb);
 
 		/* Create replication slot on publisher. */
-		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
-			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
-		else
+		if (create_logical_replication_slot(conn, false, perdb) == NULL && !dry_run)
 			return false;
 
 		disconnect_database(conn);
@@ -692,7 +678,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
  * Is the primary server ready for logical replication?
  */
 static bool
-check_publisher(LogicalRepInfo *dbinfo)
+check_publisher(PrimaryInfo *primary, LogicalRepPerdbInfoArr *dbarr)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -715,7 +701,7 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * max_replication_slots >= current + number of dbs to be converted
 	 * max_wal_senders >= current + number of dbs to be converted
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary->base_conninfo, dbarr->perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -753,10 +739,11 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * use after the transformation, hence, it will be removed at the end of
 	 * this process.
 	 */
-	if (primary_slot_name)
+	if (standby.primary_slot_name)
 	{
 		appendPQExpBuffer(str,
-						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'",
+						  standby.primary_slot_name);
 
 		pg_log_debug("command is: %s", str->data);
 
@@ -771,13 +758,14 @@ check_publisher(LogicalRepInfo *dbinfo)
 		{
 			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
 						 PQntuples(res), 1);
-			pg_free(primary_slot_name); /* it is not being used. */
-			primary_slot_name = NULL;
+			pg_free(standby.primary_slot_name); /* it is not being used. */
+			standby.primary_slot_name = NULL;
 			return false;
 		}
 		else
 		{
-			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+			pg_log_info("primary has replication slot \"%s\"",
+						standby.primary_slot_name);
 		}
 
 		PQclear(res);
@@ -791,17 +779,21 @@ check_publisher(LogicalRepInfo *dbinfo)
 		return false;
 	}
 
-	if (max_repslots - cur_repslots < num_dbs)
+	if (max_repslots - cur_repslots < dbarr->ndbs)
 	{
-		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 dbarr->ndbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + dbarr->ndbs);
 		return false;
 	}
 
-	if (max_walsenders - cur_walsenders < num_dbs)
+	if (max_walsenders - cur_walsenders < dbarr->ndbs)
 	{
-		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
-		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 dbarr->ndbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + dbarr->ndbs);
 		return false;
 	}
 
@@ -812,7 +804,7 @@ check_publisher(LogicalRepInfo *dbinfo)
  * Is the standby server ready for logical replication?
  */
 static bool
-check_subscriber(LogicalRepInfo *dbinfo)
+check_subscriber(StandbyInfo *standby, LogicalRepPerdbInfoArr *dbarr)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -832,7 +824,7 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	 * max_logical_replication_workers >= number of dbs to be converted
 	 * max_worker_processes >= 1 + number of dbs to be converted
 	 */
-	conn = connect_database(dbinfo[0].subconninfo);
+	conn = connect_database(standby->base_conninfo, dbarr->perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -849,35 +841,41 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	max_repslots = atoi(PQgetvalue(res, 1, 0));
 	max_wprocs = atoi(PQgetvalue(res, 2, 0));
 	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
-		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+		standby->primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
 
 	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
 	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
 	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
-	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+	pg_log_debug("subscriber: primary_slot_name: %s", standby->primary_slot_name);
 
 	PQclear(res);
 
 	disconnect_database(conn);
 
-	if (max_repslots < num_dbs)
+	if (max_repslots < dbarr->ndbs)
 	{
-		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 dbarr->ndbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  dbarr->ndbs);
 		return false;
 	}
 
-	if (max_lrworkers < num_dbs)
+	if (max_lrworkers < dbarr->ndbs)
 	{
-		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
-		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 dbarr->ndbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  dbarr->ndbs);
 		return false;
 	}
 
-	if (max_wprocs < num_dbs + 1)
+	if (max_wprocs < dbarr->ndbs + 1)
 	{
-		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
-		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 dbarr->ndbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  dbarr->ndbs + 1);
 		return false;
 	}
 
@@ -889,14 +887,17 @@ check_subscriber(LogicalRepInfo *dbinfo)
  * enable the subscriptions. That's the last step for logical repliation setup.
  */
 static bool
-setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+setup_subscriber(StandbyInfo *standby, PrimaryInfo *primary,
+				 LogicalRepPerdbInfoArr *dbarr, const char *consistent_lsn)
 {
 	PGconn	   *conn;
 
-	for (int i = 0; i < num_dbs; i++)
+	for (int i = 0; i < dbarr->ndbs; i++)
 	{
+		LogicalRepPerdbInfo *perdb = &dbarr->perdb[i];
+
 		/* Connect to subscriber. */
-		conn = connect_database(dbinfo[i].subconninfo);
+		conn = connect_database(standby->base_conninfo, perdb->dbname);
 		if (conn == NULL)
 			exit(1);
 
@@ -905,15 +906,15 @@ setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
 		 * available on the subscriber when the physical replica is promoted.
 		 * Remove publications from the subscriber because it has no use.
 		 */
-		drop_publication(conn, &dbinfo[i]);
+		drop_publication(conn, perdb);
 
-		create_subscription(conn, &dbinfo[i]);
+		create_subscription(conn, standby, primary, perdb);
 
 		/* Set the replication progress to the correct LSN. */
-		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+		set_replication_progress(conn, perdb, consistent_lsn);
 
 		/* Enable subscription. */
-		enable_subscription(conn, &dbinfo[i]);
+		enable_subscription(conn, perdb);
 
 		disconnect_database(conn);
 	}
@@ -929,31 +930,35 @@ setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
  * result set that contains the consistent LSN.
  */
 static char *
-create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
-								char *slot_name)
+create_logical_replication_slot(PGconn *conn, bool temporary,
+								LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res = NULL;
 	char	   *lsn = NULL;
-	bool		transient_replslot = false;
+	char		slot_name[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
 	/*
-	 * If no slot name is informed, it is a transient replication slot used
-	 * only for catch up purposes.
-	 */
-	if (slot_name[0] == '\0')
-	{
-		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+	 * Construct a name of logical replication slot. The formatting is
+	 * different depends on its persistency.
+	 *
+	 * For persistent slots: the name must be same as the subscription.
+	 * For temporary slots: OID is not needed, but another string is added.
+ 	 */
+	if (temporary)
+		snprintf(slot_name, NAMEDATALEN, "pg_subscriber_%d_startpoint",
 				 (int) getpid());
-		transient_replslot = true;
-	}
+	else
+		get_subscription_name(perdb->oid, (int) getpid(), slot_name,
+							  NAMEDATALEN);
 
-	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "SELECT * FROM pg_create_logical_replication_slot('%s', 'pgoutput', %s, false, false);",
-					  slot_name, transient_replslot ? "true" : "false");
+					  slot_name, temporary ? "true" : "false");
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -962,15 +967,19 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						 PQresultErrorMessage(res));
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, perdb->dbname, PQresultErrorMessage(res));
 			return lsn;
 		}
 	}
 
+	pg_log_info("create replication slot \"%s\" on publisher", slot_name);
+
 	/* for cleanup purposes */
-	if (!transient_replslot)
-		dbinfo->made_replslot = true;
+	if (temporary)
+		primary.made_transient_replslot = true;
+	else
+		perdb->made_replslot = true;
 
 	if (!dry_run)
 	{
@@ -984,14 +993,16 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 }
 
 static void
-drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+drop_replication_slot(PGconn *conn, LogicalRepPerdbInfo *perdb,
+					  const char *slot_name)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, perdb->dbname);
 
 	appendPQExpBuffer(str, "SELECT * FROM pg_drop_replication_slot('%s');",
 					  slot_name);
@@ -1002,8 +1013,8 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
-			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						 PQerrorMessage(conn));
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, perdb->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1039,23 +1050,25 @@ server_logfile_name(const char *datadir)
 }
 
 static void
-start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile)
+start_standby_server(StandbyInfo *standby)
 {
 	char	   *pg_ctl_cmd;
 	int			rc;
 
-	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, datadir, logfile);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"",
+						  standby->bindir, standby->pgdata, standby->server_log);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 1);
 }
 
 static void
-stop_standby_server(const char *pg_ctl_path, const char *datadir)
+stop_standby_server(StandbyInfo *standby)
 {
 	char	   *pg_ctl_cmd;
 	int			rc;
 
-	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, datadir);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", standby->bindir,
+						  standby->pgdata);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 0);
 }
@@ -1104,7 +1117,7 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo)
+wait_for_end_recovery(StandbyInfo *standby, const char *dbname)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -1113,7 +1126,7 @@ wait_for_end_recovery(const char *conninfo)
 
 	pg_log_info("waiting the postmaster to reach the consistent state");
 
-	conn = connect_database(conninfo);
+	conn = connect_database(standby->base_conninfo, dbname);
 	if (conn == NULL)
 		exit(1);
 
@@ -1155,7 +1168,7 @@ wait_for_end_recovery(const char *conninfo)
 		if (recovery_timeout > 0 && timer >= recovery_timeout)
 		{
 			pg_log_error("recovery timed out");
-			stop_standby_server(pg_ctl_path, subscriber_dir);
+			stop_standby_server(standby);
 			exit(1);
 		}
 
@@ -1180,17 +1193,21 @@ wait_for_end_recovery(const char *conninfo)
  * Create a publication that includes all tables in the database.
  */
 static void
-create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+create_publication(PGconn *conn, PrimaryInfo *primary,
+				   LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
 	/* Check if the publication needs to be created. */
 	appendPQExpBuffer(str,
 					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
-					  dbinfo->pubname);
+					  pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
@@ -1210,7 +1227,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		 */
 		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
 		{
-			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			pg_log_info("publication \"%s\" already exists", pubname);
 			return;
 		}
 		else
@@ -1223,7 +1240,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 			 * exact database oid in which puballtables is false.
 			 */
 			pg_log_error("publication \"%s\" does not replicate changes for all tables",
-						 dbinfo->pubname);
+						 pubname);
 			pg_log_error_hint("Consider renaming this publication.");
 			PQclear(res);
 			PQfinish(conn);
@@ -1234,9 +1251,9 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	PQclear(res);
 	resetPQExpBuffer(str);
 
-	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("creating publication \"%s\" on database \"%s\"", pubname, perdb->dbname);
 
-	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1246,14 +1263,14 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
-						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+						 pubname, perdb->dbname, PQerrorMessage(conn));;
 			PQfinish(conn);
 			exit(1);
 		}
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_publication = true;
+	perdb->made_publication = true;
 
 	if (!dry_run)
 		PQclear(res);
@@ -1265,16 +1282,20 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove publication if it couldn't finish all steps.
  */
 static void
-drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_publication(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		pubname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				pubname, perdb->dbname);
 
-	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1282,7 +1303,8 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 pubname, perdb->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1303,22 +1325,32 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
  * initial location.
  */
 static void
-create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+create_subscription(PGconn *conn, StandbyInfo *standby,
+					PrimaryInfo *primary,
+					LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
-	char	   *conninfo;
+	char		subname[NAMEDATALEN];
+	char		pubname[NAMEDATALEN];
+	char	   *escaped_conninfo;
 
 	Assert(conn != NULL);
 
-	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+	get_publication_name(perdb->oid, pubname, NAMEDATALEN);
 
-	conninfo = escape_single_quotes_ascii(dbinfo->pubconninfo);
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
+
+	escaped_conninfo = escape_single_quotes_ascii(primary->base_conninfo);
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
 					  "WITH (create_slot = false, copy_data = false, enabled = false)",
-					  dbinfo->subname, conninfo, dbinfo->pubname);
+					  subname,
+					  concat_conninfo_dbname(escaped_conninfo, perdb->dbname),
+					  pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1328,19 +1360,19 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+						 subname, perdb->dbname, PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
 		}
 	}
 
 	/* for cleanup purposes */
-	dbinfo->made_subscription = true;
+	perdb->made_subscription = true;
 
 	if (!dry_run)
 		PQclear(res);
 
-	pg_free(conninfo);
+	pg_free(escaped_conninfo);
 	destroyPQExpBuffer(str);
 }
 
@@ -1348,16 +1380,20 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * Remove subscription if it couldn't finish all steps.
  */
 static void
-drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+drop_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
+				subname, perdb->dbname);
 
-	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1365,7 +1401,8 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
+						 subname, perdb->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1384,18 +1421,23 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
  * printing purposes.
  */
 static void
-set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+set_replication_progress(PGconn *conn, LogicalRepPerdbInfo *perdb,
+						 const char *lsn)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 	Oid			suboid;
 	char		originname[NAMEDATALEN];
 	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+
 	appendPQExpBuffer(str,
-					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'",
+					  subname);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1436,7 +1478,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	PQclear(res);
 
 	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
-				originname, lsnstr, dbinfo->dbname);
+				originname, lsnstr, perdb->dbname);
 
 	resetPQExpBuffer(str);
 	appendPQExpBuffer(str,
@@ -1450,7 +1492,7 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
 			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
-						 dbinfo->subname, PQresultErrorMessage(res));
+						 subname, PQresultErrorMessage(res));
 			PQfinish(conn);
 			exit(1);
 		}
@@ -1469,16 +1511,20 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
  * of this setup.
  */
 static void
-enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+enable_subscription(PGconn *conn, LogicalRepPerdbInfo *perdb)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char		subname[NAMEDATALEN];
 
 	Assert(conn != NULL);
 
-	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	get_subscription_name(perdb->oid, (int) getpid(), subname, NAMEDATALEN);
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", subname,
+				perdb->dbname);
+	
 
-	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", subname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1487,7 +1533,7 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
-			pg_log_error("could not enable subscription \"%s\": %s", dbinfo->subname,
+			pg_log_error("could not enable subscription \"%s\": %s", subname,
 						 PQerrorMessage(conn));
 			PQfinish(conn);
 			exit(1);
@@ -1520,16 +1566,10 @@ main(int argc, char **argv)
 	int			option_index;
 
 	char	   *base_dir;
-	char	   *server_start_log;
 	int			len;
 
-	char	   *pub_base_conninfo = NULL;
-	char	   *sub_base_conninfo = NULL;
 	char	   *dbname_conninfo = NULL;
-	char		temp_replslot[NAMEDATALEN] = {0};
 
-	uint64		pub_sysid;
-	uint64		sub_sysid;
 	struct stat statbuf;
 
 	PGconn	   *conn;
@@ -1579,7 +1619,7 @@ main(int argc, char **argv)
 		switch (c)
 		{
 			case 'D':
-				subscriber_dir = pg_strdup(optarg);
+				standby.pgdata = pg_strdup(optarg);
 				break;
 			case 'S':
 				sub_conninfo_str = pg_strdup(optarg);
@@ -1589,7 +1629,7 @@ main(int argc, char **argv)
 				if (!simple_string_list_member(&database_names, optarg))
 				{
 					simple_string_list_append(&database_names, optarg);
-					num_dbs++;
+					dbarr.ndbs++;
 				}
 				break;
 			case 'n':
@@ -1625,7 +1665,7 @@ main(int argc, char **argv)
 	/*
 	 * Required arguments
 	 */
-	if (subscriber_dir == NULL)
+	if (standby.pgdata == NULL)
 	{
 		pg_log_error("no subscriber data directory specified");
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -1638,8 +1678,8 @@ main(int argc, char **argv)
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 		exit(1);
 	}
-	sub_base_conninfo = get_base_conninfo(sub_conninfo_str, dbname_conninfo);
-	if (sub_base_conninfo == NULL)
+	standby.base_conninfo = get_base_conninfo(sub_conninfo_str, dbname_conninfo);
+	if (standby.base_conninfo == NULL)
 		exit(1);
 
 	if (database_names.head == NULL)
@@ -1654,7 +1694,7 @@ main(int argc, char **argv)
 		if (dbname_conninfo)
 		{
 			simple_string_list_append(&database_names, dbname_conninfo);
-			num_dbs++;
+			dbarr.ndbs++;
 
 			pg_log_info("database \"%s\" was extracted from the subscriber connection string",
 						dbname_conninfo);
@@ -1668,20 +1708,20 @@ main(int argc, char **argv)
 	}
 
 	/* Obtain a connection string from the target */
-	pub_base_conninfo = get_primary_conninfo(sub_base_conninfo);
+	primary.base_conninfo = get_primary_conninfo(&standby);
 
 	/*
 	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
 	 */
-	if (!get_exec_path(argv[0]))
+	if (!get_exec_base_path(argv[0]))
 		exit(1);
 
 	/* rudimentary check for a data directory. */
-	if (!check_data_directory(subscriber_dir))
+	if (!check_data_directory(standby.pgdata))
 		exit(1);
 
-	/* Store database information for publisher and subscriber. */
-	dbinfo = store_pub_sub_info(pub_base_conninfo, sub_base_conninfo);
+	/* Store database information to dbarr */
+	store_db_names(&dbarr.perdb, dbarr.ndbs);
 
 	/* Register a function to clean up objects in case of failure. */
 	atexit(cleanup_objects_atexit);
@@ -1690,9 +1730,9 @@ main(int argc, char **argv)
 	 * Check if the subscriber data directory has the same system identifier
 	 * than the publisher data directory.
 	 */
-	pub_sysid = get_sysid_from_conn(dbinfo[0].pubconninfo);
-	sub_sysid = get_control_from_datadir(subscriber_dir);
-	if (pub_sysid != sub_sysid)
+	get_sysid_for_primary(&primary, dbarr.perdb[0].dbname);
+	get_sysid_for_standby(&standby);
+	if (primary.sysid != standby.sysid)
 	{
 		pg_log_error("subscriber data directory is not a copy of the source database cluster");
 		exit(1);
@@ -1702,7 +1742,7 @@ main(int argc, char **argv)
 	 * Create the output directory to store any data generated by this tool.
 	 */
 	base_dir = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(base_dir, MAXPGPATH, "%s/%s", subscriber_dir, PGS_OUTPUT_DIR);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", standby.pgdata, PGS_OUTPUT_DIR);
 	if (len >= MAXPGPATH)
 	{
 		pg_log_error("directory path for subscriber is too long");
@@ -1715,10 +1755,10 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
-	server_start_log = server_logfile_name(subscriber_dir);
+	standby.server_log = server_logfile_name(standby.pgdata);
 
 	/* subscriber PID file. */
-	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", standby.pgdata);
 
 	/*
 	 * The standby server must be running. That's because some checks will be
@@ -1731,7 +1771,7 @@ main(int argc, char **argv)
 		/*
 		 * Check if the standby server is ready for logical replication.
 		 */
-		if (!check_subscriber(dbinfo))
+		if (!check_subscriber(&standby, &dbarr))
 			exit(1);
 
 		/*
@@ -1740,7 +1780,7 @@ main(int argc, char **argv)
 		 * relies on check_subscriber() to obtain the primary_slot_name.
 		 * That's why it is called after it.
 		 */
-		if (!check_publisher(dbinfo))
+		if (!check_publisher(&primary, &dbarr))
 			exit(1);
 
 		/*
@@ -1749,13 +1789,13 @@ main(int argc, char **argv)
 		 * if the primary slot is in use. We could use an extra connection for
 		 * it but it doesn't seem worth.
 		 */
-		if (!setup_publisher(dbinfo))
+		if (!setup_publisher(&primary, &dbarr))
 			exit(1);
 
 		/* Stop the standby server. */
 		pg_log_info("standby is up and running");
 		pg_log_info("stopping the server to start the transformation steps");
-		stop_standby_server(pg_ctl_path, subscriber_dir);
+		stop_standby_server(&standby);
 	}
 	else
 	{
@@ -1776,11 +1816,10 @@ main(int argc, char **argv)
 	 * consistent LSN but it should be changed after adding pg_basebackup
 	 * support.
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_database(primary.base_conninfo, dbarr.perdb[0].dbname);
 	if (conn == NULL)
 		exit(1);
-	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
-													 temp_replslot);
+	consistent_lsn = create_logical_replication_slot(conn, true, &dbarr.perdb[0]);
 
 	/*
 	 * Write recovery parameters.
@@ -1806,7 +1845,7 @@ main(int argc, char **argv)
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  consistent_lsn);
-		WriteRecoveryConfig(conn, subscriber_dir, recoveryconfcontents);
+		WriteRecoveryConfig(conn, standby.pgdata, recoveryconfcontents);
 	}
 	disconnect_database(conn);
 
@@ -1816,12 +1855,12 @@ main(int argc, char **argv)
 	 * Start subscriber and wait until accepting connections.
 	 */
 	pg_log_info("starting the subscriber");
-	start_standby_server(pg_ctl_path, subscriber_dir, server_start_log);
+	start_standby_server(&standby);
 
 	/*
 	 * Waiting the subscriber to be promoted.
 	 */
-	wait_for_end_recovery(dbinfo[0].subconninfo);
+	wait_for_end_recovery(&standby, dbarr.perdb[0].dbname);
 
 	/*
 	 * Create the subscription for each database on subscriber. It does not
@@ -1830,7 +1869,7 @@ main(int argc, char **argv)
 	 * set_replication_progress). It also cleans up publications created by
 	 * this tool and replication to the standby.
 	 */
-	if (!setup_subscriber(dbinfo, consistent_lsn))
+	if (!setup_subscriber(&standby, &primary, &dbarr, consistent_lsn))
 		exit(1);
 
 	/*
@@ -1839,12 +1878,15 @@ main(int argc, char **argv)
 	 * XXX we might not fail here. Instead, we provide a warning so the user
 	 * eventually drops this replication slot later.
 	 */
-	if (primary_slot_name != NULL)
+	if (standby.primary_slot_name != NULL)
 	{
-		conn = connect_database(dbinfo[0].pubconninfo);
+		char *primary_slot_name = standby.primary_slot_name;
+		LogicalRepPerdbInfo *perdb = &dbarr.perdb[0];
+
+		conn = connect_database(primary.base_conninfo, perdb->dbname);
 		if (conn != NULL)
 		{
-			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+			drop_replication_slot(conn, perdb, primary_slot_name);
 		}
 		else
 		{
@@ -1858,19 +1900,19 @@ main(int argc, char **argv)
 	 * Stop the subscriber.
 	 */
 	pg_log_info("stopping the subscriber");
-	stop_standby_server(pg_ctl_path, subscriber_dir);
+	stop_standby_server(&standby);
 
 	/*
 	 * Change system identifier.
 	 */
-	modify_sysid(pg_resetwal_path, subscriber_dir);
+	modify_sysid(standby.bindir, standby.pgdata);
 
 	/*
 	 * The log file is kept if retain option is specified or this tool does
 	 * not run successfully. Otherwise, log file is removed.
 	 */
 	if (!retain)
-		unlink(server_start_log);
+		unlink(standby.server_log);
 
 	success = true;
 
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index a9d03acc87..1544953843 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -17,6 +17,7 @@ my $result;
 # Set up node P as primary
 $node_p = PostgreSQL::Test::Cluster->new('node_p');
 $node_p->init(allows_streaming => 'logical');
+$node_p->append_conf('postgresql.conf', 'log_line_prefix = \'%m [%p] [%d] \'');
 $node_p->start;
 
 # Set up node F as about-to-fail node
@@ -88,7 +89,7 @@ command_ok(
 		'--pgdata', $node_s->data_dir,
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
-		'--database', 'pg2'
+		'--database', 'pg2', '-r'
 	],
 	'run pg_createsubscriber on node S');
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index f51f1ff23f..3b1ec3fce1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1505,9 +1505,10 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
-LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
+LogicalRepPerdbInfo
+LogicalRepPerdbInfoArr
 LogicalRepPreparedTxnData
 LogicalRepRelId
 LogicalRepRelMapEntry
@@ -1886,6 +1887,7 @@ PREDICATELOCK
 PREDICATELOCKTAG
 PREDICATELOCKTARGET
 PREDICATELOCKTARGETTAG
+PrimaryInfo
 PROCESS_INFORMATION
 PROCLOCK
 PROCLOCKTAG
@@ -2461,6 +2463,7 @@ SQLValueFunctionOp
 SSL
 SSLExtensionInfoContext
 SSL_CTX
+StandbyInfo
 STARTUPINFO
 STRLEN
 SV
-- 
2.43.0

#111Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#110)
1 attachment(s)
Re: speed up a logical replica setup

On Thu, Feb 1, 2024, at 9:47 AM, Hayato Kuroda (Fujitsu) wrote:

I made fix patches to solve reported issues by Fabrízio.

* v13-0001: Same as v11-0001 made by Euler.
* v13-0002: Fixes ERRORs while dropping replication slots [1].
If you want to see codes which we get agreement, please apply until 0002.
=== experimental patches ===
* v13-0003: Avoids to use replication connections. The issue [2] was solved on my env.
* v13-0004: Removes -P option and use primary_conninfo instead.
* v13-0005: Refactors data structures

Thanks for rebasing the proposed patches. I'm attaching a new patch.

As I said in the previous email [1]/messages/by-id/80ce3f65-7ca3-471e-b1a4-24ac529ff4ea@app.fastmail.com I fixed some issues from your previous review:

* use pg_fatal() if possible. There are some cases that it couldn't replace
pg_log_error() + exit(1) because it requires a hint.
* pg_resetwal output. Send standard output to /dev/null to avoid extra message.
* check privileges. Make sure the current role can execute CREATE SUBSCRIPTION
and pg_replication_origin_advance().
* log directory. Refactor code that setup the log file used as server log.
* run with restricted token (Windows).
* v13-0002. Merged.
* v13-0003. I applied a modified version. I returned only the required
information for each query.
* group command-line options into a new struct CreateSubscriberOptions. The
exception is the dry_run option.
* rename functions that obtain system identifier.

WIP

I'm still working on the data structures to group options. I don't like the way
it was grouped in v13-0005. There is too many levels to reach database name.
The setup_subscriber() function requires the 3 data structures.

The documentation update is almost there. I will include the modifications in
the next patch.

Regarding v13-0004, it seems a good UI that's why I wrote a comment about it.
However, it comes with a restriction that requires a similar HBA rule for both
regular and replication connections. Is it an acceptable restriction? We might
paint ourselves into the corner. A reasonable proposal is not to remove this
option. Instead, it should be optional. If it is not provided, primary_conninfo
is used.

[1]: /messages/by-id/80ce3f65-7ca3-471e-b1a4-24ac529ff4ea@app.fastmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v14-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchtext/x-patch; name="=?UTF-8?Q?v14-0001-Creates-a-new-logical-replica-from-a-standby-ser.patc?= =?UTF-8?Q?h?="Download
From 2666b5f660c64d699a0be440c3701c7be00ec309 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v14] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  320 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1876 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   44 +
 .../t/041_pg_createsubscriber_standby.pl      |  139 ++
 src/tools/pgindent/typedefs.list              |    2 +
 10 files changed, 2410 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f5238771b7
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,320 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..28a82902b3
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1876 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_authid_d.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+/* Command-line options */
+typedef struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir;			/* standby/subscriber data directory */
+	char	   *pub_conninfo_str;		/* publisher connection string */
+	char	   *sub_conninfo_str;		/* subscriber connection string */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;					/* retain log file? */
+	int			recovery_timeout;		/* stop recovery after this time */
+} CreateSubscriberOptions;
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions opt);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo, CreateSubscriberOptions opt);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: %s", PQresultErrorMessage(res));
+	}
+	if (PQntuples(res) != 1)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: got %d rows, expected %d row",
+				 PQntuples(res), 1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt.subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt.subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path, opt.subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical max_replication_slots >= current + number of dbs to
+	 * be converted max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str, "SELECT pg_has_role(current_user, %u, 'MEMBER'), has_database_privilege(current_user, '%s', 'CREATE'), has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')", ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"", "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		return false;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical replication and
+ * enable the subscriptions. That's the last step for logical repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", transient_replslot ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (!transient_replslot)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it is
+ * run.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, CreateSubscriberOptions opt)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_fatal("could not obtain recovery progress");
+
+		if (PQntuples(res) != 1)
+			pg_fatal("unexpected result from pg_is_in_recovery function");
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (opt.recovery_timeout > 0 && timer >= opt.recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+			pg_fatal("recovery timed out");
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain publication information: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
+				 PQntuples(res), 1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not enable subscription \"%s\": %s", dbinfo->subname,
+					 PQerrorMessage(conn));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	CreateSubscriberOptions opt;
+
+	int			c;
+	int			option_index;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+	char		temp_replslot[NAMEDATALEN] = {0};
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	memset(&opt, 0, sizeof(CreateSubscriberOptions));
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.sub_conninfo_str = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				opt.sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(opt.subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure. */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server. */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo, opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/*
+	 * Change system identifier from subscriber.
+	 */
+	modify_subscriber_sysid(pg_resetwal_path, opt);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..0f02b1bfac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..534bc53a76
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91433d439b..102971164f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.30.2

#112Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#111)
6 attachment(s)
RE: speed up a logical replica setup

Dear Euler,

Thanks for updating the patch!

I'm still working on the data structures to group options. I don't like the way
it was grouped in v13-0005. There is too many levels to reach database name.
The setup_subscriber() function requires the 3 data structures.

Right, your refactoring looks fewer stack. So I pause to revise my refactoring
patch.

The documentation update is almost there. I will include the modifications in
the next patch.

OK. I think it should be modified before native speakers will attend to the
thread.

Regarding v13-0004, it seems a good UI that's why I wrote a comment about it.
However, it comes with a restriction that requires a similar HBA rule for both
regular and replication connections. Is it an acceptable restriction? We might
paint ourselves into the corner. A reasonable proposal is not to remove this
option. Instead, it should be optional. If it is not provided, primary_conninfo
is used.

I didn't have such a point of view. However, it is not related whether -P exists
or not. Even v14-0001 requires primary to accept both normal/replication connections.
If we want to avoid it, the connection from pg_createsubscriber can be restored
to replication-connection.
(I felt we do not have to use replication protocol even if we change the connection mode)

The motivation why -P is not needed is to ensure the consistency of nodes.
pg_createsubscriber assumes that the -P option can connect to the upstream node,
but no one checks it. Parsing two connection strings may be a solution but be
confusing. E.g., what if some options are different?
I think using a same parameter is a simplest solution.

And below part contains my comments for v14.

01.
```
char temp_replslot[NAMEDATALEN] = {0};
```

I found that no one refers the name of temporary slot. Can we remove the variable?

02.
```
CreateSubscriberOptions opt;
...
memset(&opt, 0, sizeof(CreateSubscriberOptions));

/* Default settings */
opt.subscriber_dir = NULL;
opt.pub_conninfo_str = NULL;
opt.sub_conninfo_str = NULL;
opt.database_names = (SimpleStringList)
{
NULL, NULL
};
opt.retain = false;
opt.recovery_timeout = 0;
```

Initialization by `CreateSubscriberOptions opt = {0};` seems enough.
All values are set to 0x0.

03.
```
/*
* Is the standby server ready for logical replication?
*/
static bool
check_subscriber(LogicalRepInfo *dbinfo)
```

You said "target server must be a standby" in [1]/messages/by-id/b315c7da-7ab1-4014-a2a9-8ab6ae26017c@app.fastmail.com, but I cannot find checks for it.
IIUC, there are two approaches:
a) check the existence "standby.signal" in the data directory
b) call an SQL function "pg_is_in_recovery"

04.
```
static char *pg_ctl_path = NULL;
static char *pg_resetwal_path = NULL;
```

I still think they can be combined as "bindir".

05.

```
/*
* Write recovery parameters.
...
WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
```

WriteRecoveryConfig() writes GUC parameters to postgresql.auto.conf, but not
sure it is good. These settings would remain on new subscriber even after the
pg_createsubscriber. Can we avoid it? I come up with passing these parameters
via pg_ctl -o option, but it requires parsing output from GenerateRecoveryConfig()
(all GUCs must be allign like "-c XXX -c XXX -c XXX...").

06.
```
static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
...
static void modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions opt);
...
static void wait_for_end_recovery(const char *conninfo, CreateSubscriberOptions opt);
```

Functions arguments should not be struct because they are passing by value.
They should be a pointer. Or, for modify_subscriber_sysid and wait_for_end_recovery,
we can pass a value which would be really used.

07.
```
static char *get_base_conninfo(char *conninfo, char *dbname,
const char *noderole);
```

Not sure noderole should be passed here. It is used only for the logging.
Can we output string before calling the function?
(The parameter is not needed anymore if -P is removed)

08.
The terminology is still not consistent. Some functions call the target as standby,
but others call it as subscriber.

09.
v14 does not work if the standby server has already been set recovery_target*
options. PSA the reproducer. I considered two approaches:

a) raise an ERROR when these parameter were set. check_subscriber() can do it
b) overwrite these GUCs as empty strings.

10.
The execution always fails if users execute --dry-run just before. Because
pg_createsubscriber stops the standby anyway. Doing dry run first is quite normal
use-case, so current implementation seems not user-friendly. How should we fix?
Below bullets are my idea:

a) avoid stopping the standby in case of dry_run: seems possible.
b) accept even if the standby is stopped: seems possible.
c) start the standby at the end of run: how arguments like pg_ctl -l should be specified?

My top-up patches fixes some issues.

v15-0001: same as v14-0001
=== experimental patches ===
v15-0002: Use replication connections when we connects to the primary.
Connections to standby is not changed because the standby/subscriber
does not require such type of connection, in principle.
If we can accept connecting to subscriber with replication mode,
this can be simplified.
v15-0003: Remove -P and use primary_conninfo instead. Same as v13-0004
v15-0004: Check whether the target is really standby. This is done by pg_is_in_recovery()
v15-0005: Avoid stopping/starting standby server in dry_run mode.
I.e., approach a). in #10 is used.
v15-0006: Overwrite recovery parameters. I.e., aproach b). in #9 is used.

[1]: /messages/by-id/b315c7da-7ab1-4014-a2a9-8ab6ae26017c@app.fastmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

Attachments:

v15-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchapplication/octet-stream; name=v15-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchDownload
From 431a444d6dde02b1aefadc07a4e10eaa27207661 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v15 1/6] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  320 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1876 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   44 +
 .../t/041_pg_createsubscriber_standby.pl      |  139 ++
 src/tools/pgindent/typedefs.list              |    2 +
 10 files changed, 2410 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f5238771b7
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,320 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..28a82902b3
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1876 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_authid_d.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+/* Command-line options */
+typedef struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir;			/* standby/subscriber data directory */
+	char	   *pub_conninfo_str;		/* publisher connection string */
+	char	   *sub_conninfo_str;		/* subscriber connection string */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;					/* retain log file? */
+	int			recovery_timeout;		/* stop recovery after this time */
+} CreateSubscriberOptions;
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions opt);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo, CreateSubscriberOptions opt);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: %s", PQresultErrorMessage(res));
+	}
+	if (PQntuples(res) != 1)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: got %d rows, expected %d row",
+				 PQntuples(res), 1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt.subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt.subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path, opt.subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical max_replication_slots >= current + number of dbs to
+	 * be converted max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str, "SELECT pg_has_role(current_user, %u, 'MEMBER'), has_database_privilege(current_user, '%s', 'CREATE'), has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')", ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"", "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		return false;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical replication and
+ * enable the subscriptions. That's the last step for logical repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", transient_replslot ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (!transient_replslot)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it is
+ * run.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, CreateSubscriberOptions opt)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_fatal("could not obtain recovery progress");
+
+		if (PQntuples(res) != 1)
+			pg_fatal("unexpected result from pg_is_in_recovery function");
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (opt.recovery_timeout > 0 && timer >= opt.recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+			pg_fatal("recovery timed out");
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain publication information: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
+				 PQntuples(res), 1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not enable subscription \"%s\": %s", dbinfo->subname,
+					 PQerrorMessage(conn));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	CreateSubscriberOptions opt;
+
+	int			c;
+	int			option_index;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+	char		temp_replslot[NAMEDATALEN] = {0};
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	memset(&opt, 0, sizeof(CreateSubscriberOptions));
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.sub_conninfo_str = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				opt.sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(opt.subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure. */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server. */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo, opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/*
+	 * Change system identifier from subscriber.
+	 */
+	modify_subscriber_sysid(pg_resetwal_path, opt);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..0f02b1bfac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..534bc53a76
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91433d439b..102971164f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.43.0

v15-0002-Use-replication-connection-when-we-connect-to-th.patchapplication/octet-stream; name=v15-0002-Use-replication-connection-when-we-connect-to-th.patchDownload
From 11c1303416d65499a17e2060280687d4020b4ba8 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 08:27:13 +0000
Subject: [PATCH v15 2/6] Use replication connection when we connect to the
 primary

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 46 +++++++++++++++------
 1 file changed, 33 insertions(+), 13 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 28a82902b3..1fee8727ad 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -68,7 +68,7 @@ static bool get_exec_path(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
 static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
-static PGconn *connect_database(const char *conninfo);
+static PGconn *connect_database(const char *conninfo, bool replication_mode);
 static void disconnect_database(PGconn *conn);
 static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
@@ -118,6 +118,19 @@ enum WaitPMResult
 };
 
 
+static inline PGconn *
+connect_primary(const char *conninfo)
+{
+	return connect_database(conninfo, true);
+}
+
+static inline PGconn *
+connect_standby(const char *conninfo)
+{
+	return connect_database(conninfo, false);
+}
+
+
 /*
  * Cleanup objects that were created by pg_createsubscriber if there is an error.
  *
@@ -138,7 +151,7 @@ cleanup_objects_atexit(void)
 	{
 		if (dbinfo[i].made_subscription)
 		{
-			conn = connect_database(dbinfo[i].subconninfo);
+			conn = connect_standby(dbinfo[i].subconninfo);
 			if (conn != NULL)
 			{
 				drop_subscription(conn, &dbinfo[i]);
@@ -150,7 +163,7 @@ cleanup_objects_atexit(void)
 
 		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
 		{
-			conn = connect_database(dbinfo[i].pubconninfo);
+			conn = connect_primary(dbinfo[i].pubconninfo);
 			if (conn != NULL)
 			{
 				if (dbinfo[i].made_publication)
@@ -398,12 +411,17 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, cons
 }
 
 static PGconn *
-connect_database(const char *conninfo)
+connect_database(const char *conninfo, bool replication_mode)
 {
 	PGconn	   *conn;
 	PGresult   *res;
+	char	   *rconninfo = NULL;
 
-	conn = PQconnectdb(conninfo);
+	/* logical replication mode */
+	if (replication_mode)
+		rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo ? rconninfo : conninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
 	{
 		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
@@ -417,6 +435,8 @@ connect_database(const char *conninfo)
 		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
 		return NULL;
 	}
+
+	pg_free(rconninfo);
 	PQclear(res);
 
 	return conn;
@@ -443,7 +463,7 @@ get_primary_sysid(const char *conninfo)
 
 	pg_log_info("getting system identifier from publisher");
 
-	conn = connect_database(conninfo);
+	conn = connect_primary(conninfo);
 	if (conn == NULL)
 		exit(1);
 
@@ -568,7 +588,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		char		pubname[NAMEDATALEN];
 		char		replslotname[NAMEDATALEN];
 
-		conn = connect_database(dbinfo[i].pubconninfo);
+		conn = connect_primary(dbinfo[i].pubconninfo);
 		if (conn == NULL)
 			exit(1);
 
@@ -659,7 +679,7 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * wal_level = logical max_replication_slots >= current + number of dbs to
 	 * be converted max_wal_senders >= current + number of dbs to be converted
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_primary(dbinfo[0].pubconninfo);
 	if (conn == NULL)
 		exit(1);
 
@@ -768,7 +788,7 @@ check_subscriber(LogicalRepInfo *dbinfo)
 
 	pg_log_info("checking settings on subscriber");
 
-	conn = connect_database(dbinfo[0].subconninfo);
+	conn = connect_standby(dbinfo[0].subconninfo);
 	if (conn == NULL)
 		exit(1);
 
@@ -879,7 +899,7 @@ setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
 	for (int i = 0; i < num_dbs; i++)
 	{
 		/* Connect to subscriber. */
-		conn = connect_database(dbinfo[i].subconninfo);
+		conn = connect_standby(dbinfo[i].subconninfo);
 		if (conn == NULL)
 			exit(1);
 
@@ -1110,7 +1130,7 @@ wait_for_end_recovery(const char *conninfo, CreateSubscriberOptions opt)
 
 	pg_log_info("waiting the postmaster to reach the consistent state");
 
-	conn = connect_database(conninfo);
+	conn = connect_standby(conninfo);
 	if (conn == NULL)
 		exit(1);
 
@@ -1772,7 +1792,7 @@ main(int argc, char **argv)
 	 * consistent LSN but it should be changed after adding pg_basebackup
 	 * support.
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_primary(dbinfo[0].pubconninfo);
 	if (conn == NULL)
 		exit(1);
 	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
@@ -1837,7 +1857,7 @@ main(int argc, char **argv)
 	 */
 	if (primary_slot_name != NULL)
 	{
-		conn = connect_database(dbinfo[0].pubconninfo);
+		conn = connect_primary(dbinfo[0].pubconninfo);
 		if (conn != NULL)
 		{
 			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
-- 
2.43.0

v15-0003-Remove-P-and-use-primary_conninfo-instead.patchapplication/octet-stream; name=v15-0003-Remove-P-and-use-primary_conninfo-instead.patchDownload
From 6a6b98506dd58f27f60dcdd7b958069ae6240cea Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 09:31:44 +0000
Subject: [PATCH v15 3/6] Remove -P and use primary_conninfo instead

XXX: This may be a problematic when the OS user who started target instance is
not the current OS user and PGPASSWORD environment variable was used for
connecting to the primary server. In this case, the password would not be
written in the primary_conninfo and the PGPASSWORD variable might not be set.
This may lead an connection error. Is this a real issue? Note that using
PGPASSWORD is not recommended.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  17 +--
 src/bin/pg_basebackup/pg_createsubscriber.c   | 107 ++++++++++++------
 .../t/040_pg_createsubscriber.pl              |   8 --
 .../t/041_pg_createsubscriber_standby.pl      |   5 +-
 4 files changed, 72 insertions(+), 65 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index f5238771b7..2ff31628ce 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -29,11 +29,6 @@ PostgreSQL documentation
      <arg choice="plain"><option>--pgdata</option></arg>
     </group>
     <replaceable>datadir</replaceable>
-    <group choice="req">
-     <arg choice="plain"><option>-P</option></arg>
-     <arg choice="plain"><option>--publisher-server</option></arg>
-    </group>
-    <replaceable>connstr</replaceable>
     <group choice="req">
      <arg choice="plain"><option>-S</option></arg>
      <arg choice="plain"><option>--subscriber-server</option></arg>
@@ -82,16 +77,6 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
-     <varlistentry>
-      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
-      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
-      <listitem>
-       <para>
-        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry>
       <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
       <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
@@ -303,7 +288,7 @@ PostgreSQL documentation
    To create a logical replica for databases <literal>hr</literal> and
    <literal>finance</literal> from a physical replica at <literal>foo</literal>:
 <screen>
-<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -S "host=localhost" -d hr -d finance</userinput>
 </screen>
   </para>
 
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1fee8727ad..135bb3e111 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -38,7 +38,6 @@
 typedef struct CreateSubscriberOptions
 {
 	char	   *subscriber_dir;			/* standby/subscriber data directory */
-	char	   *pub_conninfo_str;		/* publisher connection string */
 	char	   *sub_conninfo_str;		/* subscriber connection string */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;					/* retain log file? */
@@ -62,10 +61,11 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname,
-							   const char *noderole);
+static char *get_base_conninfo(char *conninfo, char *dbname);
 static bool get_exec_path(const char *path);
 static bool check_data_directory(const char *datadir);
+static char *get_primary_conninfo_from_target(const char *base_conninfo,
+											  const char *dbname);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
 static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
 static PGconn *connect_database(const char *conninfo, bool replication_mode);
@@ -185,7 +185,6 @@ usage(void)
 	printf(_("  %s [OPTION]...\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
-	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
@@ -210,7 +209,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+get_base_conninfo(char *conninfo, char *dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -219,7 +218,7 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 	char	   *ret;
 	int			i;
 
-	pg_log_info("validating connection string on %s", noderole);
+	pg_log_info("validating connection string on subscriber");
 
 	conn_opts = PQconninfoParse(conninfo, &errmsg);
 	if (conn_opts == NULL)
@@ -450,6 +449,57 @@ disconnect_database(PGconn *conn)
 	PQfinish(conn);
 }
 
+/*
+ * Obtain primary_conninfo from the target server. The value would be used for
+ * connecting from the pg_createsubscriber itself and logical replication apply
+ * worker.
+ */
+static char *
+get_primary_conninfo_from_target(const char *base_conninfo, const char *dbname)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	char	   *conninfo;
+	char	   *primaryconninfo;
+
+	pg_log_info("getting primary_conninfo from standby");
+
+	/*
+	 * Construct a connection string to the target instance. Since dbinfo has
+	 * not stored infomation yet, the name must be passed as an argument.
+	 */
+	conninfo = concat_conninfo_dbname(base_conninfo, dbname);
+
+	conn = connect_standby(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SHOW primary_conninfo;");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send command \"%s\": %s",
+					 "SHOW primary_conninfo;", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	primaryconninfo = pg_strdup(PQgetvalue(res, 0, 0));
+
+	if (strlen(primaryconninfo) == 0)
+	{
+		pg_log_error("primary_conninfo was empty");
+		pg_log_error_hint("Check whether the target server is really a standby.");
+		exit(1);
+	}
+
+	pg_free(conninfo);
+	PQclear(res);
+	disconnect_database(conn);
+
+	return primaryconninfo;
+}
+
 /*
  * Obtain the system identifier using the provided connection. It will be used
  * to compare if a data directory is a clone of another one.
@@ -1312,15 +1362,18 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char	   *conninfo;
 
 	Assert(conn != NULL);
 
 	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
 
+	conninfo = escape_single_quotes_ascii(dbinfo->pubconninfo);
+
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
 					  "WITH (create_slot = false, copy_data = false, enabled = false)",
-					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+					  dbinfo->subname, conninfo, dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1341,6 +1394,7 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 		PQclear(res);
 
+	pg_free(conninfo);
 	destroyPQExpBuffer(str);
 }
 
@@ -1503,7 +1557,6 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
-		{"publisher-server", required_argument, NULL, 'P'},
 		{"subscriber-server", required_argument, NULL, 'S'},
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
@@ -1560,7 +1613,6 @@ main(int argc, char **argv)
 
 	/* Default settings */
 	opt.subscriber_dir = NULL;
-	opt.pub_conninfo_str = NULL;
 	opt.sub_conninfo_str = NULL;
 	opt.database_names = (SimpleStringList)
 	{
@@ -1585,7 +1637,7 @@ main(int argc, char **argv)
 
 	get_restricted_token();
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+	while ((c = getopt_long(argc, argv, "D:S:d:nrt:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1593,9 +1645,6 @@ main(int argc, char **argv)
 			case 'D':
 				opt.subscriber_dir = pg_strdup(optarg);
 				break;
-			case 'P':
-				opt.pub_conninfo_str = pg_strdup(optarg);
-				break;
 			case 'S':
 				opt.sub_conninfo_str = pg_strdup(optarg);
 				break;
@@ -1647,34 +1696,13 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
-	/*
-	 * Parse connection string. Build a base connection string that might be
-	 * reused by multiple databases.
-	 */
-	if (opt.pub_conninfo_str == NULL)
-	{
-		/*
-		 * TODO use primary_conninfo (if available) from subscriber and
-		 * extract publisher connection string. Assume that there are
-		 * identical entries for physical and logical replication. If there is
-		 * not, we would fail anyway.
-		 */
-		pg_log_error("no publisher connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo,
-										  "publisher");
-	if (pub_base_conninfo == NULL)
-		exit(1);
-
 	if (opt.sub_conninfo_str == NULL)
 	{
 		pg_log_error("no subscriber connection string specified");
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 		exit(1);
 	}
-	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL, "subscriber");
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, dbname_conninfo);
 	if (sub_base_conninfo == NULL)
 		exit(1);
 
@@ -1684,7 +1712,7 @@ main(int argc, char **argv)
 
 		/*
 		 * If --database option is not provided, try to obtain the dbname from
-		 * the publisher conninfo. If dbname parameter is not available, error
+		 * the subscriber conninfo. If dbname parameter is not available, error
 		 * out.
 		 */
 		if (dbname_conninfo)
@@ -1692,7 +1720,7 @@ main(int argc, char **argv)
 			simple_string_list_append(&opt.database_names, dbname_conninfo);
 			num_dbs++;
 
-			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+			pg_log_info("database \"%s\" was extracted from the subscriber connection string",
 						dbname_conninfo);
 		}
 		else
@@ -1703,6 +1731,11 @@ main(int argc, char **argv)
 		}
 	}
 
+	/* Obtain a connection string from the target */
+	pub_base_conninfo =
+				get_primary_conninfo_from_target(sub_base_conninfo,
+												 opt.database_names.head->val);
+
 	/*
 	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
 	 */
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 0f02b1bfac..5c240a5417 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -17,18 +17,11 @@ my $datadir = PostgreSQL::Test::Utils::tempdir;
 
 command_fails(['pg_createsubscriber'],
 	'no subscriber data directory specified');
-command_fails(
-	[
-		'pg_createsubscriber',
-		'--pgdata', $datadir
-	],
-	'no publisher connection string specified');
 command_fails(
 	[
 		'pg_createsubscriber',
 		'--dry-run',
 		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres'
 	],
 	'no subscriber connection string specified');
 command_fails(
@@ -36,7 +29,6 @@ command_fails(
 		'pg_createsubscriber',
 		'--verbose',
 		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres',
 		'--subscriber-server', 'dbname=postgres'
 	],
 	'no database name specified');
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 534bc53a76..a9d03acc87 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -56,19 +56,17 @@ command_fails(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_f->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
-	'subscriber data directory is not a copy of the source database cluster');
+	'target database is not a physical standby');
 
 # dry run mode on node S
 command_ok(
 	[
 		'pg_createsubscriber', '--verbose', '--dry-run',
 		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
@@ -88,7 +86,6 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
-- 
2.43.0

v15-0004-Check-whether-the-target-is-really-standby.patchapplication/octet-stream; name=v15-0004-Check-whether-the-target-is-really-standby.patchDownload
From 483a500ad036168dc703208f3857e3b609db49ca Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 09:31:16 +0000
Subject: [PATCH v15 4/6] Check whether the target is really standby

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 135bb3e111..9530b11816 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -842,6 +842,21 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	if (conn == NULL)
 		exit(1);
 
+	/* The target server must be a standby */
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("The target server was not a standby");
+		return false;
+	}
+
 	/*
 	 * Subscriptions can only be created by roles that have the privileges of
 	 * pg_create_subscription role and CREATE privileges on the specified
-- 
2.43.0

v15-0005-Avoid-stopping-starting-standby-server-in-dry_ru.patchapplication/octet-stream; name=v15-0005-Avoid-stopping-starting-standby-server-in-dry_ru.patchDownload
From 012750083003446dfc4557c5c0739b818341b65a Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 09:07:40 +0000
Subject: [PATCH v15 5/6] Avoid stopping/starting standby server in dry_run
 mode

---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 25 +++++++++++++------
 .../t/041_pg_createsubscriber_standby.pl      |  4 ---
 2 files changed, 17 insertions(+), 12 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 9530b11816..fd40ed5317 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1816,10 +1816,13 @@ main(int argc, char **argv)
 		if (!setup_publisher(dbinfo))
 			exit(1);
 
-		/* Stop the standby server. */
-		pg_log_info("standby is up and running");
-		pg_log_info("stopping the server to start the transformation steps");
-		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+		if (!dry_run)
+		{
+			/* Stop the standby server. */
+			pg_log_info("standby is up and running");
+			pg_log_info("stopping the server to start the transformation steps");
+			stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+		}
 	}
 	else
 	{
@@ -1879,8 +1882,11 @@ main(int argc, char **argv)
 	/*
 	 * Start subscriber and wait until accepting connections.
 	 */
-	pg_log_info("starting the subscriber");
-	start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+	if (!dry_run)
+	{
+		pg_log_info("starting the subscriber");
+		start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+	}
 
 	/*
 	 * Waiting the subscriber to be promoted.
@@ -1921,8 +1927,11 @@ main(int argc, char **argv)
 	/*
 	 * Stop the subscriber.
 	 */
-	pg_log_info("stopping the subscriber");
-	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	if (!dry_run)
+	{
+		pg_log_info("stopping the subscriber");
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
 
 	/*
 	 * Change system identifier from subscriber.
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index a9d03acc87..a6ba58879f 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -73,10 +73,6 @@ command_ok(
 	],
 	'run pg_createsubscriber --dry-run on node S');
 
-# PID sets to undefined because subscriber was stopped behind the scenes.
-# Start subscriber
-$node_s->{_pid} = undef;
-$node_s->start;
 # Check if node S is still a standby
 is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
 	't', 'standby is in recovery');
-- 
2.43.0

v15-0006-Overwrite-recovery-parameters.patchapplication/octet-stream; name=v15-0006-Overwrite-recovery-parameters.patchDownload
From 2abe6c2c5242f32fcf4df3235e1ad10a3741b0e9 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 09:17:20 +0000
Subject: [PATCH v15 6/6] Overwrite recovery parameters

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index fd40ed5317..52b0a94fb5 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1871,6 +1871,17 @@ main(int argc, char **argv)
 	}
 	else
 	{
+		/*
+		 * XXX: there is a possibility that subscriber already has
+		 * recovery_target* option, but they can be set at most one of them. So
+		 * overwrite parameters except recovery_target_lsn to an empty string.
+		 * Note that the setting would be never restored.
+		 */
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  consistent_lsn);
 		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
-- 
2.43.0

#113Shubham Khanna
khannashubham1197@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#112)
Re: speed up a logical replica setup

On Fri, Feb 2, 2024 at 3:11 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear Euler,

Thanks for updating the patch!

I'm still working on the data structures to group options. I don't like the way
it was grouped in v13-0005. There is too many levels to reach database name.
The setup_subscriber() function requires the 3 data structures.

Right, your refactoring looks fewer stack. So I pause to revise my refactoring
patch.

The documentation update is almost there. I will include the modifications in
the next patch.

OK. I think it should be modified before native speakers will attend to the
thread.

Regarding v13-0004, it seems a good UI that's why I wrote a comment about it.
However, it comes with a restriction that requires a similar HBA rule for both
regular and replication connections. Is it an acceptable restriction? We might
paint ourselves into the corner. A reasonable proposal is not to remove this
option. Instead, it should be optional. If it is not provided, primary_conninfo
is used.

I didn't have such a point of view. However, it is not related whether -P exists
or not. Even v14-0001 requires primary to accept both normal/replication connections.
If we want to avoid it, the connection from pg_createsubscriber can be restored
to replication-connection.
(I felt we do not have to use replication protocol even if we change the connection mode)

The motivation why -P is not needed is to ensure the consistency of nodes.
pg_createsubscriber assumes that the -P option can connect to the upstream node,
but no one checks it. Parsing two connection strings may be a solution but be
confusing. E.g., what if some options are different?
I think using a same parameter is a simplest solution.

And below part contains my comments for v14.

01.
```
char temp_replslot[NAMEDATALEN] = {0};
```

I found that no one refers the name of temporary slot. Can we remove the variable?

02.
```
CreateSubscriberOptions opt;
...
memset(&opt, 0, sizeof(CreateSubscriberOptions));

/* Default settings */
opt.subscriber_dir = NULL;
opt.pub_conninfo_str = NULL;
opt.sub_conninfo_str = NULL;
opt.database_names = (SimpleStringList)
{
NULL, NULL
};
opt.retain = false;
opt.recovery_timeout = 0;
```

Initialization by `CreateSubscriberOptions opt = {0};` seems enough.
All values are set to 0x0.

03.
```
/*
* Is the standby server ready for logical replication?
*/
static bool
check_subscriber(LogicalRepInfo *dbinfo)
```

You said "target server must be a standby" in [1], but I cannot find checks for it.
IIUC, there are two approaches:
a) check the existence "standby.signal" in the data directory
b) call an SQL function "pg_is_in_recovery"

04.
```
static char *pg_ctl_path = NULL;
static char *pg_resetwal_path = NULL;
```

I still think they can be combined as "bindir".

05.

```
/*
* Write recovery parameters.
...
WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
```

WriteRecoveryConfig() writes GUC parameters to postgresql.auto.conf, but not
sure it is good. These settings would remain on new subscriber even after the
pg_createsubscriber. Can we avoid it? I come up with passing these parameters
via pg_ctl -o option, but it requires parsing output from GenerateRecoveryConfig()
(all GUCs must be allign like "-c XXX -c XXX -c XXX...").

06.
```
static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
...
static void modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions opt);
...
static void wait_for_end_recovery(const char *conninfo, CreateSubscriberOptions opt);
```

Functions arguments should not be struct because they are passing by value.
They should be a pointer. Or, for modify_subscriber_sysid and wait_for_end_recovery,
we can pass a value which would be really used.

07.
```
static char *get_base_conninfo(char *conninfo, char *dbname,
const char *noderole);
```

Not sure noderole should be passed here. It is used only for the logging.
Can we output string before calling the function?
(The parameter is not needed anymore if -P is removed)

08.
The terminology is still not consistent. Some functions call the target as standby,
but others call it as subscriber.

09.
v14 does not work if the standby server has already been set recovery_target*
options. PSA the reproducer. I considered two approaches:

a) raise an ERROR when these parameter were set. check_subscriber() can do it
b) overwrite these GUCs as empty strings.

10.
The execution always fails if users execute --dry-run just before. Because
pg_createsubscriber stops the standby anyway. Doing dry run first is quite normal
use-case, so current implementation seems not user-friendly. How should we fix?
Below bullets are my idea:

a) avoid stopping the standby in case of dry_run: seems possible.
b) accept even if the standby is stopped: seems possible.
c) start the standby at the end of run: how arguments like pg_ctl -l should be specified?

My top-up patches fixes some issues.

v15-0001: same as v14-0001
=== experimental patches ===
v15-0002: Use replication connections when we connects to the primary.
Connections to standby is not changed because the standby/subscriber
does not require such type of connection, in principle.
If we can accept connecting to subscriber with replication mode,
this can be simplified.
v15-0003: Remove -P and use primary_conninfo instead. Same as v13-0004
v15-0004: Check whether the target is really standby. This is done by pg_is_in_recovery()
v15-0005: Avoid stopping/starting standby server in dry_run mode.
I.e., approach a). in #10 is used.
v15-0006: Overwrite recovery parameters. I.e., aproach b). in #9 is used.

[1]: /messages/by-id/b315c7da-7ab1-4014-a2a9-8ab6ae26017c@app.fastmail.com

While reviewing the v15 patches I discovered that subscription
connection string has added a lot of options which are not required
now:
v15-0001
postgres=# select subname, subconninfo from pg_subscription;
subname | subconninfo
-------------------------------+------------------------------------------
pg_createsubscriber_5_1867633 | host=localhost port=5432 dbname=postgres
(1 row)

v15-0001+0002+0003
postgres=# select subname, subconninfo from pg_subscription;
subname |

subconninfo

-------------------------------+------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------
------------------------------
pg_createsubscriber_5_1895366 | user=shubham
passfile='/home/shubham/.pgpass' channel_binding=prefer ho
st=127.0.0.1 port=5432 sslmode=prefer sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_versi
on=TLSv1.2 gssencmode=disable krbsrvname=postgres gssdelegation=0
target_session_attrs=any load_balance_
hosts=disable dbname=postgres
(1 row)

Here, we can see that channel_binding, sslmode, sslcertmode, sslsni,
gssencmode, krbsrvname, etc are getting included. This does not look
intentional, we should keep the subscription connection same as in
v15-0001.

Thanks and Regards,
Shubham Khanna.

Thanks and Regards,
Shubham Khanna.

#114Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#112)
2 attachment(s)
RE: speed up a logical replica setup

Dear Euler,

03.
```
/*
* Is the standby server ready for logical replication?
*/
static bool
check_subscriber(LogicalRepInfo *dbinfo)
```

You said "target server must be a standby" in [1], but I cannot find checks for it.
IIUC, there are two approaches:
a) check the existence "standby.signal" in the data directory
b) call an SQL function "pg_is_in_recovery"

I found that current code (HEAD+v14-0001) leads stuck or timeout because the recovery
would be never finished. In attached script emulates the case standby.signal file is missing.
This script always fails with below output:

```
...
pg_createsubscriber: waiting the postmaster to reach the consistent state
pg_createsubscriber: postmaster was stopped
pg_createsubscriber: error: recovery timed out
...
```

I also attached the server log during pg_createsubscriber, and we can see that
walreceiver exits before receiving all the required changes. I cannot find a path
yet, but the inconsistency lead the situation which walreceiver exists but startup
remains. This also said that recovery status must be checked in check_subscriber().

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

server_start_20240206T080003.670.logapplication/octet-stream; name=server_start_20240206T080003.670.logDownload
test_0206.shapplication/octet-stream; name=test_0206.shDownload
#115Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Shubham Khanna (#113)
1 attachment(s)
RE: speed up a logical replica setup

Dear Shubham,

Thanks for testing our codes!

While reviewing the v15 patches I discovered that subscription
connection string has added a lot of options which are not required
now:
v15-0001
postgres=# select subname, subconninfo from pg_subscription;
subname | subconninfo
-------------------------------+------------------------------------------
pg_createsubscriber_5_1867633 | host=localhost port=5432 dbname=postgres
(1 row)

v15-0001+0002+0003
postgres=# select subname, subconninfo from pg_subscription;
subname |

subconninfo

-------------------------------+--------------------------------------------------
----------------------
----------------------------------------------------------------------------------
----------------------
----------------------------------------------------------------------------------
----------------------
------------------------------
pg_createsubscriber_5_1895366 | user=shubham
passfile='/home/shubham/.pgpass' channel_binding=prefer ho
st=127.0.0.1 port=5432 sslmode=prefer sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_versi
on=TLSv1.2 gssencmode=disable krbsrvname=postgres gssdelegation=0
target_session_attrs=any load_balance_
hosts=disable dbname=postgres
(1 row)

Here, we can see that channel_binding, sslmode, sslcertmode, sslsni,
gssencmode, krbsrvname, etc are getting included. This does not look
intentional, we should keep the subscription connection same as in
v15-0001.

You should attach the script the reproducer. I suspected you used pg_basebackup
-R command for setting up the standby. In this case, it is intentional.
These settings are not caused by the pg_createsubscriber, done by pg_basebackup.
If you set primary_conninfo manually like attached, these settings would not appear.

As the first place, listed options (E.g., passfile, channel_binding, sslmode,
sslcompression, sslcertmode, etc...) were set when you connect to the database
via libpq functions. PQconninfoOptions in fe-connect.c lists parameters and
their default value.

v15-0003 reuses the primary_conninfo for subconninfo attribute. primary_conninfo
is set by pg_basebackup specified with '-R' option.
The content is built in GenerateRecoveryConfig(), which bypass parameters from
PQconninfo(). This function returns all the libpq connection parameters even if
it is set as default. So primary_conninfo looks longer.

I don't think this works wrongly. Users still can set an arbitrary connection
string as primary_conninfo. You just use longer string unintentionally.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

shorter_subconninfo.shapplication/octet-stream; name=shorter_subconninfo.shDownload
#116Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#112)
7 attachment(s)
Re: speed up a logical replica setup

Hi,

My top-up patches fixes some issues.

v15-0001: same as v14-0001
=== experimental patches ===
v15-0002: Use replication connections when we connects to the primary.
Connections to standby is not changed because the standby/subscriber
does not require such type of connection, in principle.
If we can accept connecting to subscriber with replication mode,
this can be simplified.
v15-0003: Remove -P and use primary_conninfo instead. Same as v13-0004
v15-0004: Check whether the target is really standby. This is done by pg_is_in_recovery()
v15-0005: Avoid stopping/starting standby server in dry_run mode.
I.e., approach a). in #10 is used.
v15-0006: Overwrite recovery parameters. I.e., aproach b). in #9 is used.

[1]: /messages/by-id/b315c7da-7ab1-4014-a2a9-8ab6ae26017c@app.fastmail.com

I have created a topup patch 0007 on top of v15-0006.

I revived the patch which removes -S option and adds some options
instead. The patch add option for --port, --username and --socketdir.
This patch also ensures that anyone cannot connect to the standby
during the pg_createsubscriber, by setting listen_addresses,
unix_socket_permissions, and unix_socket_directories.

Thanks and Regards,
Shlok Kyal

Attachments:

v16-0005-Avoid-stopping-starting-standby-server-in-dry_ru.patchapplication/x-patch; name=v16-0005-Avoid-stopping-starting-standby-server-in-dry_ru.patchDownload
From 0425bf8936e2f6da9ea8a98697e5483ef701c4e8 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 09:07:40 +0000
Subject: [PATCH v16 5/7] Avoid stopping/starting standby server in dry_run
 mode

---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 25 +++++++++++++------
 .../t/041_pg_createsubscriber_standby.pl      |  4 ---
 2 files changed, 17 insertions(+), 12 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 9530b11816..fd40ed5317 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1816,10 +1816,13 @@ main(int argc, char **argv)
 		if (!setup_publisher(dbinfo))
 			exit(1);
 
-		/* Stop the standby server. */
-		pg_log_info("standby is up and running");
-		pg_log_info("stopping the server to start the transformation steps");
-		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+		if (!dry_run)
+		{
+			/* Stop the standby server. */
+			pg_log_info("standby is up and running");
+			pg_log_info("stopping the server to start the transformation steps");
+			stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+		}
 	}
 	else
 	{
@@ -1879,8 +1882,11 @@ main(int argc, char **argv)
 	/*
 	 * Start subscriber and wait until accepting connections.
 	 */
-	pg_log_info("starting the subscriber");
-	start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+	if (!dry_run)
+	{
+		pg_log_info("starting the subscriber");
+		start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+	}
 
 	/*
 	 * Waiting the subscriber to be promoted.
@@ -1921,8 +1927,11 @@ main(int argc, char **argv)
 	/*
 	 * Stop the subscriber.
 	 */
-	pg_log_info("stopping the subscriber");
-	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	if (!dry_run)
+	{
+		pg_log_info("stopping the subscriber");
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
 
 	/*
 	 * Change system identifier from subscriber.
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index a9d03acc87..a6ba58879f 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -73,10 +73,6 @@ command_ok(
 	],
 	'run pg_createsubscriber --dry-run on node S');
 
-# PID sets to undefined because subscriber was stopped behind the scenes.
-# Start subscriber
-$node_s->{_pid} = undef;
-$node_s->start;
 # Check if node S is still a standby
 is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
 	't', 'standby is in recovery');
-- 
2.34.1

v16-0004-Check-whether-the-target-is-really-standby.patchapplication/x-patch; name=v16-0004-Check-whether-the-target-is-really-standby.patchDownload
From 9d9bf76ddbe400965c3a8ca42e26585703d78858 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 09:31:16 +0000
Subject: [PATCH v16 4/7] Check whether the target is really standby

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 135bb3e111..9530b11816 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -842,6 +842,21 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	if (conn == NULL)
 		exit(1);
 
+	/* The target server must be a standby */
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("The target server was not a standby");
+		return false;
+	}
+
 	/*
 	 * Subscriptions can only be created by roles that have the privileges of
 	 * pg_create_subscription role and CREATE privileges on the specified
-- 
2.34.1

v16-0003-Remove-P-and-use-primary_conninfo-instead.patchapplication/x-patch; name=v16-0003-Remove-P-and-use-primary_conninfo-instead.patchDownload
From 231653456a571e778b6707613d1fb70ce1b753b3 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 09:31:44 +0000
Subject: [PATCH v16 3/7] Remove -P and use primary_conninfo instead

XXX: This may be a problematic when the OS user who started target instance is
not the current OS user and PGPASSWORD environment variable was used for
connecting to the primary server. In this case, the password would not be
written in the primary_conninfo and the PGPASSWORD variable might not be set.
This may lead an connection error. Is this a real issue? Note that using
PGPASSWORD is not recommended.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  17 +--
 src/bin/pg_basebackup/pg_createsubscriber.c   | 107 ++++++++++++------
 .../t/040_pg_createsubscriber.pl              |   8 --
 .../t/041_pg_createsubscriber_standby.pl      |   5 +-
 4 files changed, 72 insertions(+), 65 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index f5238771b7..2ff31628ce 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -29,11 +29,6 @@ PostgreSQL documentation
      <arg choice="plain"><option>--pgdata</option></arg>
     </group>
     <replaceable>datadir</replaceable>
-    <group choice="req">
-     <arg choice="plain"><option>-P</option></arg>
-     <arg choice="plain"><option>--publisher-server</option></arg>
-    </group>
-    <replaceable>connstr</replaceable>
     <group choice="req">
      <arg choice="plain"><option>-S</option></arg>
      <arg choice="plain"><option>--subscriber-server</option></arg>
@@ -82,16 +77,6 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
-     <varlistentry>
-      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
-      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
-      <listitem>
-       <para>
-        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry>
       <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
       <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
@@ -303,7 +288,7 @@ PostgreSQL documentation
    To create a logical replica for databases <literal>hr</literal> and
    <literal>finance</literal> from a physical replica at <literal>foo</literal>:
 <screen>
-<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -S "host=localhost" -d hr -d finance</userinput>
 </screen>
   </para>
 
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1fee8727ad..135bb3e111 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -38,7 +38,6 @@
 typedef struct CreateSubscriberOptions
 {
 	char	   *subscriber_dir;			/* standby/subscriber data directory */
-	char	   *pub_conninfo_str;		/* publisher connection string */
 	char	   *sub_conninfo_str;		/* subscriber connection string */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;					/* retain log file? */
@@ -62,10 +61,11 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname,
-							   const char *noderole);
+static char *get_base_conninfo(char *conninfo, char *dbname);
 static bool get_exec_path(const char *path);
 static bool check_data_directory(const char *datadir);
+static char *get_primary_conninfo_from_target(const char *base_conninfo,
+											  const char *dbname);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
 static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
 static PGconn *connect_database(const char *conninfo, bool replication_mode);
@@ -185,7 +185,6 @@ usage(void)
 	printf(_("  %s [OPTION]...\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
-	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
@@ -210,7 +209,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+get_base_conninfo(char *conninfo, char *dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -219,7 +218,7 @@ get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
 	char	   *ret;
 	int			i;
 
-	pg_log_info("validating connection string on %s", noderole);
+	pg_log_info("validating connection string on subscriber");
 
 	conn_opts = PQconninfoParse(conninfo, &errmsg);
 	if (conn_opts == NULL)
@@ -450,6 +449,57 @@ disconnect_database(PGconn *conn)
 	PQfinish(conn);
 }
 
+/*
+ * Obtain primary_conninfo from the target server. The value would be used for
+ * connecting from the pg_createsubscriber itself and logical replication apply
+ * worker.
+ */
+static char *
+get_primary_conninfo_from_target(const char *base_conninfo, const char *dbname)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	char	   *conninfo;
+	char	   *primaryconninfo;
+
+	pg_log_info("getting primary_conninfo from standby");
+
+	/*
+	 * Construct a connection string to the target instance. Since dbinfo has
+	 * not stored infomation yet, the name must be passed as an argument.
+	 */
+	conninfo = concat_conninfo_dbname(base_conninfo, dbname);
+
+	conn = connect_standby(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SHOW primary_conninfo;");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send command \"%s\": %s",
+					 "SHOW primary_conninfo;", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	primaryconninfo = pg_strdup(PQgetvalue(res, 0, 0));
+
+	if (strlen(primaryconninfo) == 0)
+	{
+		pg_log_error("primary_conninfo was empty");
+		pg_log_error_hint("Check whether the target server is really a standby.");
+		exit(1);
+	}
+
+	pg_free(conninfo);
+	PQclear(res);
+	disconnect_database(conn);
+
+	return primaryconninfo;
+}
+
 /*
  * Obtain the system identifier using the provided connection. It will be used
  * to compare if a data directory is a clone of another one.
@@ -1312,15 +1362,18 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char	   *conninfo;
 
 	Assert(conn != NULL);
 
 	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
 
+	conninfo = escape_single_quotes_ascii(dbinfo->pubconninfo);
+
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
 					  "WITH (create_slot = false, copy_data = false, enabled = false)",
-					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+					  dbinfo->subname, conninfo, dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1341,6 +1394,7 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 		PQclear(res);
 
+	pg_free(conninfo);
 	destroyPQExpBuffer(str);
 }
 
@@ -1503,7 +1557,6 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
-		{"publisher-server", required_argument, NULL, 'P'},
 		{"subscriber-server", required_argument, NULL, 'S'},
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
@@ -1560,7 +1613,6 @@ main(int argc, char **argv)
 
 	/* Default settings */
 	opt.subscriber_dir = NULL;
-	opt.pub_conninfo_str = NULL;
 	opt.sub_conninfo_str = NULL;
 	opt.database_names = (SimpleStringList)
 	{
@@ -1585,7 +1637,7 @@ main(int argc, char **argv)
 
 	get_restricted_token();
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+	while ((c = getopt_long(argc, argv, "D:S:d:nrt:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1593,9 +1645,6 @@ main(int argc, char **argv)
 			case 'D':
 				opt.subscriber_dir = pg_strdup(optarg);
 				break;
-			case 'P':
-				opt.pub_conninfo_str = pg_strdup(optarg);
-				break;
 			case 'S':
 				opt.sub_conninfo_str = pg_strdup(optarg);
 				break;
@@ -1647,34 +1696,13 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
-	/*
-	 * Parse connection string. Build a base connection string that might be
-	 * reused by multiple databases.
-	 */
-	if (opt.pub_conninfo_str == NULL)
-	{
-		/*
-		 * TODO use primary_conninfo (if available) from subscriber and
-		 * extract publisher connection string. Assume that there are
-		 * identical entries for physical and logical replication. If there is
-		 * not, we would fail anyway.
-		 */
-		pg_log_error("no publisher connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo,
-										  "publisher");
-	if (pub_base_conninfo == NULL)
-		exit(1);
-
 	if (opt.sub_conninfo_str == NULL)
 	{
 		pg_log_error("no subscriber connection string specified");
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 		exit(1);
 	}
-	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL, "subscriber");
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, dbname_conninfo);
 	if (sub_base_conninfo == NULL)
 		exit(1);
 
@@ -1684,7 +1712,7 @@ main(int argc, char **argv)
 
 		/*
 		 * If --database option is not provided, try to obtain the dbname from
-		 * the publisher conninfo. If dbname parameter is not available, error
+		 * the subscriber conninfo. If dbname parameter is not available, error
 		 * out.
 		 */
 		if (dbname_conninfo)
@@ -1692,7 +1720,7 @@ main(int argc, char **argv)
 			simple_string_list_append(&opt.database_names, dbname_conninfo);
 			num_dbs++;
 
-			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+			pg_log_info("database \"%s\" was extracted from the subscriber connection string",
 						dbname_conninfo);
 		}
 		else
@@ -1703,6 +1731,11 @@ main(int argc, char **argv)
 		}
 	}
 
+	/* Obtain a connection string from the target */
+	pub_base_conninfo =
+				get_primary_conninfo_from_target(sub_base_conninfo,
+												 opt.database_names.head->val);
+
 	/*
 	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
 	 */
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 0f02b1bfac..5c240a5417 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -17,18 +17,11 @@ my $datadir = PostgreSQL::Test::Utils::tempdir;
 
 command_fails(['pg_createsubscriber'],
 	'no subscriber data directory specified');
-command_fails(
-	[
-		'pg_createsubscriber',
-		'--pgdata', $datadir
-	],
-	'no publisher connection string specified');
 command_fails(
 	[
 		'pg_createsubscriber',
 		'--dry-run',
 		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres'
 	],
 	'no subscriber connection string specified');
 command_fails(
@@ -36,7 +29,6 @@ command_fails(
 		'pg_createsubscriber',
 		'--verbose',
 		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres',
 		'--subscriber-server', 'dbname=postgres'
 	],
 	'no database name specified');
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 534bc53a76..a9d03acc87 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -56,19 +56,17 @@ command_fails(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_f->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
-	'subscriber data directory is not a copy of the source database cluster');
+	'target database is not a physical standby');
 
 # dry run mode on node S
 command_ok(
 	[
 		'pg_createsubscriber', '--verbose', '--dry-run',
 		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
@@ -88,7 +86,6 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
-- 
2.34.1

v16-0006-Overwrite-recovery-parameters.patchapplication/x-patch; name=v16-0006-Overwrite-recovery-parameters.patchDownload
From da805e1dafe5936a978cf5a6b6169481f26c4a81 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 09:17:20 +0000
Subject: [PATCH v16 6/7] Overwrite recovery parameters

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index fd40ed5317..52b0a94fb5 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1871,6 +1871,17 @@ main(int argc, char **argv)
 	}
 	else
 	{
+		/*
+		 * XXX: there is a possibility that subscriber already has
+		 * recovery_target* option, but they can be set at most one of them. So
+		 * overwrite parameters except recovery_target_lsn to an empty string.
+		 * Note that the setting would be never restored.
+		 */
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  consistent_lsn);
 		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
-- 
2.34.1

v16-0002-Use-replication-connection-when-we-connect-to-th.patchapplication/x-patch; name=v16-0002-Use-replication-connection-when-we-connect-to-th.patchDownload
From b2b7f5b0cf3c23966521dbb0c4f80f183d99f0ac Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 08:27:13 +0000
Subject: [PATCH v16 2/7] Use replication connection when we connect to the
 primary

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 46 +++++++++++++++------
 1 file changed, 33 insertions(+), 13 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 28a82902b3..1fee8727ad 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -68,7 +68,7 @@ static bool get_exec_path(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
 static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
-static PGconn *connect_database(const char *conninfo);
+static PGconn *connect_database(const char *conninfo, bool replication_mode);
 static void disconnect_database(PGconn *conn);
 static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
@@ -118,6 +118,19 @@ enum WaitPMResult
 };
 
 
+static inline PGconn *
+connect_primary(const char *conninfo)
+{
+	return connect_database(conninfo, true);
+}
+
+static inline PGconn *
+connect_standby(const char *conninfo)
+{
+	return connect_database(conninfo, false);
+}
+
+
 /*
  * Cleanup objects that were created by pg_createsubscriber if there is an error.
  *
@@ -138,7 +151,7 @@ cleanup_objects_atexit(void)
 	{
 		if (dbinfo[i].made_subscription)
 		{
-			conn = connect_database(dbinfo[i].subconninfo);
+			conn = connect_standby(dbinfo[i].subconninfo);
 			if (conn != NULL)
 			{
 				drop_subscription(conn, &dbinfo[i]);
@@ -150,7 +163,7 @@ cleanup_objects_atexit(void)
 
 		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
 		{
-			conn = connect_database(dbinfo[i].pubconninfo);
+			conn = connect_primary(dbinfo[i].pubconninfo);
 			if (conn != NULL)
 			{
 				if (dbinfo[i].made_publication)
@@ -398,12 +411,17 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, cons
 }
 
 static PGconn *
-connect_database(const char *conninfo)
+connect_database(const char *conninfo, bool replication_mode)
 {
 	PGconn	   *conn;
 	PGresult   *res;
+	char	   *rconninfo = NULL;
 
-	conn = PQconnectdb(conninfo);
+	/* logical replication mode */
+	if (replication_mode)
+		rconninfo = psprintf("%s replication=database", conninfo);
+
+	conn = PQconnectdb(rconninfo ? rconninfo : conninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
 	{
 		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
@@ -417,6 +435,8 @@ connect_database(const char *conninfo)
 		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
 		return NULL;
 	}
+
+	pg_free(rconninfo);
 	PQclear(res);
 
 	return conn;
@@ -443,7 +463,7 @@ get_primary_sysid(const char *conninfo)
 
 	pg_log_info("getting system identifier from publisher");
 
-	conn = connect_database(conninfo);
+	conn = connect_primary(conninfo);
 	if (conn == NULL)
 		exit(1);
 
@@ -568,7 +588,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		char		pubname[NAMEDATALEN];
 		char		replslotname[NAMEDATALEN];
 
-		conn = connect_database(dbinfo[i].pubconninfo);
+		conn = connect_primary(dbinfo[i].pubconninfo);
 		if (conn == NULL)
 			exit(1);
 
@@ -659,7 +679,7 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * wal_level = logical max_replication_slots >= current + number of dbs to
 	 * be converted max_wal_senders >= current + number of dbs to be converted
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_primary(dbinfo[0].pubconninfo);
 	if (conn == NULL)
 		exit(1);
 
@@ -768,7 +788,7 @@ check_subscriber(LogicalRepInfo *dbinfo)
 
 	pg_log_info("checking settings on subscriber");
 
-	conn = connect_database(dbinfo[0].subconninfo);
+	conn = connect_standby(dbinfo[0].subconninfo);
 	if (conn == NULL)
 		exit(1);
 
@@ -879,7 +899,7 @@ setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
 	for (int i = 0; i < num_dbs; i++)
 	{
 		/* Connect to subscriber. */
-		conn = connect_database(dbinfo[i].subconninfo);
+		conn = connect_standby(dbinfo[i].subconninfo);
 		if (conn == NULL)
 			exit(1);
 
@@ -1110,7 +1130,7 @@ wait_for_end_recovery(const char *conninfo, CreateSubscriberOptions opt)
 
 	pg_log_info("waiting the postmaster to reach the consistent state");
 
-	conn = connect_database(conninfo);
+	conn = connect_standby(conninfo);
 	if (conn == NULL)
 		exit(1);
 
@@ -1772,7 +1792,7 @@ main(int argc, char **argv)
 	 * consistent LSN but it should be changed after adding pg_basebackup
 	 * support.
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
+	conn = connect_primary(dbinfo[0].pubconninfo);
 	if (conn == NULL)
 		exit(1);
 	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
@@ -1837,7 +1857,7 @@ main(int argc, char **argv)
 	 */
 	if (primary_slot_name != NULL)
 	{
-		conn = connect_database(dbinfo[0].pubconninfo);
+		conn = connect_primary(dbinfo[0].pubconninfo);
 		if (conn != NULL)
 		{
 			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
-- 
2.34.1

v16-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchapplication/x-patch; name=v16-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchDownload
From 978182ffd26009dc20a75dbaf3f72bf013e8065b Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v16 1/7] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica into a
logical replica. It runs on the target server and should be able to
connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.
Stop the target server. Change the system identifier from the target
server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  320 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1876 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   44 +
 .../t/041_pg_createsubscriber_standby.pl      |  139 ++
 src/tools/pgindent/typedefs.list              |    2 +
 10 files changed, 2410 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f5238771b7
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,320 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..28a82902b3
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1876 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_authid_d.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+/* Command-line options */
+typedef struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir;			/* standby/subscriber data directory */
+	char	   *pub_conninfo_str;		/* publisher connection string */
+	char	   *sub_conninfo_str;		/* subscriber connection string */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;					/* retain log file? */
+	int			recovery_timeout;		/* stop recovery after this time */
+} CreateSubscriberOptions;
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname,
+							   const char *noderole);
+static bool get_exec_path(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions opt);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo, CreateSubscriberOptions opt);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname, const char *noderole)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	pg_log_info("validating connection string on %s", noderole);
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the absolute path from other PostgreSQL binaries (pg_ctl and
+ * pg_resetwal) that is used by it.
+ */
+static bool
+get_exec_path(const char *path)
+{
+	int			rc;
+
+	pg_ctl_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_ctl",
+						 "pg_ctl (PostgreSQL) " PG_VERSION "\n",
+						 pg_ctl_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_ctl", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_ctl", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_ctl path is: %s", pg_ctl_path);
+
+	pg_resetwal_path = pg_malloc(MAXPGPATH);
+	rc = find_other_exec(path, "pg_resetwal",
+						 "pg_resetwal (PostgreSQL) " PG_VERSION "\n",
+						 pg_resetwal_path);
+	if (rc < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(path, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+		if (rc == -1)
+			pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+						 "same directory as \"%s\".\n"
+						 "Check your installation.",
+						 "pg_resetwal", progname, full_path);
+		else
+			pg_log_error("The program \"%s\" was found by \"%s\"\n"
+						 "but was not the same version as %s.\n"
+						 "Check your installation.",
+						 "pg_resetwal", full_path, progname);
+		return false;
+	}
+
+	pg_log_debug("pg_resetwal path is: %s", pg_resetwal_path);
+
+	return true;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: %s", PQresultErrorMessage(res));
+	}
+	if (PQntuples(res) != 1)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: got %d rows, expected %d row",
+				 PQntuples(res), 1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt.subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt.subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path, opt.subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], replslotname) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical max_replication_slots >= current + number of dbs to
+	 * be converted max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str, "SELECT pg_has_role(current_user, %u, 'MEMBER'), has_database_privilege(current_user, '%s', 'CREATE'), has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')", ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"", "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		return false;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical replication and
+ * enable the subscriptions. That's the last step for logical repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a consistent LSN. The returned
+ * LSN might be used to catch up the subscriber up to the required point.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the consistent LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char	   *lsn = NULL;
+	bool		transient_replslot = false;
+
+	Assert(conn != NULL);
+
+	/*
+	 * If no slot name is informed, it is a transient replication slot used
+	 * only for catch up purposes.
+	 */
+	if (slot_name[0] == '\0')
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+		transient_replslot = true;
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", transient_replslot ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (!transient_replslot)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it is
+ * run.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path, datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, CreateSubscriberOptions opt)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_fatal("could not obtain recovery progress");
+
+		if (PQntuples(res) != 1)
+			pg_fatal("unexpected result from pg_is_in_recovery function");
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (opt.recovery_timeout > 0 && timer >= opt.recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+			pg_fatal("recovery timed out");
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain publication information: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
+				 PQntuples(res), 1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not enable subscription \"%s\": %s", dbinfo->subname,
+					 PQerrorMessage(conn));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	CreateSubscriberOptions opt;
+
+	int			c;
+	int			option_index;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+	char		temp_replslot[NAMEDATALEN] = {0};
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	memset(&opt, 0, sizeof(CreateSubscriberOptions));
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.sub_conninfo_str = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				opt.sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo,
+										  "publisher");
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL, "subscriber");
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	if (!get_exec_path(argv[0]))
+		exit(1);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(opt.subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure. */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server. */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0],
+													 temp_replslot);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the follwing recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes.
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo, opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/*
+	 * Change system identifier from subscriber.
+	 */
+	modify_subscriber_sysid(pg_resetwal_path, opt);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..0f02b1bfac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..534bc53a76
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91433d439b..102971164f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.34.1

v16-0007-Remove-S-option-to-force-unix-domain-connection.patchapplication/x-patch; name=v16-0007-Remove-S-option-to-force-unix-domain-connection.patchDownload
From 4b8d8a7c043699bee95d313200949227843168fe Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 6 Feb 2024 14:45:03 +0530
Subject: [PATCH v16 7/7] Remove -S option to force unix domain connection

With this patch removed -S option and added option for username(-u), port(-p)
and socket directory(-s) for standby. This helps to force standby to use
unix domain connection.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  49 +++--
 src/bin/pg_basebackup/pg_createsubscriber.c   | 184 ++++++++++--------
 .../t/041_pg_createsubscriber_standby.pl      |  12 +-
 3 files changed, 146 insertions(+), 99 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 2ff31628ce..3cf729627c 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -29,11 +29,6 @@ PostgreSQL documentation
      <arg choice="plain"><option>--pgdata</option></arg>
     </group>
     <replaceable>datadir</replaceable>
-    <group choice="req">
-     <arg choice="plain"><option>-S</option></arg>
-     <arg choice="plain"><option>--subscriber-server</option></arg>
-    </group>
-    <replaceable>connstr</replaceable>
     <group choice="req">
      <arg choice="plain"><option>-d</option></arg>
      <arg choice="plain"><option>--database</option></arg>
@@ -77,16 +72,6 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
-     <varlistentry>
-      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
-      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
-      <listitem>
-       <para>
-        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry>
       <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
       <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
@@ -108,6 +93,17 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-p</option> <replaceable>port</replaceable></term>
+      <term><option>--port=</option><replaceable>port</replaceable></term>
+      <listitem>
+       <para>
+        the subscriber's port number;
+        default port number is 50111.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-r</option></term>
       <term><option>--retain</option></term>
@@ -118,6 +114,17 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-s</option> <replaceable>dir</replaceable></term>
+      <term><option>--socketdir=</option><replaceable>dir</replaceable></term>
+      <listitem>
+       <para>
+        directory to use for postmaster sockets during upgrade;
+        default is current working directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
        <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
        <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
@@ -129,6 +136,16 @@ PostgreSQL documentation
        </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-u</option> <replaceable>username</replaceable></term>
+      <term><option>--username=</option><replaceable>username</replaceable></term>
+      <listitem>
+       <para>
+        subscriber's install user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-v</option></term>
       <term><option>--verbose</option></term>
@@ -288,7 +305,7 @@ PostgreSQL documentation
    To create a logical replica for databases <literal>hr</literal> and
    <literal>finance</literal> from a physical replica at <literal>foo</literal>:
 <screen>
-<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -S "host=localhost" -d hr -d finance</userinput>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -d hr -d finance</userinput>
 </screen>
   </para>
 
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 52b0a94fb5..a2b39d6aa1 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -38,7 +38,6 @@
 typedef struct CreateSubscriberOptions
 {
 	char	   *subscriber_dir;			/* standby/subscriber data directory */
-	char	   *sub_conninfo_str;		/* subscriber connection string */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;					/* retain log file? */
 	int			recovery_timeout;		/* stop recovery after this time */
@@ -61,7 +60,6 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname);
 static bool get_exec_path(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *get_primary_conninfo_from_target(const char *base_conninfo,
@@ -81,7 +79,7 @@ static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinf
 											 char *slot_name);
 static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
 static char *setup_server_logfile(const char *datadir);
-static void start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile);
+static void start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile, unsigned short subport, char *sockdir);
 static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
 static void wait_for_end_recovery(const char *conninfo, CreateSubscriberOptions opt);
@@ -91,9 +89,12 @@ static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
 static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static char *construct_sub_conninfo(char *username, unsigned short subport, char *sockdir);
+static void update_sub_info(SimpleStringList dbnames, LogicalRepInfo *dbinfo, const char *sub_base_conninfo);
 
 #define	USEC_PER_SEC	1000000
 #define	WAIT_INTERVAL	1		/* 1 second */
+#define DEF_PGSPORT		50111
 
 /* Options */
 static const char *progname;
@@ -109,6 +110,8 @@ static char *pg_resetwal_path = NULL;
 static LogicalRepInfo *dbinfo;
 static int	num_dbs = 0;
 
+static bool is_standby_restarted = false;
+
 enum WaitPMResult
 {
 	POSTMASTER_READY,
@@ -185,11 +188,13 @@ usage(void)
 	printf(_("  %s [OPTION]...\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
-	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
 	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
 	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -p, --port=PORT                     subscriber port number (default port is %d.)\n"), DEF_PGSPORT);
+	printf(_(" -s, --socketdir=DIR                 socket directory to use (default current dir.)\n"));
+	printf(_(" -u, --username=NAME                 subscriber superuser\n"));
 	printf(_(" -v, --verbose                       output verbose messages\n"));
 	printf(_(" -V, --version                       output version information, then exit\n"));
 	printf(_(" -?, --help                          show this help, then exit\n"));
@@ -197,63 +202,6 @@ usage(void)
 	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
 }
 
-/*
- * Validate a connection string. Returns a base connection string that is a
- * connection string without a database name.
- * Since we might process multiple databases, each database name will be
- * appended to this base connection string to provide a final connection string.
- * If the second argument (dbname) is not null, returns dbname if the provided
- * connection string contains it. If option --database is not provided, uses
- * dbname as the only database to setup the logical replica.
- * It is the caller's responsibility to free the returned connection string and
- * dbname.
- */
-static char *
-get_base_conninfo(char *conninfo, char *dbname)
-{
-	PQExpBuffer buf = createPQExpBuffer();
-	PQconninfoOption *conn_opts = NULL;
-	PQconninfoOption *conn_opt;
-	char	   *errmsg = NULL;
-	char	   *ret;
-	int			i;
-
-	pg_log_info("validating connection string on subscriber");
-
-	conn_opts = PQconninfoParse(conninfo, &errmsg);
-	if (conn_opts == NULL)
-	{
-		pg_log_error("could not parse connection string: %s", errmsg);
-		return NULL;
-	}
-
-	i = 0;
-	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
-	{
-		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
-		{
-			if (dbname)
-				dbname = pg_strdup(conn_opt->val);
-			continue;
-		}
-
-		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
-		{
-			if (i > 0)
-				appendPQExpBufferChar(buf, ' ');
-			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
-			i++;
-		}
-	}
-
-	ret = pg_strdup(buf->data);
-
-	destroyPQExpBuffer(buf);
-	PQconninfoFree(conn_opts);
-
-	return ret;
-}
-
 /*
  * Get the absolute path from other PostgreSQL binaries (pg_ctl and
  * pg_resetwal) that is used by it.
@@ -409,6 +357,26 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, cons
 	return dbinfo;
 }
 
+/*
+ * Update connection info of subscriber
+ */
+static void
+update_sub_info(SimpleStringList dbnames, LogicalRepInfo *dbinfo, const char *sub_base_conninfo)
+{
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+}
+
 static PGconn *
 connect_database(const char *conninfo, bool replication_mode)
 {
@@ -1121,14 +1089,26 @@ setup_server_logfile(const char *datadir)
 }
 
 static void
-start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile)
+start_standby_server(const char *pg_ctl_path, const char *datadir, const char *logfile, unsigned short subport, char *sockdir)
 {
 	char	   *pg_ctl_cmd;
 	int			rc;
 
-	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"", pg_ctl_path, datadir, logfile);
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -o \"-p %d\" -l \"%s\"", pg_ctl_path, datadir, subport, logfile);
+
+#if !defined(WIN32)
+	/* prevent TCP/IP connections, restrict socket access */
+	pg_ctl_cmd = psprintf("%s -o \"-c listen_addresses='' -c unix_socket_permissions=0700\"", pg_ctl_cmd);
+
+	/* Have a sockdir?	Tell the postmaster. */
+	if (sockdir)
+		pg_ctl_cmd = psprintf("%s -o \"-c unix_socket_directories=%s\"", pg_ctl_cmd, sockdir);
+#endif
+
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 1);
+
+	is_standby_restarted = true;
 }
 
 static void
@@ -1564,6 +1544,30 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	destroyPQExpBuffer(str);
 }
 
+static char *
+construct_sub_conninfo(char *username, unsigned short subport, char *sockdir)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	if (username)
+		appendPQExpBuffer(buf, "user=%s ", username);
+
+#if !defined(WIN32)
+	if (is_standby_restarted && sockdir)
+		appendPQExpBuffer(buf, "host=%s ", sockdir);
+#endif
+
+	appendPQExpBuffer(buf, "port=%d fallback_application_name=%s",
+					  subport, progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
 int
 main(int argc, char **argv)
 {
@@ -1572,12 +1576,14 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
-		{"subscriber-server", required_argument, NULL, 'S'},
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
 		{"recovery-timeout", required_argument, NULL, 't'},
 		{"retain", no_argument, NULL, 'r'},
 		{"verbose", no_argument, NULL, 'v'},
+		{"username", required_argument, NULL, 'u'},
+		{"port", required_argument, NULL, 'p'},
+		{"socketdir", required_argument, NULL, 's'},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -1604,6 +1610,10 @@ main(int argc, char **argv)
 
 	char		pidfile[MAXPGPATH];
 
+	unsigned short subport = DEF_PGSPORT;
+	char	   *username = NULL;
+	char	   *sockdir = NULL;
+
 	pg_logging_init(argv[0]);
 	pg_logging_set_level(PG_LOG_WARNING);
 	progname = get_progname(argv[0]);
@@ -1628,7 +1638,6 @@ main(int argc, char **argv)
 
 	/* Default settings */
 	opt.subscriber_dir = NULL;
-	opt.sub_conninfo_str = NULL;
 	opt.database_names = (SimpleStringList)
 	{
 		NULL, NULL
@@ -1652,7 +1661,7 @@ main(int argc, char **argv)
 
 	get_restricted_token();
 
-	while ((c = getopt_long(argc, argv, "D:S:d:nrt:v",
+	while ((c = getopt_long(argc, argv, "D:d:nrt:s:u:p:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1660,9 +1669,6 @@ main(int argc, char **argv)
 			case 'D':
 				opt.subscriber_dir = pg_strdup(optarg);
 				break;
-			case 'S':
-				opt.sub_conninfo_str = pg_strdup(optarg);
-				break;
 			case 'd':
 				/* Ignore duplicated database names. */
 				if (!simple_string_list_member(&opt.database_names, optarg))
@@ -1671,6 +1677,9 @@ main(int argc, char **argv)
 					num_dbs++;
 				}
 				break;
+			case 's':
+				sockdir = pg_strdup(optarg);
+				break;
 			case 'n':
 				dry_run = true;
 				break;
@@ -1683,6 +1692,14 @@ main(int argc, char **argv)
 			case 'v':
 				pg_logging_increase_verbosity();
 				break;
+			case 'u':
+				pfree(username);
+				username = pg_strdup(optarg);
+				break;
+			case 'p':
+				if ((subport = atoi(optarg)) <= 0)
+					pg_fatal("invalid old port number");
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -1711,13 +1728,7 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
-	if (opt.sub_conninfo_str == NULL)
-	{
-		pg_log_error("no subscriber connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, dbname_conninfo);
+	sub_base_conninfo = construct_sub_conninfo(username, subport, sockdir);
 	if (sub_base_conninfo == NULL)
 		exit(1);
 
@@ -1746,6 +1757,16 @@ main(int argc, char **argv)
 		}
 	}
 
+	/* Set current dir as default socket dir */
+	if (sockdir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		sockdir = pg_strdup(cwd);
+	}
+
 	/* Obtain a connection string from the target */
 	pub_base_conninfo =
 				get_primary_conninfo_from_target(sub_base_conninfo,
@@ -1896,7 +1917,16 @@ main(int argc, char **argv)
 	if (!dry_run)
 	{
 		pg_log_info("starting the subscriber");
-		start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+		start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log, subport, sockdir);
+	}
+
+	/*
+	 * Update subinfo after the server is restarted
+	 */
+	if (is_standby_restarted)
+	{
+		sub_base_conninfo = construct_sub_conninfo(username, subport, sockdir);
+		update_sub_info(opt.database_names, dbinfo, sub_base_conninfo);
 	}
 
 	/*
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index a6ba58879f..c77f5f9523 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -56,9 +56,9 @@ command_fails(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
-		'--subscriber-server', $node_f->connstr('pg1'),
 		'--database', 'pg1',
-		'--database', 'pg2'
+		'--database', 'pg2',
+		'--port', $node_s->port
 	],
 	'target database is not a physical standby');
 
@@ -67,9 +67,9 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose', '--dry-run',
 		'--pgdata', $node_s->data_dir,
-		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
-		'--database', 'pg2'
+		'--database', 'pg2',
+		'--port', $node_s->port
 	],
 	'run pg_createsubscriber --dry-run on node S');
 
@@ -82,9 +82,9 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_s->data_dir,
-		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
-		'--database', 'pg2'
+		'--database', 'pg2',
+		'--port', $node_s->port
 	],
 	'run pg_createsubscriber on node S');
 
-- 
2.34.1

#117Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#112)
1 attachment(s)
Re: speed up a logical replica setup

On Fri, Feb 2, 2024, at 6:41 AM, Hayato Kuroda (Fujitsu) wrote:

Thanks for updating the patch!

Thanks for taking a look.

I'm still working on the data structures to group options. I don't like the way
it was grouped in v13-0005. There is too many levels to reach database name.
The setup_subscriber() function requires the 3 data structures.

Right, your refactoring looks fewer stack. So I pause to revise my refactoring
patch.

I didn't complete this task yet so I didn't include it in this patch.

The documentation update is almost there. I will include the modifications in
the next patch.

OK. I think it should be modified before native speakers will attend to the
thread.

Same for this one.

Regarding v13-0004, it seems a good UI that's why I wrote a comment about it.
However, it comes with a restriction that requires a similar HBA rule for both
regular and replication connections. Is it an acceptable restriction? We might
paint ourselves into the corner. A reasonable proposal is not to remove this
option. Instead, it should be optional. If it is not provided, primary_conninfo
is used.

I didn't have such a point of view. However, it is not related whether -P exists
or not. Even v14-0001 requires primary to accept both normal/replication connections.
If we want to avoid it, the connection from pg_createsubscriber can be restored
to replication-connection.
(I felt we do not have to use replication protocol even if we change the connection mode)

That's correct. We made a decision to use non physical replication connections
(besides the one used to connect primary <-> standby). Hence, my concern about
a HBA rule falls apart. I'm not convinced that using a modified
primary_conninfo is the only/right answer to fill the subscription connection
string. Physical vs logical replication has different requirements when we talk
about users. The physical replication requires only the REPLICATION privilege.
On the other hand, to create a subscription you must have the privileges of
pg_create_subscription role and also CREATE privilege on the specified
database. Unless, you are always recommending the superuser for this tool, I'm
afraid there will be cases that reusing primary_conninfo will be an issue. The
more I think about this UI, the more I think that, if it is not hundreds of
lines of code, it uses the primary_conninfo the -P is not specified.

The motivation why -P is not needed is to ensure the consistency of nodes.
pg_createsubscriber assumes that the -P option can connect to the upstream node,
but no one checks it. Parsing two connection strings may be a solution but be
confusing. E.g., what if some options are different?
I think using a same parameter is a simplest solution.

Ugh. An error will occur the first time (get_primary_sysid) it tries to connect
to primary.

I found that no one refers the name of temporary slot. Can we remove the variable?

It is gone. I did a refactor in the create_logical_replication_slot function.
Slot name is assigned internally (no need for slot_name or temp_replslot) and
temporary parameter is included.

Initialization by `CreateSubscriberOptions opt = {0};` seems enough.
All values are set to 0x0.

It is. However, I keep the assignments for 2 reasons: (a) there might be
parameters whose default value is not zero, (b) the standard does not say that
a null pointer must be represented by zero and (c) there is no harm in being
paranoid during initial assignment.

You said "target server must be a standby" in [1], but I cannot find checks for it.
IIUC, there are two approaches:
a) check the existence "standby.signal" in the data directory
b) call an SQL function "pg_is_in_recovery"

I applied v16-0004 that implements option (b).

I still think they can be combined as "bindir".

I applied a patch that has a single variable for BINDIR.

WriteRecoveryConfig() writes GUC parameters to postgresql.auto.conf, but not
sure it is good. These settings would remain on new subscriber even after the
pg_createsubscriber. Can we avoid it? I come up with passing these parameters
via pg_ctl -o option, but it requires parsing output from GenerateRecoveryConfig()
(all GUCs must be allign like "-c XXX -c XXX -c XXX...").

I applied a modified version of v16-0006.

Functions arguments should not be struct because they are passing by value.
They should be a pointer. Or, for modify_subscriber_sysid and wait_for_end_recovery,
we can pass a value which would be really used.

Done.

07.
```
static char *get_base_conninfo(char *conninfo, char *dbname,
const char *noderole);
```

Not sure noderole should be passed here. It is used only for the logging.
Can we output string before calling the function?
(The parameter is not needed anymore if -P is removed)

Done.

08.
The terminology is still not consistent. Some functions call the target as standby,
but others call it as subscriber.

The terminology should reflect the actual server role. I'm calling it "standby"
if it is a physical replica and "subscriber" if it is a logical replica. Maybe
"standby" isn't clear enough.

09.
v14 does not work if the standby server has already been set recovery_target*
options. PSA the reproducer. I considered two approaches:

a) raise an ERROR when these parameter were set. check_subscriber() can do it
b) overwrite these GUCs as empty strings.

I prefer (b) that's exactly what you provided in v16-0006.

10.
The execution always fails if users execute --dry-run just before. Because
pg_createsubscriber stops the standby anyway. Doing dry run first is quite normal
use-case, so current implementation seems not user-friendly. How should we fix?
Below bullets are my idea:

a) avoid stopping the standby in case of dry_run: seems possible.
b) accept even if the standby is stopped: seems possible.
c) start the standby at the end of run: how arguments like pg_ctl -l should be specified?

I prefer (a). I applied a slightly modified version of v16-0005.

This new patch contains the following changes:

* check whether the target is really a standby server (0004)
* refactor: pg_create_logical_replication_slot function
* use a single variable for pg_ctl and pg_resetwal directory
* avoid recovery errors applying default settings for some GUCs (0006)
* don't stop/start the standby in dry run mode (0005)
* miscellaneous fixes

I don't understand why v16-0002 is required. In a previous version, this patch
was using connections in logical replication mode. After some discussion we
decided to change it to regular connections and use SQL functions (instead of
replication commands). Is it a requirement for v16-0003?

I started reviewing v16-0007 but didn't finish yet. The general idea is ok.
However, I'm still worried about preventing some use cases if it provides only
the local connection option. What if you want to keep monitoring this instance
while the transformation is happening? Let's say it has a backlog that will
take some time to apply. Unless, you have a local agent, you have no data about
this server until pg_createsubscriber terminates. Even the local agent might
not connect to the server unless you use the current port.

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v17-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchtext/x-patch; name="=?UTF-8?Q?v17-0001-Creates-a-new-logical-replica-from-a-standby-ser.patc?= =?UTF-8?Q?h?="Download
From ae5a0efe6b0056fb108c3764fe99caebc0554f76 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v17] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica
into a logical replica. It runs on the target server and should be able
to connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.  Stop the
target server. Change the system identifier from the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  320 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1869 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   44 +
 .../t/041_pg_createsubscriber_standby.pl      |  135 ++
 src/tools/pgindent/typedefs.list              |    2 +
 10 files changed, 2399 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f5238771b7
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,320 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..9628f32a3e
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1869 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_authid_d.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+/* Command-line options */
+typedef struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *sub_conninfo_str;	/* subscriber connection string */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+} CreateSubscriberOptions;
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname);
+static char *get_bin_directory(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile);
+static void stop_standby_server(const char *pg_bin_dir, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the directory that the pg_createsubscriber is in. Since it uses other
+ * PostgreSQL binaries (pg_ctl and pg_resetwal), the directory is used to build
+ * the full path for it.
+ */
+static char *
+get_bin_directory(const char *path)
+{
+	char		full_path[MAXPGPATH];
+	char	   *dirname;
+	char	   *sep;
+
+	if (find_my_exec(path, full_path) < 0)
+	{
+		pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+					 "same directory as \"%s\".\n",
+					 "pg_ctl", progname, full_path);
+		pg_log_error_hint("Check your installation.");
+		exit(1);
+	}
+
+	/*
+	 * Strip the file name from the path. It will be used to build the full
+	 * path for binaries used by this tool.
+	 */
+	dirname = pg_malloc(MAXPGPATH);
+	sep = strrchr(full_path, 'p');
+	Assert(sep != NULL);
+	strlcpy(dirname, full_path, sep - full_path);
+
+	pg_log_debug("pg_ctl path is:  %s/%s", dirname, "pg_ctl");
+	pg_log_debug("pg_resetwal path is:  %s/%s", dirname, "pg_resetwal");
+
+	return dirname;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: %s", PQresultErrorMessage(res));
+	}
+	if (PQntuples(res) != 1)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: got %d rows, expected %d row",
+				 PQntuples(res), 1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\" > \"%s\"", pg_bin_dir, opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical max_replication_slots >= current + number of dbs to
+	 * be converted max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/* The target server must be a standby */
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("The target server is not a standby");
+		return false;
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str, "SELECT pg_has_role(current_user, %u, 'MEMBER'), has_database_privilege(current_user, '%s', 'CREATE'), has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')", ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"", "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		return false;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical replication and
+ * enable the subscriptions. That's the last step for logical repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/*
+	 * This temporary replication slot is only used for catchup purposes.
+	 */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+	{
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it is
+ * run.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"", pg_bin_dir, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_bin_dir, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", pg_bin_dir, datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_fatal("could not obtain recovery progress");
+
+		if (PQntuples(res) != 1)
+			pg_fatal("unexpected result from pg_is_in_recovery function");
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_bin_dir, opt->subscriber_dir);
+			pg_fatal("recovery timed out");
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain publication information: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
+				 PQntuples(res), 1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not enable subscription \"%s\": %s", dbinfo->subname,
+					 PQerrorMessage(conn));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_bin_dir = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.sub_conninfo_str = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				opt.sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	pg_bin_dir = get_bin_directory(argv[0]);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(opt.subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure. */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server. */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		if (!dry_run)
+			stop_standby_server(pg_bin_dir, opt.subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0], true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	if (!dry_run)
+		start_standby_server(pg_bin_dir, opt.subscriber_dir, server_start_log);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_bin_dir, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+	if (!dry_run)
+		stop_standby_server(pg_bin_dir, opt.subscriber_dir);
+
+	/*
+	 * Change system identifier from subscriber.
+	 */
+	modify_subscriber_sysid(pg_bin_dir, &opt);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..0f02b1bfac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..2db41cbc9b
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,135 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91433d439b..102971164f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.30.2

#118Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Shlok Kyal (#116)
RE: speed up a logical replica setup

Dear Shlok,

Thanks for updating the patch!

I have created a topup patch 0007 on top of v15-0006.

I revived the patch which removes -S option and adds some options
instead. The patch add option for --port, --username and --socketdir.
This patch also ensures that anyone cannot connect to the standby
during the pg_createsubscriber, by setting listen_addresses,
unix_socket_permissions, and unix_socket_directories.

IIUC, there are two reasons why removing -S may be good:

* force users to specify a local-connection, and
* avoid connection establishment on standby during the pg_createsubscriber.

First bullet is still valid, but we should describe that like pg_upgrade:

pg_upgrade will connect to the old and new servers several times, so you might
want to set authentication to peer in pg_hba.conf or use a ~/.pgpass file
(see Section 33.16).

Regarding the second bullet, this patch cannot ensure it. pg_createsubscriber
just accepts port number which has been already accepted by the standby, it does
not change the port number. So any local applications on the standby server can
connect to the server and may change primary_conninfo, primary_slot_name, data, etc.
So...what should be? How do other think?

Beside, 0007 does not follow the recent changes on 0001. E.g., options should be
record in CreateSubscriberOptions. Also, should we check the privilege of socket
directory?

[1]: /messages/by-id/TY3PR01MB988902B992A4F2E99E1385EDF56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#119Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#117)
RE: speed up a logical replica setup

Dear Euler,

Sorry for posting e-mail each other. I will read your patch
but I want to reply one of yours.

I started reviewing v16-0007 but didn't finish yet. The general idea is ok.
However, I'm still worried about preventing some use cases if it provides only
the local connection option. What if you want to keep monitoring this instance
while the transformation is happening? Let's say it has a backlog that will
take some time to apply. Unless, you have a local agent, you have no data about
this server until pg_createsubscriber terminates. Even the local agent might
not connect to the server unless you use the current port.

(IIUC, 0007 could not overwrite a port number - refactoring is needed)

Ah, actually I did not have such a point of view. Assuming that changed port number
can avoid connection establishments, there are four options:
a) Does not overwrite port and listen_addresses. This allows us to monitor by
external agents, but someone can modify GUCs and data during operations.
b) Overwrite port but do not do listen_addresses. Not sure it is useful...
c) Overwrite listen_addresses but do not do port. This allows us to monitor by
local agents, and we can partially protect the database. But there is still a
room.
d) Overwrite both port and listen_addresses. This can protect databases perfectly
but no one can monitor.

Hmm, which one should be chosen? I prefer c) or d).
Do you know how pglogical_create_subscriber does?

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#120Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#117)
1 attachment(s)
RE: speed up a logical replica setup

Dear Euler,

I have not finished reviewing, but I can reply to you first.

I didn't complete this task yet so I didn't include it in this patch.

Just to confirm - No need to forcibly include my refactoring patch: you can
modify based on your idea.

That's correct. We made a decision to use non physical replication connections
(besides the one used to connect primary <-> standby). Hence, my concern about
a HBA rule falls apart. I'm not convinced that using a modified
primary_conninfo is the only/right answer to fill the subscription connection
string. Physical vs logical replication has different requirements when we talk
about users. The physical replication requires only the REPLICATION privilege.
On the other hand, to create a subscription you must have the privileges of
pg_create_subscription role and also CREATE privilege on the specified
database. Unless, you are always recommending the superuser for this tool, I'm
afraid there will be cases that reusing primary_conninfo will be an issue. The
more I think about this UI, the more I think that, if it is not hundreds of
lines of code, it uses the primary_conninfo the -P is not specified.

Valid point. It is one of the best practice that users set minimal privileges
for each accounts. However...

Ugh. An error will occur the first time (get_primary_sysid) it tries to connect
to primary.

I'm not sure it is correct. I think there are several patterns.

a) -P option specified a ghost server, i.e., pg_createsubscriber cannot connect to
anything. In this case, get_primary_sysid() would be failed because
connect_database() would be failed.

b) -P option specified an existing server, but it is not my upstream.
get_primary_sysid() would be suceeded.
In this case, there are two furher branches:

b-1) nodes have different system_identifier. pg_createsubscriber would raise
an ERROR and stop.
b-2) nodes have the same system_identifier (e.g., nodes were generated by the
same master). In this case, current checkings would be passed but
pg_createsubscriber would stuck or reach timeout. PSA the reproducer.

Can pg_createsubscriber check the relation two nodes have been connected by
replication protocol? Or, can we assume that all the node surely have different
system_identifier?

It is. However, I keep the assignments for 2 reasons: (a) there might be
parameters whose default value is not zero, (b) the standard does not say that
a null pointer must be represented by zero and (c) there is no harm in being
paranoid during initial assignment.

Hmn, so, no need to use `= {0};`, but up to you. Also, according to C99 standard[1]https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf,
NULL seemed to be defined as 0x0:
```
An integer constant expression with the value 0, or such an expression cast to type
void *, is called a null pointer constant.
If a null pointer constant is converted to a
pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal
to a pointer to any object or function.
```

WriteRecoveryConfig() writes GUC parameters to postgresql.auto.conf, but not
sure it is good. These settings would remain on new subscriber even after the
pg_createsubscriber. Can we avoid it? I come up with passing these parameters
via pg_ctl -o option, but it requires parsing output from GenerateRecoveryConfig()
(all GUCs must be allign like "-c XXX -c XXX -c XXX...").

I applied a modified version of v16-0006.

Sorry for confusing, it is not a solution. This approach writes parameter
settings to postgresql.auto.conf, and they are never removed. Overwritten
settings would remain.

I don't understand why v16-0002 is required. In a previous version, this patch
was using connections in logical replication mode. After some discussion we
decided to change it to regular connections and use SQL functions (instead of
replication commands). Is it a requirement for v16-0003?

No, it is not required by 0003. I forgot the decision that we force DBAs to open
normal connection establishments for pg_createsubscriber. Please ignore...

[1]: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

Attachments:

test_0207.shapplication/octet-stream; name=test_0207.shDownload
#121Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#119)
Re: speed up a logical replica setup

On Wed, Feb 7, 2024, at 2:31 AM, Hayato Kuroda (Fujitsu) wrote:

Ah, actually I did not have such a point of view. Assuming that changed port number
can avoid connection establishments, there are four options:
a) Does not overwrite port and listen_addresses. This allows us to monitor by
external agents, but someone can modify GUCs and data during operations.
b) Overwrite port but do not do listen_addresses. Not sure it is useful...
c) Overwrite listen_addresses but do not do port. This allows us to monitor by
local agents, and we can partially protect the database. But there is still a
room.
d) Overwrite both port and listen_addresses. This can protect databases perfectly
but no one can monitor.

Remember the target server was a standby (read only access). I don't expect an
application trying to modify it; unless it is a buggy application. Regarding
GUCs, almost all of them is PGC_POSTMASTER (so it cannot be modified unless the
server is restarted). The ones that are not PGC_POSTMASTER, does not affect the
pg_createsubscriber execution [1]https://www.postgresql.org/docs/current/logical-replication-config.html.

postgres=# select name, setting, context from pg_settings where name in ('max_replication_slots', 'max_logical_replication_workers', 'max_worker_processes', 'max_sync_workers_per_subscription', 'max_parallel_apply_workers_per_subscription');
name | setting | context
---------------------------------------------+---------+------------
max_logical_replication_workers | 4 | postmaster
max_parallel_apply_workers_per_subscription | 2 | sighup
max_replication_slots | 10 | postmaster
max_sync_workers_per_subscription | 2 | sighup
max_worker_processes | 8 | postmaster
(5 rows)

I'm just pointing out that this case is a different from pg_upgrade (from which
this idea was taken). I'm not saying that's a bad idea. I'm just arguing that
you might be preventing some access read only access (monitoring) when it is
perfectly fine to connect to the database and execute queries. As I said
before, the current UI allows anyone to setup the standby to accept only local
connections. Of course, it is an extra step but it is possible. However, once
you apply v16-0007, there is no option but use only local connection during the
transformation. Is it an acceptable limitation?

Under reflection, I don't expect a big window

1802 /*
1803 * Start subscriber and wait until accepting connections.
1804 */
1805 pg_log_info("starting the subscriber");
1806 if (!dry_run)
1807 start_standby_server(pg_bin_dir, opt.subscriber_dir, server_start_log);
1808
1809 /*
1810 * Waiting the subscriber to be promoted.
1811 */
1812 wait_for_end_recovery(dbinfo[0].subconninfo, pg_bin_dir, &opt);
.
.
.
1845 /*
1846 * Stop the subscriber.
1847 */
1848 pg_log_info("stopping the subscriber");
1849 if (!dry_run)
1850 stop_standby_server(pg_bin_dir, opt.subscriber_dir);

... mainly because the majority of the time will be wasted in
wait_for_end_recovery() if the server takes some time to reach consistent state
(and during this phase it cannot accept connections anyway). Aren't we worrying
too much about it?

Hmm, which one should be chosen? I prefer c) or d).
Do you know how pglogical_create_subscriber does?

pglogical_create_subscriber does nothing [2]https://github.com/2ndQuadrant/pglogical/blob/REL2_x_STABLE/pglogical_create_subscriber.c#L488[3]https://github.com/2ndQuadrant/pglogical/blob/REL2_x_STABLE/pglogical_create_subscriber.c#L529.

[1]: https://www.postgresql.org/docs/current/logical-replication-config.html
[2]: https://github.com/2ndQuadrant/pglogical/blob/REL2_x_STABLE/pglogical_create_subscriber.c#L488
[3]: https://github.com/2ndQuadrant/pglogical/blob/REL2_x_STABLE/pglogical_create_subscriber.c#L529

--
Euler Taveira
EDB https://www.enterprisedb.com/

#122Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#121)
RE: speed up a logical replica setup

Dear Euler,

Remember the target server was a standby (read only access). I don't expect an
application trying to modify it; unless it is a buggy application.

What if the client modifies the data just after the promotion?
Naively considered, all the changes can be accepted, but are there any issues?

Regarding
GUCs, almost all of them is PGC_POSTMASTER (so it cannot be modified unless the
server is restarted). The ones that are not PGC_POSTMASTER, does not affect the
pg_createsubscriber execution [1].

IIUC, primary_conninfo and primary_slot_name is PGC_SIGHUP.

I'm just pointing out that this case is a different from pg_upgrade (from which
this idea was taken). I'm not saying that's a bad idea. I'm just arguing that
you might be preventing some access read only access (monitoring) when it is
perfectly fine to connect to the database and execute queries. As I said
before, the current UI allows anyone to setup the standby to accept only local
connections. Of course, it is an extra step but it is possible. However, once
you apply v16-0007, there is no option but use only local connection during the
transformation. Is it an acceptable limitation?

My remained concern is written above. If they do not problematic we may not have
to restrict them for now. At that time, changes

1) overwriting a port number,
2) setting listen_addresses = ''

are not needed, right? IIUC inconsistency of -P may be still problematic.

pglogical_create_subscriber does nothing [2][3].

Oh, thanks.
Just to confirm - pglogical set shared_preload_libraries to '', should we follow or not?

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#123Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#117)
5 attachment(s)
RE: speed up a logical replica setup

Dear Euler,

Here are my minor comments for 17.

01.
```
/* Options */
static const char *progname;

static char *primary_slot_name = NULL;
static bool dry_run = false;

static bool success = false;

static LogicalRepInfo *dbinfo;
static int num_dbs = 0;
```

The comment seems out-of-date. There is only one option.

02. check_subscriber and check_publisher

Missing pg_catalog prefix in some lines.

03. get_base_conninfo

I think dbname would not be set. IIUC, dbname should be a pointer of the pointer.

04.

I check the coverage and found two functions have been never called:
- drop_subscription
- drop_replication_slot

Also, some cases were not tested. Below bullet showed notable ones for me.
(Some of them would not be needed based on discussions)

* -r is specified
* -t is specified
* -P option contains dbname
* -d is not specified
* GUC settings are wrong
* primary_slot_name is specified on the standby
* standby server is not working

In feature level, we may able to check the server log is surely removed in case
of success.

So, which tests should be added? drop_subscription() is called only when the
cleanup phase, so it may be difficult to test. According to others, it seems that
-r and -t are not tested. GUC-settings have many test cases so not sure they
should be. Based on this, others can be tested.

PSA my top-up patch set.

V18-0001: same as your patch, v17-0001.
V18-0002: modify the alignment of codes.
V18-0003: change an argument of get_base_conninfo. Per comment 3.
=== experimental patches ===
V18-0004: Add testcases per comment 4.
V18-0005: Remove -P option. I'm not sure it should be needed, but I made just in case.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

Attachments:

v18-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchapplication/octet-stream; name=v18-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchDownload
From cd301506991059a796504a10ff3fb783cdd64c7b Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v18 1/5] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica
into a logical replica. It runs on the target server and should be able
to connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.  Stop the
target server. Change the system identifier from the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  320 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1869 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   44 +
 .../t/041_pg_createsubscriber_standby.pl      |  135 ++
 src/tools/pgindent/typedefs.list              |    2 +
 10 files changed, 2399 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f5238771b7
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,320 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..9628f32a3e
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1869 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_authid_d.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+/* Command-line options */
+typedef struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *sub_conninfo_str;	/* subscriber connection string */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+} CreateSubscriberOptions;
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname);
+static char *get_bin_directory(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile);
+static void stop_standby_server(const char *pg_bin_dir, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the directory that the pg_createsubscriber is in. Since it uses other
+ * PostgreSQL binaries (pg_ctl and pg_resetwal), the directory is used to build
+ * the full path for it.
+ */
+static char *
+get_bin_directory(const char *path)
+{
+	char		full_path[MAXPGPATH];
+	char	   *dirname;
+	char	   *sep;
+
+	if (find_my_exec(path, full_path) < 0)
+	{
+		pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+					 "same directory as \"%s\".\n",
+					 "pg_ctl", progname, full_path);
+		pg_log_error_hint("Check your installation.");
+		exit(1);
+	}
+
+	/*
+	 * Strip the file name from the path. It will be used to build the full
+	 * path for binaries used by this tool.
+	 */
+	dirname = pg_malloc(MAXPGPATH);
+	sep = strrchr(full_path, 'p');
+	Assert(sep != NULL);
+	strlcpy(dirname, full_path, sep - full_path);
+
+	pg_log_debug("pg_ctl path is:  %s/%s", dirname, "pg_ctl");
+	pg_log_debug("pg_resetwal path is:  %s/%s", dirname, "pg_resetwal");
+
+	return dirname;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: %s", PQresultErrorMessage(res));
+	}
+	if (PQntuples(res) != 1)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: got %d rows, expected %d row",
+				 PQntuples(res), 1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\" > \"%s\"", pg_bin_dir, opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical max_replication_slots >= current + number of dbs to
+	 * be converted max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/* The target server must be a standby */
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("The target server is not a standby");
+		return false;
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str, "SELECT pg_has_role(current_user, %u, 'MEMBER'), has_database_privilege(current_user, '%s', 'CREATE'), has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')", ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"", "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		return false;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical replication and
+ * enable the subscriptions. That's the last step for logical repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/*
+	 * This temporary replication slot is only used for catchup purposes.
+	 */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+	{
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it is
+ * run.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"", pg_bin_dir, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_bin_dir, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", pg_bin_dir, datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_fatal("could not obtain recovery progress");
+
+		if (PQntuples(res) != 1)
+			pg_fatal("unexpected result from pg_is_in_recovery function");
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_bin_dir, opt->subscriber_dir);
+			pg_fatal("recovery timed out");
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain publication information: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
+				 PQntuples(res), 1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not enable subscription \"%s\": %s", dbinfo->subname,
+					 PQerrorMessage(conn));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_bin_dir = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.sub_conninfo_str = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				opt.sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	pg_bin_dir = get_bin_directory(argv[0]);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(opt.subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure. */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server. */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		if (!dry_run)
+			stop_standby_server(pg_bin_dir, opt.subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0], true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	if (!dry_run)
+		start_standby_server(pg_bin_dir, opt.subscriber_dir, server_start_log);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_bin_dir, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+	if (!dry_run)
+		stop_standby_server(pg_bin_dir, opt.subscriber_dir);
+
+	/*
+	 * Change system identifier from subscriber.
+	 */
+	modify_subscriber_sysid(pg_bin_dir, &opt);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..0f02b1bfac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..2db41cbc9b
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,135 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91433d439b..102971164f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.43.0

v18-0002-Follow-coding-conversions.patchapplication/octet-stream; name=v18-0002-Follow-coding-conversions.patchDownload
From 122000e06a309a30493ab6d97eb336048408c97e Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 8 Feb 2024 12:34:52 +0000
Subject: [PATCH v18 2/5] Follow coding conversions

---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 393 +++++++++++-------
 .../t/040_pg_createsubscriber.pl              |  11 +-
 .../t/041_pg_createsubscriber_standby.pl      |  24 +-
 3 files changed, 256 insertions(+), 172 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 9628f32a3e..7a5ef4f251 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -37,12 +37,12 @@
 /* Command-line options */
 typedef struct CreateSubscriberOptions
 {
-	char	   *subscriber_dir; /* standby/subscriber data directory */
-	char	   *pub_conninfo_str;	/* publisher connection string */
-	char	   *sub_conninfo_str;	/* subscriber connection string */
+	char	   *subscriber_dir; 		/* standby/subscriber data directory */
+	char	   *pub_conninfo_str;		/* publisher connection string */
+	char	   *sub_conninfo_str;		/* subscriber connection string */
 	SimpleStringList database_names;	/* list of database names */
-	bool		retain;			/* retain log file? */
-	int			recovery_timeout;	/* stop recovery after this time */
+	bool		retain;					/* retain log file? */
+	int			recovery_timeout;		/* stop recovery after this time */
 } CreateSubscriberOptions;
 
 typedef struct LogicalRepInfo
@@ -66,29 +66,38 @@ static char *get_base_conninfo(char *conninfo, char *dbname);
 static char *get_bin_directory(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
-static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+										  const char *pub_base_conninfo,
+										  const char *sub_base_conninfo);
 static PGconn *connect_database(const char *conninfo);
 static void disconnect_database(PGconn *conn);
 static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
-static void modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static void modify_subscriber_sysid(const char *pg_bin_dir,
+									CreateSubscriberOptions *opt);
 static bool check_publisher(LogicalRepInfo *dbinfo);
 static bool setup_publisher(LogicalRepInfo *dbinfo);
 static bool check_subscriber(LogicalRepInfo *dbinfo);
-static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
-static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+static bool setup_subscriber(LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn,
+											 LogicalRepInfo *dbinfo,
 											 bool temporary);
-static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								  const char *slot_name);
 static char *setup_server_logfile(const char *datadir);
-static void start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile);
+static void start_standby_server(const char *pg_bin_dir, const char *datadir,
+								 const char *logfile);
 static void stop_standby_server(const char *pg_bin_dir, const char *datadir);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
-static void wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
+								  CreateSubscriberOptions *opt);
 static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo,
+									 const char *lsn);
 static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 
 #define	USEC_PER_SEC	1000000
@@ -115,7 +124,8 @@ enum WaitPMResult
 
 
 /*
- * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
  *
  * Replication slots, publications and subscriptions are created. Depending on
  * the step it failed, it should remove the already created objects if it is
@@ -184,11 +194,13 @@ usage(void)
 /*
  * Validate a connection string. Returns a base connection string that is a
  * connection string without a database name.
+ *
  * Since we might process multiple databases, each database name will be
- * appended to this base connection string to provide a final connection string.
- * If the second argument (dbname) is not null, returns dbname if the provided
- * connection string contains it. If option --database is not provided, uses
- * dbname as the only database to setup the logical replica.
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
  * It is the caller's responsibility to free the returned connection string and
  * dbname.
  */
@@ -291,7 +303,8 @@ check_data_directory(const char *datadir)
 		if (errno == ENOENT)
 			pg_log_error("data directory \"%s\" does not exist", datadir);
 		else
-			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+			pg_log_error("could not access directory \"%s\": %s", datadir,
+						 strerror(errno));
 
 		return false;
 	}
@@ -299,7 +312,8 @@ check_data_directory(const char *datadir)
 	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
 	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
 	{
-		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		pg_log_error("directory \"%s\" is not a database cluster directory",
+					 datadir);
 		return false;
 	}
 
@@ -334,7 +348,8 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
  * Store publication and subscription information.
  */
 static LogicalRepInfo *
-store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo)
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
 {
 	LogicalRepInfo *dbinfo;
 	SimpleStringListCell *cell;
@@ -346,7 +361,7 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, cons
 	{
 		char	   *conninfo;
 
-		/* Publisher. */
+		/* Fill attributes related with the publisher */
 		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
 		dbinfo[i].pubconninfo = conninfo;
 		dbinfo[i].dbname = cell->val;
@@ -355,7 +370,7 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, cons
 		dbinfo[i].made_subscription = false;
 		/* other struct fields will be filled later. */
 
-		/* Subscriber. */
+		/* Same as subscriber */
 		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
 		dbinfo[i].subconninfo = conninfo;
 
@@ -374,15 +389,17 @@ connect_database(const char *conninfo)
 	conn = PQconnectdb(conninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
 	{
-		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
 		return NULL;
 	}
 
-	/* secure search_path */
+	/* Secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
 		return NULL;
 	}
 	PQclear(res);
@@ -420,7 +437,8 @@ get_primary_sysid(const char *conninfo)
 	{
 		PQclear(res);
 		disconnect_database(conn);
-		pg_fatal("could not get system identifier: %s", PQresultErrorMessage(res));
+		pg_fatal("could not get system identifier: %s",
+				 PQresultErrorMessage(res));
 	}
 	if (PQntuples(res) != 1)
 	{
@@ -432,7 +450,8 @@ get_primary_sysid(const char *conninfo)
 
 	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
 
-	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
 
 	PQclear(res);
 	disconnect_database(conn);
@@ -460,7 +479,8 @@ get_standby_sysid(const char *datadir)
 
 	sysid = cf->system_identifier;
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
 
 	pfree(cf);
 
@@ -501,11 +521,13 @@ modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt)
 	if (!dry_run)
 		update_controlfile(opt->subscriber_dir, cf, true);
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+	pg_log_info("system identifier is %llu on subscriber", 
+				(unsigned long long) cf->system_identifier);
 
 	pg_log_info("running pg_resetwal on the subscriber");
 
-	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\" > \"%s\"", pg_bin_dir, opt->subscriber_dir, DEVNULL);
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\" > \"%s\"", pg_bin_dir,
+					   opt->subscriber_dir, DEVNULL);
 
 	pg_log_debug("command is: %s", cmd_str);
 
@@ -541,10 +563,12 @@ setup_publisher(LogicalRepInfo *dbinfo)
 			exit(1);
 
 		res = PQexec(conn,
-					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
 			return false;
 		}
 
@@ -555,7 +579,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
 			return false;
 		}
 
-		/* Remember database OID. */
+		/* Remember database OID */
 		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
 
 		PQclear(res);
@@ -565,7 +589,8 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
 		 * '\0').
 		 */
-		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
 		dbinfo[i].pubname = pg_strdup(pubname);
 
 		/*
@@ -578,10 +603,10 @@ setup_publisher(LogicalRepInfo *dbinfo)
 
 		/*
 		 * Build the replication slot name. The name must not exceed
-		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
-		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
-		 * probability of collision. By default, subscription name is used as
-		 * replication slot name.
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42 characters
+		 * (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the probability
+		 * of collision. By default, subscription name is used as replication
+		 * slot name.
 		 */
 		snprintf(replslotname, sizeof(replslotname),
 				 "pg_createsubscriber_%u_%d",
@@ -589,9 +614,11 @@ setup_publisher(LogicalRepInfo *dbinfo)
 				 (int) getpid());
 		dbinfo[i].subname = pg_strdup(replslotname);
 
-		/* Create replication slot on publisher. */
-		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL || dry_run)
-			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
 		else
 			return false;
 
@@ -624,24 +651,37 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * Since these parameters are not a requirement for physical replication,
 	 * we should check it to make sure it won't fail.
 	 *
-	 * wal_level = logical max_replication_slots >= current + number of dbs to
-	 * be converted max_wal_senders >= current + number of dbs to be converted
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo);
 	if (conn == NULL)
 		exit(1);
 
 	res = PQexec(conn,
-				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
-				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
-				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
-				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
-				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
-				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+				 "WITH wl AS "
+				 " (SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "  WHERE name = 'wal_level'),"
+				 "total_mrs AS "
+				 " (SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "  WHERE name = 'max_replication_slots'),"
+				 "cur_mrs AS "
+				 " (SELECT count(*) AS cmrs "
+				 "  FROM pg_catalog.pg_replication_slots),"
+				 "total_mws AS "
+				 " (SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "  WHERE name = 'max_wal_senders'),"
+				 "cur_mws AS "
+				 " (SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "  WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
 		return false;
 	}
 
@@ -668,14 +708,17 @@ check_publisher(LogicalRepInfo *dbinfo)
 	if (primary_slot_name)
 	{
 		appendPQExpBuffer(str,
-						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+						  "SELECT 1 FROM pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
 
 		pg_log_debug("command is: %s", str->data);
 
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
 			return false;
 		}
 
@@ -688,9 +731,8 @@ check_publisher(LogicalRepInfo *dbinfo)
 			return false;
 		}
 		else
-		{
-			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
-		}
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
 
 		PQclear(res);
 	}
@@ -705,15 +747,19 @@ check_publisher(LogicalRepInfo *dbinfo)
 
 	if (max_repslots - cur_repslots < num_dbs)
 	{
-		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
 		return false;
 	}
 
 	if (max_walsenders - cur_walsenders < num_dbs)
 	{
-		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
-		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
 		return false;
 	}
 
@@ -760,7 +806,14 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	 * pg_create_subscription role and CREATE privileges on the specified
 	 * database.
 	 */
-	appendPQExpBuffer(str, "SELECT pg_has_role(current_user, %u, 'MEMBER'), has_database_privilege(current_user, '%s', 'CREATE'), has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')", ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "       pg_catalog.has_database_privilege(current_user, "
+					  "                                         '%s', 'CREATE'), "
+					  "       pg_catalog.has_function_privilege(current_user, "
+					  "                                         'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', "
+					  "                                         'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -768,7 +821,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not obtain access privilege information: %s", PQresultErrorMessage(res));
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
 		return false;
 	}
 
@@ -786,7 +840,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	}
 	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
 	{
-		pg_log_error("permission denied for function \"%s\"", "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
 		return false;
 	}
 
@@ -798,16 +853,22 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	 * Since these parameters are not a requirement for physical replication,
 	 * we should check it to make sure it won't fail.
 	 *
-	 * max_replication_slots >= number of dbs to be converted
-	 * max_logical_replication_workers >= number of dbs to be converted
-	 * max_worker_processes >= 1 + number of dbs to be converted
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
 	 */
 	res = PQexec(conn,
-				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+				 "SELECT setting FROM pg_settings WHERE name IN ( "
+				 "       'max_logical_replication_workers', "
+				 "       'max_replication_slots', "
+				 "       'max_worker_processes', "
+				 "       'primary_slot_name') "
+				 "ORDER BY name");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
 		return false;
 	}
 
@@ -817,7 +878,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
 		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
 
-	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
 	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
 	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
 	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
@@ -828,22 +890,28 @@ check_subscriber(LogicalRepInfo *dbinfo)
 
 	if (max_repslots < num_dbs)
 	{
-		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
 		return false;
 	}
 
 	if (max_lrworkers < num_dbs)
 	{
-		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
-		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
 		return false;
 	}
 
 	if (max_wprocs < num_dbs + 1)
 	{
-		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
-		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
 		return false;
 	}
 
@@ -851,8 +919,9 @@ check_subscriber(LogicalRepInfo *dbinfo)
 }
 
 /*
- * Create the subscriptions, adjust the initial location for logical replication and
- * enable the subscriptions. That's the last step for logical repliation setup.
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
  */
 static bool
 setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
@@ -875,10 +944,10 @@ setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
 
 		create_subscription(conn, &dbinfo[i]);
 
-		/* Set the replication progress to the correct LSN. */
+		/* Set the replication progress to the correct LSN */
 		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
 
-		/* Enable subscription. */
+		/* Enable subscription */
 		enable_subscription(conn, &dbinfo[i]);
 
 		disconnect_database(conn);
@@ -904,22 +973,23 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 
 	Assert(conn != NULL);
 
-	/*
-	 * This temporary replication slot is only used for catchup purposes.
-	 */
+	/* This temporary replication slot is only used for catchup purposes */
 	if (temporary)
 	{
 		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
 				 (int) getpid());
 	}
 	else
-	{
 		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
-	}
 
-	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
 
-	appendPQExpBuffer(str, "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+	appendPQExpBuffer(str,
+					  "SELECT lsn "
+					  "FROM pg_create_logical_replication_slot('%s', '%s', "
+					  "                                        '%s', false, "
+					  "                                        false)",
 					  slot_name, "pgoutput", temporary ? "true" : "false");
 
 	pg_log_debug("command is: %s", str->data);
@@ -929,13 +999,14 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
 						 PQresultErrorMessage(res));
 			return lsn;
 		}
 	}
 
-	/* for cleanup purposes */
+	/* For cleanup purposes */
 	if (!temporary)
 		dbinfo->made_replslot = true;
 
@@ -951,14 +1022,16 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 }
 
 static void
-drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+					  const char *slot_name)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
 
@@ -968,8 +1041,8 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
-			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						 PQerrorMessage(conn));
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1004,7 +1077,7 @@ setup_server_logfile(const char *datadir)
 	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
 		pg_fatal("could not create directory \"%s\": %m", base_dir);
 
-	/* append timestamp with ISO 8601 format. */
+	/* Append timestamp with ISO 8601 format */
 	gettimeofday(&time, NULL);
 	tt = (time_t) time.tv_sec;
 	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
@@ -1012,7 +1085,8 @@ setup_server_logfile(const char *datadir)
 			 ".%03d", (int) (time.tv_usec / 1000));
 
 	filename = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir,
+				   PGS_OUTPUT_DIR, timebuf);
 	if (len >= MAXPGPATH)
 		pg_fatal("log file path is too long");
 
@@ -1020,12 +1094,14 @@ setup_server_logfile(const char *datadir)
 }
 
 static void
-start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile)
+start_standby_server(const char *pg_bin_dir, const char *datadir,
+					 const char *logfile)
 {
 	char	   *pg_ctl_cmd;
 	int			rc;
 
-	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"", pg_bin_dir, datadir, logfile);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"",
+						  pg_bin_dir, datadir, logfile);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 1);
 }
@@ -1036,7 +1112,8 @@ stop_standby_server(const char *pg_bin_dir, const char *datadir)
 	char	   *pg_ctl_cmd;
 	int			rc;
 
-	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", pg_bin_dir, datadir);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", pg_bin_dir,
+						  datadir);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 0);
 }
@@ -1056,7 +1133,8 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
 		else if (WIFSIGNALED(rc))
 		{
 #if defined(WIN32)
-			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
 			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
 #else
 			pg_log_error("pg_ctl was terminated by signal %d: %s",
@@ -1085,7 +1163,8 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt)
+wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
+					  CreateSubscriberOptions *opt)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -1124,16 +1203,14 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscr
 			break;
 		}
 
-		/*
-		 * Bail out after recovery_timeout seconds if this option is set.
-		 */
+		/* Bail out after recovery_timeout seconds if this option is set */
 		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
 		{
 			stop_standby_server(pg_bin_dir, opt->subscriber_dir);
 			pg_fatal("recovery timed out");
 		}
 
-		/* Keep waiting. */
+		/* Keep waiting */
 		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
 
 		timer += WAIT_INTERVAL;
@@ -1158,9 +1235,10 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	/* Check if the publication needs to be created. */
+	/* Check if the publication needs to be created */
 	appendPQExpBuffer(str,
-					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  "SELECT puballtables FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
 					  dbinfo->pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1204,9 +1282,11 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	PQclear(res);
 	resetPQExpBuffer(str);
 
-	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
 
-	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1241,7 +1321,8 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
 
@@ -1251,7 +1332,8 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1279,11 +1361,13 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
-					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  "WITH (create_slot = false, copy_data = false, " 
+					  "      enabled = false)",
 					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
@@ -1319,7 +1403,8 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
 
@@ -1329,7 +1414,8 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1359,7 +1445,9 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	Assert(conn != NULL);
 
 	appendPQExpBuffer(str,
-					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1381,7 +1469,8 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	if (dry_run)
 	{
 		suboid = InvalidOid;
-		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
 	}
 	else
 	{
@@ -1402,7 +1491,9 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 
 	resetPQExpBuffer(str);
 	appendPQExpBuffer(str,
-					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', "
+					  "                                                '%s')",
+					  originname, lsnstr);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1437,7 +1528,8 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
 
@@ -1449,8 +1541,8 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			PQfinish(conn);
-			pg_fatal("could not enable subscription \"%s\": %s", dbinfo->subname,
-					 PQerrorMessage(conn));
+			pg_fatal("could not enable subscription \"%s\": %s",
+					 dbinfo->subname, PQerrorMessage(conn));
 		}
 
 		PQclear(res);
@@ -1563,7 +1655,7 @@ main(int argc, char **argv)
 				opt.sub_conninfo_str = pg_strdup(optarg);
 				break;
 			case 'd':
-				/* Ignore duplicated database names. */
+				/* Ignore duplicated database names */
 				if (!simple_string_list_member(&opt.database_names, optarg))
 				{
 					simple_string_list_append(&opt.database_names, optarg);
@@ -1584,7 +1676,8 @@ main(int argc, char **argv)
 				break;
 			default:
 				/* getopt_long already emitted a complaint */
-				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				pg_log_error_hint("Try \"%s --help\" for more information.",
+								  progname);
 				exit(1);
 		}
 	}
@@ -1627,7 +1720,8 @@ main(int argc, char **argv)
 		exit(1);
 	}
 	pg_log_info("validating connection string on publisher");
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo);
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  dbname_conninfo);
 	if (pub_base_conninfo == NULL)
 		exit(1);
 
@@ -1662,24 +1756,24 @@ main(int argc, char **argv)
 		else
 		{
 			pg_log_error("no database name specified");
-			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
 			exit(1);
 		}
 	}
 
-	/*
-	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
-	 */
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
 	pg_bin_dir = get_bin_directory(argv[0]);
 
 	/* rudimentary check for a data directory. */
 	if (!check_data_directory(opt.subscriber_dir))
 		exit(1);
 
-	/* Store database information for publisher and subscriber. */
-	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo, sub_base_conninfo);
+	/* Store database information for publisher and subscriber */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
 
-	/* Register a function to clean up objects in case of failure. */
+	/* Register a function to clean up objects in case of failure */
 	atexit(cleanup_objects_atexit);
 
 	/*
@@ -1691,9 +1785,7 @@ main(int argc, char **argv)
 	if (pub_sysid != sub_sysid)
 		pg_fatal("subscriber data directory is not a copy of the source database cluster");
 
-	/*
-	 * Create the output directory to store any data generated by this tool.
-	 */
+	/* Create the output directory to store any data generated by this tool */
 	server_start_log = setup_server_logfile(opt.subscriber_dir);
 
 	/* subscriber PID file. */
@@ -1707,9 +1799,7 @@ main(int argc, char **argv)
 	 */
 	if (stat(pidfile, &statbuf) == 0)
 	{
-		/*
-		 * Check if the standby server is ready for logical replication.
-		 */
+		/* Check if the standby server is ready for logical replication */
 		if (!check_subscriber(dbinfo))
 			exit(1);
 
@@ -1731,7 +1821,7 @@ main(int argc, char **argv)
 		if (!setup_publisher(dbinfo))
 			exit(1);
 
-		/* Stop the standby server. */
+		/* Stop the standby server */
 		pg_log_info("standby is up and running");
 		pg_log_info("stopping the server to start the transformation steps");
 		if (!dry_run)
@@ -1776,9 +1866,12 @@ main(int argc, char **argv)
 	 */
 	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
-	appendPQExpBuffer(recoveryconfcontents, "recovery_target_timeline = 'latest'\n");
-	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
-	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
@@ -1786,7 +1879,8 @@ main(int argc, char **argv)
 	if (dry_run)
 	{
 		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
-		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
 						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
 	}
 	else
@@ -1799,16 +1893,12 @@ main(int argc, char **argv)
 
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
 
-	/*
-	 * Start subscriber and wait until accepting connections.
-	 */
+	/* Start subscriber and wait until accepting connections */
 	pg_log_info("starting the subscriber");
 	if (!dry_run)
 		start_standby_server(pg_bin_dir, opt.subscriber_dir, server_start_log);
 
-	/*
-	 * Waiting the subscriber to be promoted.
-	 */
+	/* Waiting the subscriber to be promoted */
 	wait_for_end_recovery(dbinfo[0].subconninfo, pg_bin_dir, &opt);
 
 	/*
@@ -1836,22 +1926,19 @@ main(int argc, char **argv)
 		}
 		else
 		{
-			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning("could not drop replication slot \"%s\" on primary",
+						   primary_slot_name);
 			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
 		}
 		disconnect_database(conn);
 	}
 
-	/*
-	 * Stop the subscriber.
-	 */
+	/* Stop the subscriber */
 	pg_log_info("stopping the subscriber");
 	if (!dry_run)
 		stop_standby_server(pg_bin_dir, opt.subscriber_dir);
 
-	/*
-	 * Change system identifier from subscriber.
-	 */
+	/* Change system identifier from subscriber */
 	modify_subscriber_sysid(pg_bin_dir, &opt);
 
 	/*
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 0f02b1bfac..95eb4e70ac 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -18,23 +18,18 @@ my $datadir = PostgreSQL::Test::Utils::tempdir;
 command_fails(['pg_createsubscriber'],
 	'no subscriber data directory specified');
 command_fails(
-	[
-		'pg_createsubscriber',
-		'--pgdata', $datadir
-	],
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
 	'no publisher connection string specified');
 command_fails(
 	[
-		'pg_createsubscriber',
-		'--dry-run',
+		'pg_createsubscriber', '--dry-run',
 		'--pgdata', $datadir,
 		'--publisher-server', 'dbname=postgres'
 	],
 	'no subscriber connection string specified');
 command_fails(
 	[
-		'pg_createsubscriber',
-		'--verbose',
+		'pg_createsubscriber', '--verbose',
 		'--pgdata', $datadir,
 		'--publisher-server', 'dbname=postgres',
 		'--subscriber-server', 'dbname=postgres'
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 2db41cbc9b..58f9d95f3b 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -23,7 +23,7 @@ $node_p->start;
 # The extra option forces it to initialize a new cluster instead of copying a
 # previously initdb's cluster.
 $node_f = PostgreSQL::Test::Cluster->new('node_f');
-$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->init(allows_streaming => 'logical', extra => ['--no-instructions']);
 $node_f->start;
 
 # On node P
@@ -66,12 +66,13 @@ command_fails(
 # dry run mode on node S
 command_ok(
 	[
-		'pg_createsubscriber', '--verbose', '--dry-run',
-		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
-		'--subscriber-server', $node_s->connstr('pg1'),
-		'--database', 'pg1',
-		'--database', 'pg2'
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
 	],
 	'run pg_createsubscriber --dry-run on node S');
 
@@ -120,12 +121,13 @@ third row),
 
 # Check result on database pg2
 $result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
-is( $result, qq(row 1),
-	'logical replication works on database pg2');
+is($result, qq(row 1), 'logical replication works on database pg2');
 
 # Different system identifier?
-my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
-my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
 ok($sysid_p != $sysid_s, 'system identifier was changed');
 
 # clean up
-- 
2.43.0

v18-0003-Fix-argument-for-get_base_conninfo.patchapplication/octet-stream; name=v18-0003-Fix-argument-for-get_base_conninfo.patchDownload
From 5ca601ec8f4b25ba51daeb7044a8043cfcec34f7 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 8 Feb 2024 13:58:48 +0000
Subject: [PATCH v18 3/5] Fix argument for get_base_conninfo

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 7a5ef4f251..09e746b85b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -62,7 +62,7 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname);
+static char *get_base_conninfo(char *conninfo, char **dbname);
 static char *get_bin_directory(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
@@ -205,7 +205,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char *dbname)
+get_base_conninfo(char *conninfo, char **dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -227,7 +227,7 @@ get_base_conninfo(char *conninfo, char *dbname)
 		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
 		{
 			if (dbname)
-				dbname = pg_strdup(conn_opt->val);
+				*dbname = pg_strdup(conn_opt->val);
 			continue;
 		}
 
@@ -1721,7 +1721,7 @@ main(int argc, char **argv)
 	}
 	pg_log_info("validating connection string on publisher");
 	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
-										  dbname_conninfo);
+										  &dbname_conninfo);
 	if (pub_base_conninfo == NULL)
 		exit(1);
 
-- 
2.43.0

v18-0004-Add-testcase.patchapplication/octet-stream; name=v18-0004-Add-testcase.patchDownload
From 7cd6c219c70dca379ccd0ac391d0c5e4bc8fa139 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 8 Feb 2024 14:05:59 +0000
Subject: [PATCH v18 4/5] Add testcase

---
 .../t/041_pg_createsubscriber_standby.pl      | 53 ++++++++++++++++---
 1 file changed, 47 insertions(+), 6 deletions(-)

diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 58f9d95f3b..d7567ef8e9 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -13,6 +13,7 @@ my $node_p;
 my $node_f;
 my $node_s;
 my $result;
+my $slotname;
 
 # Set up node P as primary
 $node_p = PostgreSQL::Test::Cluster->new('node_p');
@@ -30,6 +31,7 @@ $node_f->start;
 # - create databases
 # - create test tables
 # - insert a row
+# - create a physical relication slot
 $node_p->safe_psql(
 	'postgres', q(
 	CREATE DATABASE pg1;
@@ -38,18 +40,19 @@ $node_p->safe_psql(
 $node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
 $node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
 $node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+$slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
 
 # Set up node S as standby linking to node P
 $node_p->backup('backup_1');
 $node_s = PostgreSQL::Test::Cluster->new('node_s');
 $node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
-$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->append_conf('postgresql.conf', qq[
+log_min_messages = debug2
+primary_slot_name = '$slotname'
+]);
 $node_s->set_standby_mode();
-$node_s->start;
-
-# Insert another row on node P and wait node S to catch up
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
-$node_p->wait_for_replay_catchup($node_s);
 
 # Run pg_createsubscriber on about-to-fail node F
 command_fails(
@@ -63,6 +66,25 @@ command_fails(
 	],
 	'subscriber data directory is not a copy of the source database cluster');
 
+# Run pg_createsubscriber on the stopped node
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'target server must be running');
+
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
 # dry run mode on node S
 command_ok(
 	[
@@ -80,6 +102,17 @@ command_ok(
 is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
 	't', 'standby is in recovery');
 
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1')
+	],
+	'run pg_createsubscriber without --databases');
+
 # Run pg_createsubscriber on node S
 command_ok(
 	[
@@ -92,6 +125,14 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');
 
+ok(-d $node_s->data_dir . "/pg_createsubscriber_output.d",
+	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success");
+
+# Confirm the physical slot has been removed
+$result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'");
+is ( $result, qq(0), 'the physical replication slot specifeid as primary_slot_name has been removed');
+
 # Insert rows on P
 $node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
 $node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
-- 
2.43.0

v18-0005-Remove-P-and-use-primary_conninfo-instead.patchapplication/octet-stream; name=v18-0005-Remove-P-and-use-primary_conninfo-instead.patchDownload
From fe7fb7a2144e3e70b318cdfefa4d181b40f107d6 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 09:31:44 +0000
Subject: [PATCH v18 5/5] Remove -P and use primary_conninfo instead

XXX: This may be a problematic when the OS user who started target instance is
not the current OS user and PGPASSWORD environment variable was used for
connecting to the primary server. In this case, the password would not be
written in the primary_conninfo and the PGPASSWORD variable might not be set.
This may lead an connection error. Is this a real issue? Note that using
PGPASSWORD is not recommended.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  17 +--
 src/bin/pg_basebackup/pg_createsubscriber.c   | 101 ++++++++++++------
 .../t/040_pg_createsubscriber.pl              |  11 +-
 .../t/041_pg_createsubscriber_standby.pl      |  10 +-
 4 files changed, 72 insertions(+), 67 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index f5238771b7..2ff31628ce 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -29,11 +29,6 @@ PostgreSQL documentation
      <arg choice="plain"><option>--pgdata</option></arg>
     </group>
     <replaceable>datadir</replaceable>
-    <group choice="req">
-     <arg choice="plain"><option>-P</option></arg>
-     <arg choice="plain"><option>--publisher-server</option></arg>
-    </group>
-    <replaceable>connstr</replaceable>
     <group choice="req">
      <arg choice="plain"><option>-S</option></arg>
      <arg choice="plain"><option>--subscriber-server</option></arg>
@@ -82,16 +77,6 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
-     <varlistentry>
-      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
-      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
-      <listitem>
-       <para>
-        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry>
       <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
       <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
@@ -303,7 +288,7 @@ PostgreSQL documentation
    To create a logical replica for databases <literal>hr</literal> and
    <literal>finance</literal> from a physical replica at <literal>foo</literal>:
 <screen>
-<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -S "host=localhost" -d hr -d finance</userinput>
 </screen>
   </para>
 
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 09e746b85b..9549b889a8 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -38,7 +38,6 @@
 typedef struct CreateSubscriberOptions
 {
 	char	   *subscriber_dir; 		/* standby/subscriber data directory */
-	char	   *pub_conninfo_str;		/* publisher connection string */
 	char	   *sub_conninfo_str;		/* subscriber connection string */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;					/* retain log file? */
@@ -66,6 +65,8 @@ static char *get_base_conninfo(char *conninfo, char **dbname);
 static char *get_bin_directory(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static char *get_primary_conninfo_from_target(const char *base_conninfo,
+											  const char *dbname);
 static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
 										  const char *pub_base_conninfo,
 										  const char *sub_base_conninfo);
@@ -178,7 +179,6 @@ usage(void)
 	printf(_("  %s [OPTION]...\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
-	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
@@ -415,6 +415,57 @@ disconnect_database(PGconn *conn)
 	PQfinish(conn);
 }
 
+/*
+ * Obtain primary_conninfo from the target server. The value would be used for
+ * connecting from the pg_createsubscriber itself and logical replication apply
+ * worker.
+ */
+static char *
+get_primary_conninfo_from_target(const char *base_conninfo, const char *dbname)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	char	   *conninfo;
+	char	   *primaryconninfo;
+
+	pg_log_info("getting primary_conninfo from standby");
+
+	/*
+	 * Construct a connection string to the target instance. Since dbinfo has
+	 * not stored infomation yet, the name must be passed as an argument.
+	 */
+	conninfo = concat_conninfo_dbname(base_conninfo, dbname);
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SHOW primary_conninfo;");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send command \"%s\": %s",
+					 "SHOW primary_conninfo;", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	primaryconninfo = pg_strdup(PQgetvalue(res, 0, 0));
+
+	if (strlen(primaryconninfo) == 0)
+	{
+		pg_log_error("primary_conninfo was empty");
+		pg_log_error_hint("Check whether the target server is really a standby.");
+		exit(1);
+	}
+
+	pg_free(conninfo);
+	PQclear(res);
+	disconnect_database(conn);
+
+	return primaryconninfo;
+}
+
 /*
  * Obtain the system identifier using the provided connection. It will be used
  * to compare if a data directory is a clone of another one.
@@ -1358,17 +1409,20 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char	   *conninfo;
 
 	Assert(conn != NULL);
 
 	pg_log_info("creating subscription \"%s\" on database \"%s\"",
 				dbinfo->subname, dbinfo->dbname);
 
+	conninfo = escape_single_quotes_ascii(dbinfo->pubconninfo);
+
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
 					  "WITH (create_slot = false, copy_data = false, " 
 					  "      enabled = false)",
-					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+					  dbinfo->subname, conninfo, dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1389,6 +1443,7 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 		PQclear(res);
 
+	pg_free(conninfo);
 	destroyPQExpBuffer(str);
 }
 
@@ -1559,7 +1614,6 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
-		{"publisher-server", required_argument, NULL, 'P'},
 		{"subscriber-server", required_argument, NULL, 'S'},
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
@@ -1615,7 +1669,6 @@ main(int argc, char **argv)
 
 	/* Default settings */
 	opt.subscriber_dir = NULL;
-	opt.pub_conninfo_str = NULL;
 	opt.sub_conninfo_str = NULL;
 	opt.database_names = (SimpleStringList)
 	{
@@ -1640,7 +1693,7 @@ main(int argc, char **argv)
 
 	get_restricted_token();
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+	while ((c = getopt_long(argc, argv, "D:S:d:nrt:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1648,9 +1701,6 @@ main(int argc, char **argv)
 			case 'D':
 				opt.subscriber_dir = pg_strdup(optarg);
 				break;
-			case 'P':
-				opt.pub_conninfo_str = pg_strdup(optarg);
-				break;
 			case 'S':
 				opt.sub_conninfo_str = pg_strdup(optarg);
 				break;
@@ -1703,28 +1753,6 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
-	/*
-	 * Parse connection string. Build a base connection string that might be
-	 * reused by multiple databases.
-	 */
-	if (opt.pub_conninfo_str == NULL)
-	{
-		/*
-		 * TODO use primary_conninfo (if available) from subscriber and
-		 * extract publisher connection string. Assume that there are
-		 * identical entries for physical and logical replication. If there is
-		 * not, we would fail anyway.
-		 */
-		pg_log_error("no publisher connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	pg_log_info("validating connection string on publisher");
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
-										  &dbname_conninfo);
-	if (pub_base_conninfo == NULL)
-		exit(1);
-
 	if (opt.sub_conninfo_str == NULL)
 	{
 		pg_log_error("no subscriber connection string specified");
@@ -1732,7 +1760,7 @@ main(int argc, char **argv)
 		exit(1);
 	}
 	pg_log_info("validating connection string on subscriber");
-	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, &dbname_conninfo);
 	if (sub_base_conninfo == NULL)
 		exit(1);
 
@@ -1742,7 +1770,7 @@ main(int argc, char **argv)
 
 		/*
 		 * If --database option is not provided, try to obtain the dbname from
-		 * the publisher conninfo. If dbname parameter is not available, error
+		 * the subscriber conninfo. If dbname parameter is not available, error
 		 * out.
 		 */
 		if (dbname_conninfo)
@@ -1750,7 +1778,7 @@ main(int argc, char **argv)
 			simple_string_list_append(&opt.database_names, dbname_conninfo);
 			num_dbs++;
 
-			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+			pg_log_info("database \"%s\" was extracted from the subscriber connection string",
 						dbname_conninfo);
 		}
 		else
@@ -1762,6 +1790,11 @@ main(int argc, char **argv)
 		}
 	}
 
+	/* Obtain a connection string from the target */
+	pub_base_conninfo =
+				get_primary_conninfo_from_target(sub_base_conninfo,
+												 opt.database_names.head->val);
+
 	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
 	pg_bin_dir = get_bin_directory(argv[0]);
 
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 95eb4e70ac..da8250d1b7 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -17,21 +17,12 @@ my $datadir = PostgreSQL::Test::Utils::tempdir;
 
 command_fails(['pg_createsubscriber'],
 	'no subscriber data directory specified');
-command_fails(
-	[ 'pg_createsubscriber', '--pgdata', $datadir ],
-	'no publisher connection string specified');
-command_fails(
-	[
-		'pg_createsubscriber', '--dry-run',
-		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres'
-	],
+command_fails([ 'pg_createsubscriber', '--dry-run', '--pgdata', $datadir, ],
 	'no subscriber connection string specified');
 command_fails(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres',
 		'--subscriber-server', 'dbname=postgres'
 	],
 	'no database name specified');
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index d7567ef8e9..6b68276ce3 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -59,12 +59,11 @@ command_fails(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_f->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
-	'subscriber data directory is not a copy of the source database cluster');
+	'target database is not a physical standby');
 
 # Run pg_createsubscriber on the stopped node
 command_fails(
@@ -90,8 +89,7 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
-		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->data_dir, '--subscriber-server',
 		$node_s->connstr('pg1'), '--database',
 		'pg1', '--database',
 		'pg2'
@@ -107,8 +105,7 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
-		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->data_dir, '--subscriber-server',
 		$node_s->connstr('pg1')
 	],
 	'run pg_createsubscriber without --databases');
@@ -118,7 +115,6 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
-- 
2.43.0

#124vignesh C
vignesh21@gmail.com
In reply to: Euler Taveira (#117)
Re: speed up a logical replica setup

On Wed, 7 Feb 2024 at 10:24, Euler Taveira <euler@eulerto.com> wrote:

On Fri, Feb 2, 2024, at 6:41 AM, Hayato Kuroda (Fujitsu) wrote:

Thanks for updating the patch!

Thanks for the updated patch, few comments:
Few comments:
1) Cleanup function handler flag should be reset, i.e.
dbinfo->made_replslot = false; should be there else there will be an
error during  drop replication slot cleanup in error flow:
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const
char *slot_name)
+{
+       PQExpBuffer str = createPQExpBuffer();
+       PGresult   *res;
+
+       Assert(conn != NULL);
+
+       pg_log_info("dropping the replication slot \"%s\" on database
\"%s\"", slot_name, dbinfo->dbname);
+
+       appendPQExpBuffer(str, "SELECT
pg_drop_replication_slot('%s')", slot_name);
+
+       pg_log_debug("command is: %s", str->data);
2) Cleanup function handler flag should be reset, i.e.
dbinfo->made_publication = false; should be there else there will be
an error during drop publication cleanup in error flow:
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+       PQExpBuffer str = createPQExpBuffer();
+       PGresult   *res;
+
+       Assert(conn != NULL);
+
+       pg_log_info("dropping publication \"%s\" on database \"%s\"",
dbinfo->pubname, dbinfo->dbname);
+
+       appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+       pg_log_debug("command is: %s", str->data);
3) Cleanup function handler flag should be reset, i.e.
dbinfo->made_subscription = false; should be there else there will be
an error during drop publication cleanup in error flow:
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+       PQExpBuffer str = createPQExpBuffer();
+       PGresult   *res;
+
+       Assert(conn != NULL);
+
+       pg_log_info("dropping subscription \"%s\" on database \"%s\"",
dbinfo->subname, dbinfo->dbname);
+
+       appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+       pg_log_debug("command is: %s", str->data);
4) I was not sure if drop_publication is required here, as we will not
create any publication in subscriber node:
+               if (dbinfo[i].made_subscription)
+               {
+                       conn = connect_database(dbinfo[i].subconninfo);
+                       if (conn != NULL)
+                       {
+                               drop_subscription(conn, &dbinfo[i]);
+                               if (dbinfo[i].made_publication)
+                                       drop_publication(conn, &dbinfo[i]);
+                               disconnect_database(conn);
+                       }
+               }
5) The connection should be disconnected in case of error case:
+       /* secure search_path */
+       res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               pg_log_error("could not clear search_path: %s",
PQresultErrorMessage(res));
+               return NULL;
+       }
+       PQclear(res);
6) There should be a line break before postgres_fe inclusion, to keep
it consistent:
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>

7) These includes are not required:
7.a) #include <signal.h>
7.b) #include <sys/stat.h>
7.c) #include <sys/wait.h>
7.d) #include "access/xlogdefs.h"
7.e) #include "catalog/pg_control.h"
7.f) #include "common/file_utils.h"
7.g) #include "utils/pidfile.h"

+ *             src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_authid_d.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
8) POSTMASTER_STANDBY and POSTMASTER_FAILED are not being used, is it
required or kept for future purpose:
+enum WaitPMResult
+{
+       POSTMASTER_READY,
+       POSTMASTER_STANDBY,
+       POSTMASTER_STILL_STARTING,
+       POSTMASTER_FAILED
+};
9) pg_createsubscriber should be kept after pg_basebackup to maintain
the consistent order:
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_createsubscriber
10) dry-run help message is not very clear, how about something
similar to pg_upgrade's message like "check clusters only, don't
change any data":
+       printf(_(" -d, --database=DBNAME               database to
create a subscription\n"));
+       printf(_(" -n, --dry-run                       stop before
modifying anything\n"));
+       printf(_(" -t, --recovery-timeout=SECS         seconds to wait
for recovery to end\n"));
+       printf(_(" -r, --retain                        retain log file
after success\n"));
+       printf(_(" -v, --verbose                       output verbose
messages\n"));
+       printf(_(" -V, --version                       output version
information, then exit\n"));

Regards,
Vignesh

#125Shubham Khanna
khannashubham1197@gmail.com
In reply to: Euler Taveira (#117)
Re: speed up a logical replica setup

On Wed, Feb 7, 2024 at 10:24 AM Euler Taveira <euler@eulerto.com> wrote:

On Fri, Feb 2, 2024, at 6:41 AM, Hayato Kuroda (Fujitsu) wrote:

Thanks for updating the patch!

Thanks for taking a look.

I'm still working on the data structures to group options. I don't like the way
it was grouped in v13-0005. There is too many levels to reach database name.
The setup_subscriber() function requires the 3 data structures.

Right, your refactoring looks fewer stack. So I pause to revise my refactoring
patch.

I didn't complete this task yet so I didn't include it in this patch.

The documentation update is almost there. I will include the modifications in
the next patch.

OK. I think it should be modified before native speakers will attend to the
thread.

Same for this one.

Regarding v13-0004, it seems a good UI that's why I wrote a comment about it.
However, it comes with a restriction that requires a similar HBA rule for both
regular and replication connections. Is it an acceptable restriction? We might
paint ourselves into the corner. A reasonable proposal is not to remove this
option. Instead, it should be optional. If it is not provided, primary_conninfo
is used.

I didn't have such a point of view. However, it is not related whether -P exists
or not. Even v14-0001 requires primary to accept both normal/replication connections.
If we want to avoid it, the connection from pg_createsubscriber can be restored
to replication-connection.
(I felt we do not have to use replication protocol even if we change the connection mode)

That's correct. We made a decision to use non physical replication connections
(besides the one used to connect primary <-> standby). Hence, my concern about
a HBA rule falls apart. I'm not convinced that using a modified
primary_conninfo is the only/right answer to fill the subscription connection
string. Physical vs logical replication has different requirements when we talk
about users. The physical replication requires only the REPLICATION privilege.
On the other hand, to create a subscription you must have the privileges of
pg_create_subscription role and also CREATE privilege on the specified
database. Unless, you are always recommending the superuser for this tool, I'm
afraid there will be cases that reusing primary_conninfo will be an issue. The
more I think about this UI, the more I think that, if it is not hundreds of
lines of code, it uses the primary_conninfo the -P is not specified.

The motivation why -P is not needed is to ensure the consistency of nodes.
pg_createsubscriber assumes that the -P option can connect to the upstream node,
but no one checks it. Parsing two connection strings may be a solution but be
confusing. E.g., what if some options are different?
I think using a same parameter is a simplest solution.

Ugh. An error will occur the first time (get_primary_sysid) it tries to connect
to primary.

I found that no one refers the name of temporary slot. Can we remove the variable?

It is gone. I did a refactor in the create_logical_replication_slot function.
Slot name is assigned internally (no need for slot_name or temp_replslot) and
temporary parameter is included.

Initialization by `CreateSubscriberOptions opt = {0};` seems enough.
All values are set to 0x0.

It is. However, I keep the assignments for 2 reasons: (a) there might be
parameters whose default value is not zero, (b) the standard does not say that
a null pointer must be represented by zero and (c) there is no harm in being
paranoid during initial assignment.

You said "target server must be a standby" in [1], but I cannot find checks for it.
IIUC, there are two approaches:
a) check the existence "standby.signal" in the data directory
b) call an SQL function "pg_is_in_recovery"

I applied v16-0004 that implements option (b).

I still think they can be combined as "bindir".

I applied a patch that has a single variable for BINDIR.

WriteRecoveryConfig() writes GUC parameters to postgresql.auto.conf, but not
sure it is good. These settings would remain on new subscriber even after the
pg_createsubscriber. Can we avoid it? I come up with passing these parameters
via pg_ctl -o option, but it requires parsing output from GenerateRecoveryConfig()
(all GUCs must be allign like "-c XXX -c XXX -c XXX...").

I applied a modified version of v16-0006.

Functions arguments should not be struct because they are passing by value.
They should be a pointer. Or, for modify_subscriber_sysid and wait_for_end_recovery,
we can pass a value which would be really used.

Done.

07.
```
static char *get_base_conninfo(char *conninfo, char *dbname,
const char *noderole);
```

Not sure noderole should be passed here. It is used only for the logging.
Can we output string before calling the function?
(The parameter is not needed anymore if -P is removed)

Done.

08.
The terminology is still not consistent. Some functions call the target as standby,
but others call it as subscriber.

The terminology should reflect the actual server role. I'm calling it "standby"
if it is a physical replica and "subscriber" if it is a logical replica. Maybe
"standby" isn't clear enough.

09.
v14 does not work if the standby server has already been set recovery_target*
options. PSA the reproducer. I considered two approaches:

a) raise an ERROR when these parameter were set. check_subscriber() can do it
b) overwrite these GUCs as empty strings.

I prefer (b) that's exactly what you provided in v16-0006.

10.
The execution always fails if users execute --dry-run just before. Because
pg_createsubscriber stops the standby anyway. Doing dry run first is quite normal
use-case, so current implementation seems not user-friendly. How should we fix?
Below bullets are my idea:

a) avoid stopping the standby in case of dry_run: seems possible.
b) accept even if the standby is stopped: seems possible.
c) start the standby at the end of run: how arguments like pg_ctl -l should be specified?

I prefer (a). I applied a slightly modified version of v16-0005.

This new patch contains the following changes:

* check whether the target is really a standby server (0004)
* refactor: pg_create_logical_replication_slot function
* use a single variable for pg_ctl and pg_resetwal directory
* avoid recovery errors applying default settings for some GUCs (0006)
* don't stop/start the standby in dry run mode (0005)
* miscellaneous fixes

I don't understand why v16-0002 is required. In a previous version, this patch
was using connections in logical replication mode. After some discussion we
decided to change it to regular connections and use SQL functions (instead of
replication commands). Is it a requirement for v16-0003?

I started reviewing v16-0007 but didn't finish yet. The general idea is ok.
However, I'm still worried about preventing some use cases if it provides only
the local connection option. What if you want to keep monitoring this instance
while the transformation is happening? Let's say it has a backlog that will
take some time to apply. Unless, you have a local agent, you have no data about
this server until pg_createsubscriber terminates. Even the local agent might
not connect to the server unless you use the current port.

I tried verifying few scenarios by using 5 databases and came across
the following errors:

./pg_createsubscriber -D ../new_standby -P "host=localhost port=5432
dbname=postgres" -S "host=localhost port=9000 dbname=postgres" -d db1
-d db2 -d db3 -d db4 -d db5

pg_createsubscriber: error: publisher requires 6 wal sender
processes, but only 5 remain
pg_createsubscriber: hint: Consider increasing max_wal_senders to at least 7.

It is successful only with 7 wal senders, so we can change error
messages accordingly.

pg_createsubscriber: error: publisher requires 6 replication slots,
but only 5 remain
pg_createsubscriber: hint: Consider increasing max_replication_slots
to at least 7.

It is successful only with 7 replication slots, so we can change error
messages accordingly.

Thanks and Regards,
Shubham Khanna,

#126Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#117)
5 attachment(s)
RE: speed up a logical replica setup

Dear Euler,

Further comments for v17.

01.
This program assumes that the target server has same major version with this.
Because the target server would be restarted by same version's pg_ctl command.
I felt it should be ensured by reading the PG_VERSION.

02.
pg_upgrade checked the version of using executables, like pg_ctl, postgres, and
pg_resetwal. I felt it should be as well.

03. get_bin_directory
```
if (find_my_exec(path, full_path) < 0)
{
pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
"same directory as \"%s\".\n",
"pg_ctl", progname, full_path);
```

s/"pg_ctl"/progname

04.
Missing canonicalize_path()?

05.
Assuming that the target server is a cascade standby, i.e., it has a role as
another primary. In this case, I thought the child node would not work. Because
pg_createsubcriber runs pg_resetwal and all WAL files would be discarded at that
time. I have not tested, but should the program detect it and exit earlier?

06.
wait_for_end_recovery() waits forever even if the standby has been disconnected
from the primary, right? should we check the status of the replication via
pg_stat_wal_receiver?

07.
The cleanup function has couple of bugs.

* If subscriptions have been created on the database, the function also tries to
drop a publication. But it leads an ERROR because it has been already dropped.
See setup_subscriber().
* If the subscription has been created, drop_replication_slot() leads an ERROR.
Because the subscriber tried to drop the subscription while executing DROP SUBSCRIPTION.

08.
I found that all messages (ERROR, WARNING, INFO, etc...) would output to stderr,
but I felt it should be on stdout. Is there a reason? pg_dump outputs messages to
stderr, but the motivation might be to avoid confusion with dumps.

09.
I'm not sure the cleanup for subscriber is really needed. Assuming that there
are two databases, e.g., pg1 pg2 , and we fail to create a subscription on pg2.
This can happen when the subscription which has the same name has been already
created on the primary server.
In this case a subscirption pn pg1 would be removed. But what is a next step?
Since a timelineID on the standby server is larger than the primary (note that
the standby has been promoted once), we cannot resume the physical replication
as-is. IIUC the easiest method to retry is removing a cluster once and restarting
from pg_basebackup. If so, no need to cleanup the standby because it is corrupted.
We just say "Please remove the cluster and recreate again".

Here is a reproducer.

1. apply the txt patch atop 0001 patch.
2. run test_corruption.sh.
3. when you find a below output [1]``` pg_createsubscriber: creating the replication slot "pg_createsubscriber_16389_3884" on database "testdb" pg_createsubscriber: XXX: sleep 20s ```, connect to a testdb from another terminal and
run CREATE SUBSCRITPION for the same subscription on the primary
4. Finally, pg_createsubscriber would fail the creation.

I also attached server logs of both nodes and the output.
Note again that this is a real issue. I used a tricky way for surely overlapping name,
but this can happen randomly.

10.
While investigating #09, I found that we cannot report properly a reason why the
subscription cannot be created. The output said:

```
pg_createsubscriber: error: could not create subscription "pg_createsubscriber_16389_3884" on database "testdb": out of memory
```

But the standby serverlog said:

```
ERROR: subscription "pg_createsubscriber_16389_3884" already exists
STATEMENT: CREATE SUBSCRIPTION pg_createsubscriber_16389_3884 CONNECTION 'user=postgres port=5431 dbname=testdb' PUBLICATION pg_createsubscriber_16389 WITH (create_slot = false, copy_data = false, enabled = false)
```

[1]: ``` pg_createsubscriber: creating the replication slot "pg_createsubscriber_16389_3884" on database "testdb" pg_createsubscriber: XXX: sleep 20s ```
```
pg_createsubscriber: creating the replication slot "pg_createsubscriber_16389_3884" on database "testdb"
pg_createsubscriber: XXX: sleep 20s
```

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

Attachments:

N1.logapplication/octet-stream; name=N1.logDownload
result.datapplication/octet-stream; name=result.datDownload
server_start_20240209T115112.963.logapplication/octet-stream; name=server_start_20240209T115112.963.logDownload
test_corruption.shapplication/octet-stream; name=test_corruption.shDownload
add_sleep.txttext/plain; name=add_sleep.txtDownload
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 9628f32a3e..fdb3e92aa3 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -919,6 +919,9 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 
 	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
 
+	pg_log_info("XXX: sleep 20s");
+	sleep(20);
+
 	appendPQExpBuffer(str, "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
 					  slot_name, "pgoutput", temporary ? "true" : "false");
 
#127Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#123)
9 attachment(s)
RE: speed up a logical replica setup

Dear hackers,

Since the original author seems bit busy, I updated the patch set.

01.
```
/* Options */
static const char *progname;

static char *primary_slot_name = NULL;
static bool dry_run = false;

static bool success = false;

static LogicalRepInfo *dbinfo;
static int num_dbs = 0;
```

The comment seems out-of-date. There is only one option.

Changed the comment to /* Global variables */.

02. check_subscriber and check_publisher

Missing pg_catalog prefix in some lines.

This has been already addressed in v18.

03. get_base_conninfo

I think dbname would not be set. IIUC, dbname should be a pointer of the pointer.

This has been already addressed in v18.

04.

I check the coverage and found two functions have been never called:
- drop_subscription
- drop_replication_slot

Also, some cases were not tested. Below bullet showed notable ones for me.
(Some of them would not be needed based on discussions)

* -r is specified
* -t is specified
* -P option contains dbname
* -d is not specified
* GUC settings are wrong
* primary_slot_name is specified on the standby
* standby server is not working

In feature level, we may able to check the server log is surely removed in case
of success.

So, which tests should be added? drop_subscription() is called only when the
cleanup phase, so it may be difficult to test. According to others, it seems that
-r and -t are not tested. GUC-settings have many test cases so not sure they
should be. Based on this, others can be tested.

This has been already addressed in v18.

PSA my top-up patch set.

V19-0001: same as Euler's patch, v17-0001.
V19-0002: Update docs per recent changes. Also, some adjustments were done.
V19-0003: Modify the alignment of codes. Mostly same as v18-0002.
V19-0004: Change an argument of get_base_conninfo. Same as v18-0003.
=== experimental patches ===
V19-0005: Add testcases. Same as v18-0004.
V19-0006: Update a comment above global variables.
V19-0007: Address comments from Vignesh.
V19-0008: Fix error message in get_bin_directory().
V19-0009: Remove -P option. Same as v18-0005.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

v19-0008-Fix-error-message-for-get_bin_directory.patchapplication/octet-stream; name=v19-0008-Fix-error-message-for-get_bin_directory.patchDownload
From b30d78f010b12ddad317ca67b698421101c231a4 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Tue, 13 Feb 2024 12:08:40 +0000
Subject: [PATCH v19 8/9] Fix error message for get_bin_directory

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index a20cec8312..28ea5835e9 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -252,9 +252,7 @@ get_bin_directory(const char *path)
 
 	if (find_my_exec(path, full_path) < 0)
 	{
-		pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
-					 "same directory as \"%s\".\n",
-					 "pg_ctl", progname, full_path);
+		pg_log_error("invalid binary directory");
 		pg_log_error_hint("Check your installation.");
 		exit(1);
 	}
-- 
2.43.0

v19-0009-Remove-P-and-use-primary_conninfo-instead.patchapplication/octet-stream; name=v19-0009-Remove-P-and-use-primary_conninfo-instead.patchDownload
From 0c1bb96f5794518cc2c73b35e7238d2940925ee6 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 2 Feb 2024 09:31:44 +0000
Subject: [PATCH v19 9/9] Remove -P and use primary_conninfo instead

XXX: This may be a problematic when the OS user who started target instance is
not the current OS user and PGPASSWORD environment variable was used for
connecting to the primary server. In this case, the password would not be
written in the primary_conninfo and the PGPASSWORD variable might not be set.
This may lead an connection error. Is this a real issue? Note that using
PGPASSWORD is not recommended.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  17 +--
 src/bin/pg_basebackup/pg_createsubscriber.c   | 101 ++++++++++++------
 .../t/040_pg_createsubscriber.pl              |  11 +-
 .../t/041_pg_createsubscriber_standby.pl      |  10 +-
 4 files changed, 72 insertions(+), 67 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 275a3365da..e10270fc24 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -29,11 +29,6 @@ PostgreSQL documentation
      <arg choice="plain"><option>--pgdata</option></arg>
     </group>
     <replaceable>datadir</replaceable>
-    <group choice="req">
-     <arg choice="plain"><option>-P</option></arg>
-     <arg choice="plain"><option>--publisher-server</option></arg>
-    </group>
-    <replaceable>connstr</replaceable>
     <group choice="req">
      <arg choice="plain"><option>-S</option></arg>
      <arg choice="plain"><option>--subscriber-server</option></arg>
@@ -160,16 +155,6 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
-     <varlistentry>
-      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
-      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
-      <listitem>
-       <para>
-        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry>
       <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
       <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
@@ -380,7 +365,7 @@ PostgreSQL documentation
    create subscriptions for databases <literal>hr</literal> and
    <literal>finance</literal> from a physical standby:
 <screen>
-<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -S "host=localhost" -d hr -d finance</userinput>
 </screen>
   </para>
 
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 28ea5835e9..cd72e77fef 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -32,7 +32,6 @@
 typedef struct CreateSubscriberOptions
 {
 	char	   *subscriber_dir; 		/* standby/subscriber data directory */
-	char	   *pub_conninfo_str;		/* publisher connection string */
 	char	   *sub_conninfo_str;		/* subscriber connection string */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;					/* retain log file? */
@@ -60,6 +59,8 @@ static char *get_base_conninfo(char *conninfo, char **dbname);
 static char *get_bin_directory(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static char *get_primary_conninfo_from_target(const char *base_conninfo,
+											  const char *dbname);
 static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
 										  const char *pub_base_conninfo,
 										  const char *sub_base_conninfo);
@@ -168,7 +169,6 @@ usage(void)
 	printf(_("  %s [OPTION]...\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
-	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -n, --dry-run                       check clusters only, don't change target server\n"));
@@ -404,6 +404,57 @@ disconnect_database(PGconn *conn)
 	PQfinish(conn);
 }
 
+/*
+ * Obtain primary_conninfo from the target server. The value would be used for
+ * connecting from the pg_createsubscriber itself and logical replication apply
+ * worker.
+ */
+static char *
+get_primary_conninfo_from_target(const char *base_conninfo, const char *dbname)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	char	   *conninfo;
+	char	   *primaryconninfo;
+
+	pg_log_info("getting primary_conninfo from standby");
+
+	/*
+	 * Construct a connection string to the target instance. Since dbinfo has
+	 * not stored infomation yet, the name must be passed as an argument.
+	 */
+	conninfo = concat_conninfo_dbname(base_conninfo, dbname);
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SHOW primary_conninfo;");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not send command \"%s\": %s",
+					 "SHOW primary_conninfo;", PQresultErrorMessage(res));
+		PQclear(res);
+		disconnect_database(conn);
+		exit(1);
+	}
+
+	primaryconninfo = pg_strdup(PQgetvalue(res, 0, 0));
+
+	if (strlen(primaryconninfo) == 0)
+	{
+		pg_log_error("primary_conninfo was empty");
+		pg_log_error_hint("Check whether the target server is really a standby.");
+		exit(1);
+	}
+
+	pg_free(conninfo);
+	PQclear(res);
+	disconnect_database(conn);
+
+	return primaryconninfo;
+}
+
 /*
  * Obtain the system identifier using the provided connection. It will be used
  * to compare if a data directory is a clone of another one.
@@ -1358,17 +1409,20 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
+	char	   *conninfo;
 
 	Assert(conn != NULL);
 
 	pg_log_info("creating subscription \"%s\" on database \"%s\"",
 				dbinfo->subname, dbinfo->dbname);
 
+	conninfo = escape_single_quotes_ascii(dbinfo->pubconninfo);
+
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
 					  "WITH (create_slot = false, copy_data = false, "
 					  "      enabled = false)",
-					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+					  dbinfo->subname, conninfo, dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1389,6 +1443,7 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 		PQclear(res);
 
+	pg_free(conninfo);
 	destroyPQExpBuffer(str);
 }
 
@@ -1562,7 +1617,6 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
-		{"publisher-server", required_argument, NULL, 'P'},
 		{"subscriber-server", required_argument, NULL, 'S'},
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
@@ -1618,7 +1672,6 @@ main(int argc, char **argv)
 
 	/* Default settings */
 	opt.subscriber_dir = NULL;
-	opt.pub_conninfo_str = NULL;
 	opt.sub_conninfo_str = NULL;
 	opt.database_names = (SimpleStringList)
 	{
@@ -1643,7 +1696,7 @@ main(int argc, char **argv)
 
 	get_restricted_token();
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+	while ((c = getopt_long(argc, argv, "D:S:d:nrt:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1651,9 +1704,6 @@ main(int argc, char **argv)
 			case 'D':
 				opt.subscriber_dir = pg_strdup(optarg);
 				break;
-			case 'P':
-				opt.pub_conninfo_str = pg_strdup(optarg);
-				break;
 			case 'S':
 				opt.sub_conninfo_str = pg_strdup(optarg);
 				break;
@@ -1706,28 +1756,6 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
-	/*
-	 * Parse connection string. Build a base connection string that might be
-	 * reused by multiple databases.
-	 */
-	if (opt.pub_conninfo_str == NULL)
-	{
-		/*
-		 * TODO use primary_conninfo (if available) from subscriber and
-		 * extract publisher connection string. Assume that there are
-		 * identical entries for physical and logical replication. If there is
-		 * not, we would fail anyway.
-		 */
-		pg_log_error("no publisher connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	pg_log_info("validating connection string on publisher");
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
-										  &dbname_conninfo);
-	if (pub_base_conninfo == NULL)
-		exit(1);
-
 	if (opt.sub_conninfo_str == NULL)
 	{
 		pg_log_error("no subscriber connection string specified");
@@ -1735,7 +1763,7 @@ main(int argc, char **argv)
 		exit(1);
 	}
 	pg_log_info("validating connection string on subscriber");
-	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, &dbname_conninfo);
 	if (sub_base_conninfo == NULL)
 		exit(1);
 
@@ -1745,7 +1773,7 @@ main(int argc, char **argv)
 
 		/*
 		 * If --database option is not provided, try to obtain the dbname from
-		 * the publisher conninfo. If dbname parameter is not available, error
+		 * the subscriber conninfo. If dbname parameter is not available, error
 		 * out.
 		 */
 		if (dbname_conninfo)
@@ -1753,7 +1781,7 @@ main(int argc, char **argv)
 			simple_string_list_append(&opt.database_names, dbname_conninfo);
 			num_dbs++;
 
-			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+			pg_log_info("database \"%s\" was extracted from the subscriber connection string",
 						dbname_conninfo);
 		}
 		else
@@ -1765,6 +1793,11 @@ main(int argc, char **argv)
 		}
 	}
 
+	/* Obtain a connection string from the target */
+	pub_base_conninfo =
+				get_primary_conninfo_from_target(sub_base_conninfo,
+												 opt.database_names.head->val);
+
 	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
 	pg_bin_dir = get_bin_directory(argv[0]);
 
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 95eb4e70ac..da8250d1b7 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -17,21 +17,12 @@ my $datadir = PostgreSQL::Test::Utils::tempdir;
 
 command_fails(['pg_createsubscriber'],
 	'no subscriber data directory specified');
-command_fails(
-	[ 'pg_createsubscriber', '--pgdata', $datadir ],
-	'no publisher connection string specified');
-command_fails(
-	[
-		'pg_createsubscriber', '--dry-run',
-		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres'
-	],
+command_fails([ 'pg_createsubscriber', '--dry-run', '--pgdata', $datadir, ],
 	'no subscriber connection string specified');
 command_fails(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $datadir,
-		'--publisher-server', 'dbname=postgres',
 		'--subscriber-server', 'dbname=postgres'
 	],
 	'no database name specified');
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index d7567ef8e9..6b68276ce3 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -59,12 +59,11 @@ command_fails(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_f->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
-	'subscriber data directory is not a copy of the source database cluster');
+	'target database is not a physical standby');
 
 # Run pg_createsubscriber on the stopped node
 command_fails(
@@ -90,8 +89,7 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
-		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->data_dir, '--subscriber-server',
 		$node_s->connstr('pg1'), '--database',
 		'pg1', '--database',
 		'pg2'
@@ -107,8 +105,7 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
-		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->data_dir, '--subscriber-server',
 		$node_s->connstr('pg1')
 	],
 	'run pg_createsubscriber without --databases');
@@ -118,7 +115,6 @@ command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
 		'--subscriber-server', $node_s->connstr('pg1'),
 		'--database', 'pg1',
 		'--database', 'pg2'
-- 
2.43.0

v19-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchapplication/octet-stream; name=v19-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchDownload
From 85a61e379a52d4691aaed7ad93e543ec26c95a36 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v19 1/9] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica
into a logical replica. It runs on the target server and should be able
to connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.  Stop the
target server. Change the system identifier from the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  320 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1869 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   44 +
 .../t/041_pg_createsubscriber_standby.pl      |  135 ++
 src/tools/pgindent/typedefs.list              |    2 +
 10 files changed, 2399 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f5238771b7
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,320 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..9628f32a3e
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1869 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_authid_d.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+/* Command-line options */
+typedef struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *sub_conninfo_str;	/* subscriber connection string */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+} CreateSubscriberOptions;
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname);
+static char *get_bin_directory(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile);
+static void stop_standby_server(const char *pg_bin_dir, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the directory that the pg_createsubscriber is in. Since it uses other
+ * PostgreSQL binaries (pg_ctl and pg_resetwal), the directory is used to build
+ * the full path for it.
+ */
+static char *
+get_bin_directory(const char *path)
+{
+	char		full_path[MAXPGPATH];
+	char	   *dirname;
+	char	   *sep;
+
+	if (find_my_exec(path, full_path) < 0)
+	{
+		pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+					 "same directory as \"%s\".\n",
+					 "pg_ctl", progname, full_path);
+		pg_log_error_hint("Check your installation.");
+		exit(1);
+	}
+
+	/*
+	 * Strip the file name from the path. It will be used to build the full
+	 * path for binaries used by this tool.
+	 */
+	dirname = pg_malloc(MAXPGPATH);
+	sep = strrchr(full_path, 'p');
+	Assert(sep != NULL);
+	strlcpy(dirname, full_path, sep - full_path);
+
+	pg_log_debug("pg_ctl path is:  %s/%s", dirname, "pg_ctl");
+	pg_log_debug("pg_resetwal path is:  %s/%s", dirname, "pg_resetwal");
+
+	return dirname;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: %s", PQresultErrorMessage(res));
+	}
+	if (PQntuples(res) != 1)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: got %d rows, expected %d row",
+				 PQntuples(res), 1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\" > \"%s\"", pg_bin_dir, opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical max_replication_slots >= current + number of dbs to
+	 * be converted max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/* The target server must be a standby */
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("The target server is not a standby");
+		return false;
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str, "SELECT pg_has_role(current_user, %u, 'MEMBER'), has_database_privilege(current_user, '%s', 'CREATE'), has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')", ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"", "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		return false;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical replication and
+ * enable the subscriptions. That's the last step for logical repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/*
+	 * This temporary replication slot is only used for catchup purposes.
+	 */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+	{
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it is
+ * run.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"", pg_bin_dir, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_bin_dir, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", pg_bin_dir, datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_fatal("could not obtain recovery progress");
+
+		if (PQntuples(res) != 1)
+			pg_fatal("unexpected result from pg_is_in_recovery function");
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_bin_dir, opt->subscriber_dir);
+			pg_fatal("recovery timed out");
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain publication information: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
+				 PQntuples(res), 1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not enable subscription \"%s\": %s", dbinfo->subname,
+					 PQerrorMessage(conn));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_bin_dir = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.sub_conninfo_str = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				opt.sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	pg_bin_dir = get_bin_directory(argv[0]);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(opt.subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure. */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server. */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		if (!dry_run)
+			stop_standby_server(pg_bin_dir, opt.subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0], true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	if (!dry_run)
+		start_standby_server(pg_bin_dir, opt.subscriber_dir, server_start_log);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_bin_dir, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+	if (!dry_run)
+		stop_standby_server(pg_bin_dir, opt.subscriber_dir);
+
+	/*
+	 * Change system identifier from subscriber.
+	 */
+	modify_subscriber_sysid(pg_bin_dir, &opt);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..0f02b1bfac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..2db41cbc9b
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,135 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91433d439b..102971164f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.43.0

v19-0002-Update-documentation.patchapplication/octet-stream; name=v19-0002-Update-documentation.patchDownload
From d13056baea458751b5b801d6cf278d1291679b78 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Tue, 13 Feb 2024 10:59:47 +0000
Subject: [PATCH v19 2/9] Update documentation

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 203 +++++++++++++++-------
 1 file changed, 140 insertions(+), 63 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index f5238771b7..275a3365da 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -48,19 +48,97 @@ PostgreSQL documentation
   </cmdsynopsis>
  </refsynopsisdiv>
 
- <refsect1>
+ <refsect1 id="r1-app-pg_createsubscriber-1">
   <title>Description</title>
   <para>
-    <application>pg_createsubscriber</application> creates a new logical
-    replica from a physical standby server.
+   The <application>pg_createsubscriber</application> creates a new <link
+   linkend="logical-replication-subscription">subscriber</link> from a physical
+   standby server.
   </para>
 
   <para>
-   The <application>pg_createsubscriber</application> should be run at the target
-   server. The source server (known as publisher server) should accept logical
-   replication connections from the target server (known as subscriber server).
-   The target server should accept local logical replication connection.
+   The <application>pg_createsubscriber</application> must be run at the target
+   server. The source server (known as publisher server) must accept both
+   normal and logical replication connections from the target server (known as
+   subscriber server). The target server must accept normal local connections.
   </para>
+
+  <para>
+   There are some prerequisites for both the source and target instance. If
+   these are not met an error will be reported.
+  </para>
+
+  <itemizedlist>
+   <listitem>
+    <para>
+     The given target data directory must have the same system identifier than the
+     source data directory.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must be used as a physical standby.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The given database user for the target instance must have privileges for
+     creating subscriptions and using functions for replication origin.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     and <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases.
+    </para>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+     configured to a value greater than the number of target databases.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The source instance must have
+     <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+     <literal>logical</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases and replication slots.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases and walsenders.
+    </para>
+   </listitem>
+  </itemizedlist>
+
+  <note>
+   <para>
+    After the successful conversion, a physical replication slot configured as
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>
+    would be removed from a primary instance.
+   </para>
+
+   <para>
+    The <application>pg_createsubscriber</application> focuses on large-scale
+    systems that contain more data than 1GB.  For smaller systems, initial data
+    synchronization of <link linkend="logical-replication">logical
+    replication</link> is recommended.
+   </para>
+  </note>
  </refsect1>
 
  <refsect1>
@@ -191,7 +269,7 @@ PostgreSQL documentation
  </refsect1>
 
  <refsect1>
-  <title>Notes</title>
+  <title>How It Works</title>
 
   <para>
    The transformation proceeds in the following steps:
@@ -200,97 +278,89 @@ PostgreSQL documentation
   <procedure>
    <step>
     <para>
-     <application>pg_createsubscriber</application> checks if the given target data
-     directory has the same system identifier than the source data directory.
-     Since it uses the recovery process as one of the steps, it starts the
-     target server as a replica from the source server. If the system
-     identifier is not the same, <application>pg_createsubscriber</application> will
-     terminate with an error.
+     Checks the target can be converted.  In particular, things listed in
+     <link linkend="r1-app-pg_createsubscriber-1">above section</link> would be
+     checked.  If these are not met <application>pg_createsubscriber</application>
+     will terminate with an error.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> checks if the target data
-     directory is used by a physical replica. Stop the physical replica if it is
-     running. One of the next steps is to add some recovery parameters that
-     requires a server start. This step avoids an error.
+     Creates a publication and a logical replication slot for each specified
+     database on the source instance.  These publications and logical replication
+     slots have generated names:
+     <quote><literal>pg_createsubscriber_%u</literal></quote> (parameters:
+     Database <parameter>oid</parameter>) for publications,
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     Database <parameter>oid</parameter>, Pid <parameter>int</parameter>) for
+     replication slots.
     </para>
    </step>
-
    <step>
     <para>
-     <application>pg_createsubscriber</application> creates one replication slot for
-     each specified database on the source server. The replication slot name
-     contains a <literal>pg_createsubscriber</literal> prefix. These replication
-     slots will be used by the subscriptions in a future step.  A temporary
-     replication slot is used to get a consistent start location. This
-     consistent LSN will be used as a stopping point in the <xref
-     linkend="guc-recovery-target-lsn"/> parameter and by the
-     subscriptions as a replication starting point. It guarantees that no
-     transaction will be lost.
+     Stops the target instance.  This is needed to add some recovery parameters
+     during the conversion.
     </para>
    </step>
-
    <step>
     <para>
-     <application>pg_createsubscriber</application> writes recovery parameters into
-     the target data directory and start the target server. It specifies a LSN
-     (consistent LSN that was obtained in the previous step) of write-ahead
-     log location up to which recovery will proceed. It also specifies
-     <literal>promote</literal> as the action that the server should take once
-     the recovery target is reached. This step finishes once the server ends
-     standby mode and is accepting read-write operations.
+     Creates a temporary replication slot to get a consistent start location.
+     The slot has generated names:
+     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
+     (parameters: Pid <parameter>int</parameter>).  Got consistent LSN will be
+     used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
+     parameter and by the subscriptions as a replication starting point. It
+     guarantees that no transaction will be lost.
+    </para>
+   </step>
+   <step>
+    <para>
+     Writes recovery parameters into the target data directory and starts the
+     target instance.  It specifies a LSN (consistent LSN that was obtained in
+     the previous step) of write-ahead log location up to which recovery will
+     proceed. It also specifies <literal>promote</literal> as the action that
+     the server should take once the recovery target is reached. This step
+     finishes once the server ends standby mode and is accepting read-write
+     operations.
     </para>
    </step>
 
    <step>
     <para>
-     Next, <application>pg_createsubscriber</application> creates one publication
-     for each specified database on the source server. Each publication
-     replicates changes for all tables in the database. The publication name
-     contains a <literal>pg_createsubscriber</literal> prefix. These publication
-     will be used by a corresponding subscription in a next step.
+     Creates a subscription for each specified database on the target instance.
+     These subscriptions have generated name:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     Database <parameter>oid</parameter>, Pid <parameter>int</parameter>).
+     These subscription have same subscription options:
+     <quote><literal>create_slot = false, copy_data = false, enabled = false</literal></quote>.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> creates one subscription for
-     each specified database on the target server. Each subscription name
-     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
-     name is identical to the subscription name. It does not copy existing data
-     from the source server. It does not create a replication slot. Instead, it
-     uses the replication slot that was created in a previous step. The
-     subscription is created but it is not enabled yet. The reason is the
-     replication progress must be set to the consistent LSN but replication
-     origin name contains the subscription oid in its name. Hence, the
-     subscription will be enabled in a separate step.
+     Sets replication progress to the consistent LSN that was obtained in a
+     previous step.  This is the exact LSN to be used as a initial location for
+     each subscription.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> sets the replication progress to
-     the consistent LSN that was obtained in a previous step. When the target
-     server started the recovery process, it caught up to the consistent LSN.
-     This is the exact LSN to be used as a initial location for each
-     subscription.
+     Enables the subscription for each specified database on the target server.
+     The subscription starts streaming from the consistent LSN.
     </para>
    </step>
 
    <step>
     <para>
-     Finally, <application>pg_createsubscriber</application> enables the subscription
-     for each specified database on the target server. The subscription starts
-     streaming from the consistent LSN.
+     Stops the standby server.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> stops the target server to change
-     its system identifier.
+     Updates a system identifier on the target server.
     </para>
    </step>
   </procedure>
@@ -300,8 +370,15 @@ PostgreSQL documentation
   <title>Examples</title>
 
   <para>
-   To create a logical replica for databases <literal>hr</literal> and
-   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+   Here is an example of using <application>pg_createsubscriber</application>.
+   Before running the command, please make sure target server is stopped.
+<screen>
+<prompt>$</prompt> <userinput>pg_ctl -D /usr/local/pgsql/data stop</userinput>
+</screen>
+
+   Then run <application>pg_createsubscriber</application>. Below tries to
+   create subscriptions for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical standby:
 <screen>
 <prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
 </screen>
-- 
2.43.0

v19-0003-Follow-coding-conversions.patchapplication/octet-stream; name=v19-0003-Follow-coding-conversions.patchDownload
From 038e404ca75c1506a3014a465bfbcfe72ff2ab5c Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 8 Feb 2024 12:34:52 +0000
Subject: [PATCH v19 3/9] Follow coding conversions

---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 393 +++++++++++-------
 .../t/040_pg_createsubscriber.pl              |  11 +-
 .../t/041_pg_createsubscriber_standby.pl      |  24 +-
 3 files changed, 256 insertions(+), 172 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 9628f32a3e..0ef670ae6d 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -37,12 +37,12 @@
 /* Command-line options */
 typedef struct CreateSubscriberOptions
 {
-	char	   *subscriber_dir; /* standby/subscriber data directory */
-	char	   *pub_conninfo_str;	/* publisher connection string */
-	char	   *sub_conninfo_str;	/* subscriber connection string */
+	char	   *subscriber_dir; 		/* standby/subscriber data directory */
+	char	   *pub_conninfo_str;		/* publisher connection string */
+	char	   *sub_conninfo_str;		/* subscriber connection string */
 	SimpleStringList database_names;	/* list of database names */
-	bool		retain;			/* retain log file? */
-	int			recovery_timeout;	/* stop recovery after this time */
+	bool		retain;					/* retain log file? */
+	int			recovery_timeout;		/* stop recovery after this time */
 } CreateSubscriberOptions;
 
 typedef struct LogicalRepInfo
@@ -66,29 +66,38 @@ static char *get_base_conninfo(char *conninfo, char *dbname);
 static char *get_bin_directory(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
-static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+										  const char *pub_base_conninfo,
+										  const char *sub_base_conninfo);
 static PGconn *connect_database(const char *conninfo);
 static void disconnect_database(PGconn *conn);
 static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
-static void modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static void modify_subscriber_sysid(const char *pg_bin_dir,
+									CreateSubscriberOptions *opt);
 static bool check_publisher(LogicalRepInfo *dbinfo);
 static bool setup_publisher(LogicalRepInfo *dbinfo);
 static bool check_subscriber(LogicalRepInfo *dbinfo);
-static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
-static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+static bool setup_subscriber(LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn,
+											 LogicalRepInfo *dbinfo,
 											 bool temporary);
-static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								  const char *slot_name);
 static char *setup_server_logfile(const char *datadir);
-static void start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile);
+static void start_standby_server(const char *pg_bin_dir, const char *datadir,
+								 const char *logfile);
 static void stop_standby_server(const char *pg_bin_dir, const char *datadir);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
-static void wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
+								  CreateSubscriberOptions *opt);
 static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo,
+									 const char *lsn);
 static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 
 #define	USEC_PER_SEC	1000000
@@ -115,7 +124,8 @@ enum WaitPMResult
 
 
 /*
- * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
  *
  * Replication slots, publications and subscriptions are created. Depending on
  * the step it failed, it should remove the already created objects if it is
@@ -184,11 +194,13 @@ usage(void)
 /*
  * Validate a connection string. Returns a base connection string that is a
  * connection string without a database name.
+ *
  * Since we might process multiple databases, each database name will be
- * appended to this base connection string to provide a final connection string.
- * If the second argument (dbname) is not null, returns dbname if the provided
- * connection string contains it. If option --database is not provided, uses
- * dbname as the only database to setup the logical replica.
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
  * It is the caller's responsibility to free the returned connection string and
  * dbname.
  */
@@ -291,7 +303,8 @@ check_data_directory(const char *datadir)
 		if (errno == ENOENT)
 			pg_log_error("data directory \"%s\" does not exist", datadir);
 		else
-			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+			pg_log_error("could not access directory \"%s\": %s", datadir,
+						 strerror(errno));
 
 		return false;
 	}
@@ -299,7 +312,8 @@ check_data_directory(const char *datadir)
 	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
 	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
 	{
-		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		pg_log_error("directory \"%s\" is not a database cluster directory",
+					 datadir);
 		return false;
 	}
 
@@ -334,7 +348,8 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
  * Store publication and subscription information.
  */
 static LogicalRepInfo *
-store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo)
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
 {
 	LogicalRepInfo *dbinfo;
 	SimpleStringListCell *cell;
@@ -346,7 +361,7 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, cons
 	{
 		char	   *conninfo;
 
-		/* Publisher. */
+		/* Fill attributes related with the publisher */
 		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
 		dbinfo[i].pubconninfo = conninfo;
 		dbinfo[i].dbname = cell->val;
@@ -355,7 +370,7 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, cons
 		dbinfo[i].made_subscription = false;
 		/* other struct fields will be filled later. */
 
-		/* Subscriber. */
+		/* Same as subscriber */
 		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
 		dbinfo[i].subconninfo = conninfo;
 
@@ -374,15 +389,17 @@ connect_database(const char *conninfo)
 	conn = PQconnectdb(conninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
 	{
-		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
 		return NULL;
 	}
 
-	/* secure search_path */
+	/* Secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
 		return NULL;
 	}
 	PQclear(res);
@@ -420,7 +437,8 @@ get_primary_sysid(const char *conninfo)
 	{
 		PQclear(res);
 		disconnect_database(conn);
-		pg_fatal("could not get system identifier: %s", PQresultErrorMessage(res));
+		pg_fatal("could not get system identifier: %s",
+				 PQresultErrorMessage(res));
 	}
 	if (PQntuples(res) != 1)
 	{
@@ -432,7 +450,8 @@ get_primary_sysid(const char *conninfo)
 
 	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
 
-	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
 
 	PQclear(res);
 	disconnect_database(conn);
@@ -460,7 +479,8 @@ get_standby_sysid(const char *datadir)
 
 	sysid = cf->system_identifier;
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
 
 	pfree(cf);
 
@@ -501,11 +521,13 @@ modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt)
 	if (!dry_run)
 		update_controlfile(opt->subscriber_dir, cf, true);
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
 
 	pg_log_info("running pg_resetwal on the subscriber");
 
-	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\" > \"%s\"", pg_bin_dir, opt->subscriber_dir, DEVNULL);
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\" > \"%s\"", pg_bin_dir,
+					   opt->subscriber_dir, DEVNULL);
 
 	pg_log_debug("command is: %s", cmd_str);
 
@@ -541,10 +563,12 @@ setup_publisher(LogicalRepInfo *dbinfo)
 			exit(1);
 
 		res = PQexec(conn,
-					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
 			return false;
 		}
 
@@ -555,7 +579,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
 			return false;
 		}
 
-		/* Remember database OID. */
+		/* Remember database OID */
 		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
 
 		PQclear(res);
@@ -565,7 +589,8 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
 		 * '\0').
 		 */
-		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
 		dbinfo[i].pubname = pg_strdup(pubname);
 
 		/*
@@ -578,10 +603,10 @@ setup_publisher(LogicalRepInfo *dbinfo)
 
 		/*
 		 * Build the replication slot name. The name must not exceed
-		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
-		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
-		 * probability of collision. By default, subscription name is used as
-		 * replication slot name.
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42 characters
+		 * (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the probability
+		 * of collision. By default, subscription name is used as replication
+		 * slot name.
 		 */
 		snprintf(replslotname, sizeof(replslotname),
 				 "pg_createsubscriber_%u_%d",
@@ -589,9 +614,11 @@ setup_publisher(LogicalRepInfo *dbinfo)
 				 (int) getpid());
 		dbinfo[i].subname = pg_strdup(replslotname);
 
-		/* Create replication slot on publisher. */
-		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL || dry_run)
-			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
 		else
 			return false;
 
@@ -624,24 +651,37 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * Since these parameters are not a requirement for physical replication,
 	 * we should check it to make sure it won't fail.
 	 *
-	 * wal_level = logical max_replication_slots >= current + number of dbs to
-	 * be converted max_wal_senders >= current + number of dbs to be converted
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo);
 	if (conn == NULL)
 		exit(1);
 
 	res = PQexec(conn,
-				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
-				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
-				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
-				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
-				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
-				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+				 "WITH wl AS "
+				 " (SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "  WHERE name = 'wal_level'),"
+				 "total_mrs AS "
+				 " (SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "  WHERE name = 'max_replication_slots'),"
+				 "cur_mrs AS "
+				 " (SELECT count(*) AS cmrs "
+				 "  FROM pg_catalog.pg_replication_slots),"
+				 "total_mws AS "
+				 " (SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "  WHERE name = 'max_wal_senders'),"
+				 "cur_mws AS "
+				 " (SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "  WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
 		return false;
 	}
 
@@ -668,14 +708,17 @@ check_publisher(LogicalRepInfo *dbinfo)
 	if (primary_slot_name)
 	{
 		appendPQExpBuffer(str,
-						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+						  "SELECT 1 FROM pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
 
 		pg_log_debug("command is: %s", str->data);
 
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
 			return false;
 		}
 
@@ -688,9 +731,8 @@ check_publisher(LogicalRepInfo *dbinfo)
 			return false;
 		}
 		else
-		{
-			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
-		}
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
 
 		PQclear(res);
 	}
@@ -705,15 +747,19 @@ check_publisher(LogicalRepInfo *dbinfo)
 
 	if (max_repslots - cur_repslots < num_dbs)
 	{
-		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
 		return false;
 	}
 
 	if (max_walsenders - cur_walsenders < num_dbs)
 	{
-		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
-		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
 		return false;
 	}
 
@@ -760,7 +806,14 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	 * pg_create_subscription role and CREATE privileges on the specified
 	 * database.
 	 */
-	appendPQExpBuffer(str, "SELECT pg_has_role(current_user, %u, 'MEMBER'), has_database_privilege(current_user, '%s', 'CREATE'), has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')", ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "       pg_catalog.has_database_privilege(current_user, "
+					  "                                         '%s', 'CREATE'), "
+					  "       pg_catalog.has_function_privilege(current_user, "
+					  "                                         'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', "
+					  "                                         'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -768,7 +821,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not obtain access privilege information: %s", PQresultErrorMessage(res));
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
 		return false;
 	}
 
@@ -786,7 +840,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	}
 	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
 	{
-		pg_log_error("permission denied for function \"%s\"", "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
 		return false;
 	}
 
@@ -798,16 +853,22 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	 * Since these parameters are not a requirement for physical replication,
 	 * we should check it to make sure it won't fail.
 	 *
-	 * max_replication_slots >= number of dbs to be converted
-	 * max_logical_replication_workers >= number of dbs to be converted
-	 * max_worker_processes >= 1 + number of dbs to be converted
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
 	 */
 	res = PQexec(conn,
-				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+				 "SELECT setting FROM pg_settings WHERE name IN ( "
+				 "       'max_logical_replication_workers', "
+				 "       'max_replication_slots', "
+				 "       'max_worker_processes', "
+				 "       'primary_slot_name') "
+				 "ORDER BY name");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
 		return false;
 	}
 
@@ -817,7 +878,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
 		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
 
-	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
 	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
 	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
 	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
@@ -828,22 +890,28 @@ check_subscriber(LogicalRepInfo *dbinfo)
 
 	if (max_repslots < num_dbs)
 	{
-		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
 		return false;
 	}
 
 	if (max_lrworkers < num_dbs)
 	{
-		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
-		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
 		return false;
 	}
 
 	if (max_wprocs < num_dbs + 1)
 	{
-		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
-		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
 		return false;
 	}
 
@@ -851,8 +919,9 @@ check_subscriber(LogicalRepInfo *dbinfo)
 }
 
 /*
- * Create the subscriptions, adjust the initial location for logical replication and
- * enable the subscriptions. That's the last step for logical repliation setup.
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
  */
 static bool
 setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
@@ -875,10 +944,10 @@ setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
 
 		create_subscription(conn, &dbinfo[i]);
 
-		/* Set the replication progress to the correct LSN. */
+		/* Set the replication progress to the correct LSN */
 		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
 
-		/* Enable subscription. */
+		/* Enable subscription */
 		enable_subscription(conn, &dbinfo[i]);
 
 		disconnect_database(conn);
@@ -904,22 +973,23 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 
 	Assert(conn != NULL);
 
-	/*
-	 * This temporary replication slot is only used for catchup purposes.
-	 */
+	/* This temporary replication slot is only used for catchup purposes */
 	if (temporary)
 	{
 		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
 				 (int) getpid());
 	}
 	else
-	{
 		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
-	}
 
-	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
 
-	appendPQExpBuffer(str, "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+	appendPQExpBuffer(str,
+					  "SELECT lsn "
+					  "FROM pg_create_logical_replication_slot('%s', '%s', "
+					  "                                        '%s', false, "
+					  "                                        false)",
 					  slot_name, "pgoutput", temporary ? "true" : "false");
 
 	pg_log_debug("command is: %s", str->data);
@@ -929,13 +999,14 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
 						 PQresultErrorMessage(res));
 			return lsn;
 		}
 	}
 
-	/* for cleanup purposes */
+	/* For cleanup purposes */
 	if (!temporary)
 		dbinfo->made_replslot = true;
 
@@ -951,14 +1022,16 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 }
 
 static void
-drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+					  const char *slot_name)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
 
@@ -968,8 +1041,8 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
-			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						 PQerrorMessage(conn));
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1004,7 +1077,7 @@ setup_server_logfile(const char *datadir)
 	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
 		pg_fatal("could not create directory \"%s\": %m", base_dir);
 
-	/* append timestamp with ISO 8601 format. */
+	/* Append timestamp with ISO 8601 format */
 	gettimeofday(&time, NULL);
 	tt = (time_t) time.tv_sec;
 	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
@@ -1012,7 +1085,8 @@ setup_server_logfile(const char *datadir)
 			 ".%03d", (int) (time.tv_usec / 1000));
 
 	filename = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir,
+				   PGS_OUTPUT_DIR, timebuf);
 	if (len >= MAXPGPATH)
 		pg_fatal("log file path is too long");
 
@@ -1020,12 +1094,14 @@ setup_server_logfile(const char *datadir)
 }
 
 static void
-start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile)
+start_standby_server(const char *pg_bin_dir, const char *datadir,
+					 const char *logfile)
 {
 	char	   *pg_ctl_cmd;
 	int			rc;
 
-	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"", pg_bin_dir, datadir, logfile);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"",
+						  pg_bin_dir, datadir, logfile);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 1);
 }
@@ -1036,7 +1112,8 @@ stop_standby_server(const char *pg_bin_dir, const char *datadir)
 	char	   *pg_ctl_cmd;
 	int			rc;
 
-	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", pg_bin_dir, datadir);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", pg_bin_dir,
+						  datadir);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 0);
 }
@@ -1056,7 +1133,8 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
 		else if (WIFSIGNALED(rc))
 		{
 #if defined(WIN32)
-			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
 			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
 #else
 			pg_log_error("pg_ctl was terminated by signal %d: %s",
@@ -1085,7 +1163,8 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt)
+wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
+					  CreateSubscriberOptions *opt)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -1124,16 +1203,14 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscr
 			break;
 		}
 
-		/*
-		 * Bail out after recovery_timeout seconds if this option is set.
-		 */
+		/* Bail out after recovery_timeout seconds if this option is set */
 		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
 		{
 			stop_standby_server(pg_bin_dir, opt->subscriber_dir);
 			pg_fatal("recovery timed out");
 		}
 
-		/* Keep waiting. */
+		/* Keep waiting */
 		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
 
 		timer += WAIT_INTERVAL;
@@ -1158,9 +1235,10 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	/* Check if the publication needs to be created. */
+	/* Check if the publication needs to be created */
 	appendPQExpBuffer(str,
-					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  "SELECT puballtables FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
 					  dbinfo->pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1204,9 +1282,11 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	PQclear(res);
 	resetPQExpBuffer(str);
 
-	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
 
-	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1241,7 +1321,8 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
 
@@ -1251,7 +1332,8 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1279,11 +1361,13 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
-					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  "WITH (create_slot = false, copy_data = false, "
+					  "      enabled = false)",
 					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
@@ -1319,7 +1403,8 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
 
@@ -1329,7 +1414,8 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1359,7 +1445,9 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	Assert(conn != NULL);
 
 	appendPQExpBuffer(str,
-					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1381,7 +1469,8 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	if (dry_run)
 	{
 		suboid = InvalidOid;
-		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
 	}
 	else
 	{
@@ -1402,7 +1491,9 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 
 	resetPQExpBuffer(str);
 	appendPQExpBuffer(str,
-					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', "
+					  "                                                '%s')",
+					  originname, lsnstr);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1437,7 +1528,8 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
 
@@ -1449,8 +1541,8 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			PQfinish(conn);
-			pg_fatal("could not enable subscription \"%s\": %s", dbinfo->subname,
-					 PQerrorMessage(conn));
+			pg_fatal("could not enable subscription \"%s\": %s",
+					 dbinfo->subname, PQerrorMessage(conn));
 		}
 
 		PQclear(res);
@@ -1563,7 +1655,7 @@ main(int argc, char **argv)
 				opt.sub_conninfo_str = pg_strdup(optarg);
 				break;
 			case 'd':
-				/* Ignore duplicated database names. */
+				/* Ignore duplicated database names */
 				if (!simple_string_list_member(&opt.database_names, optarg))
 				{
 					simple_string_list_append(&opt.database_names, optarg);
@@ -1584,7 +1676,8 @@ main(int argc, char **argv)
 				break;
 			default:
 				/* getopt_long already emitted a complaint */
-				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				pg_log_error_hint("Try \"%s --help\" for more information.",
+								  progname);
 				exit(1);
 		}
 	}
@@ -1627,7 +1720,8 @@ main(int argc, char **argv)
 		exit(1);
 	}
 	pg_log_info("validating connection string on publisher");
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo);
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  dbname_conninfo);
 	if (pub_base_conninfo == NULL)
 		exit(1);
 
@@ -1662,24 +1756,24 @@ main(int argc, char **argv)
 		else
 		{
 			pg_log_error("no database name specified");
-			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
 			exit(1);
 		}
 	}
 
-	/*
-	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
-	 */
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
 	pg_bin_dir = get_bin_directory(argv[0]);
 
 	/* rudimentary check for a data directory. */
 	if (!check_data_directory(opt.subscriber_dir))
 		exit(1);
 
-	/* Store database information for publisher and subscriber. */
-	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo, sub_base_conninfo);
+	/* Store database information for publisher and subscriber */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
 
-	/* Register a function to clean up objects in case of failure. */
+	/* Register a function to clean up objects in case of failure */
 	atexit(cleanup_objects_atexit);
 
 	/*
@@ -1691,9 +1785,7 @@ main(int argc, char **argv)
 	if (pub_sysid != sub_sysid)
 		pg_fatal("subscriber data directory is not a copy of the source database cluster");
 
-	/*
-	 * Create the output directory to store any data generated by this tool.
-	 */
+	/* Create the output directory to store any data generated by this tool */
 	server_start_log = setup_server_logfile(opt.subscriber_dir);
 
 	/* subscriber PID file. */
@@ -1707,9 +1799,7 @@ main(int argc, char **argv)
 	 */
 	if (stat(pidfile, &statbuf) == 0)
 	{
-		/*
-		 * Check if the standby server is ready for logical replication.
-		 */
+		/* Check if the standby server is ready for logical replication */
 		if (!check_subscriber(dbinfo))
 			exit(1);
 
@@ -1731,7 +1821,7 @@ main(int argc, char **argv)
 		if (!setup_publisher(dbinfo))
 			exit(1);
 
-		/* Stop the standby server. */
+		/* Stop the standby server */
 		pg_log_info("standby is up and running");
 		pg_log_info("stopping the server to start the transformation steps");
 		if (!dry_run)
@@ -1776,9 +1866,12 @@ main(int argc, char **argv)
 	 */
 	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
-	appendPQExpBuffer(recoveryconfcontents, "recovery_target_timeline = 'latest'\n");
-	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
-	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
@@ -1786,7 +1879,8 @@ main(int argc, char **argv)
 	if (dry_run)
 	{
 		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
-		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
 						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
 	}
 	else
@@ -1799,16 +1893,12 @@ main(int argc, char **argv)
 
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
 
-	/*
-	 * Start subscriber and wait until accepting connections.
-	 */
+	/* Start subscriber and wait until accepting connections */
 	pg_log_info("starting the subscriber");
 	if (!dry_run)
 		start_standby_server(pg_bin_dir, opt.subscriber_dir, server_start_log);
 
-	/*
-	 * Waiting the subscriber to be promoted.
-	 */
+	/* Waiting the subscriber to be promoted */
 	wait_for_end_recovery(dbinfo[0].subconninfo, pg_bin_dir, &opt);
 
 	/*
@@ -1836,22 +1926,19 @@ main(int argc, char **argv)
 		}
 		else
 		{
-			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning("could not drop replication slot \"%s\" on primary",
+						   primary_slot_name);
 			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
 		}
 		disconnect_database(conn);
 	}
 
-	/*
-	 * Stop the subscriber.
-	 */
+	/* Stop the subscriber */
 	pg_log_info("stopping the subscriber");
 	if (!dry_run)
 		stop_standby_server(pg_bin_dir, opt.subscriber_dir);
 
-	/*
-	 * Change system identifier from subscriber.
-	 */
+	/* Change system identifier from subscriber */
 	modify_subscriber_sysid(pg_bin_dir, &opt);
 
 	/*
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 0f02b1bfac..95eb4e70ac 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -18,23 +18,18 @@ my $datadir = PostgreSQL::Test::Utils::tempdir;
 command_fails(['pg_createsubscriber'],
 	'no subscriber data directory specified');
 command_fails(
-	[
-		'pg_createsubscriber',
-		'--pgdata', $datadir
-	],
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
 	'no publisher connection string specified');
 command_fails(
 	[
-		'pg_createsubscriber',
-		'--dry-run',
+		'pg_createsubscriber', '--dry-run',
 		'--pgdata', $datadir,
 		'--publisher-server', 'dbname=postgres'
 	],
 	'no subscriber connection string specified');
 command_fails(
 	[
-		'pg_createsubscriber',
-		'--verbose',
+		'pg_createsubscriber', '--verbose',
 		'--pgdata', $datadir,
 		'--publisher-server', 'dbname=postgres',
 		'--subscriber-server', 'dbname=postgres'
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 2db41cbc9b..58f9d95f3b 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -23,7 +23,7 @@ $node_p->start;
 # The extra option forces it to initialize a new cluster instead of copying a
 # previously initdb's cluster.
 $node_f = PostgreSQL::Test::Cluster->new('node_f');
-$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->init(allows_streaming => 'logical', extra => ['--no-instructions']);
 $node_f->start;
 
 # On node P
@@ -66,12 +66,13 @@ command_fails(
 # dry run mode on node S
 command_ok(
 	[
-		'pg_createsubscriber', '--verbose', '--dry-run',
-		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
-		'--subscriber-server', $node_s->connstr('pg1'),
-		'--database', 'pg1',
-		'--database', 'pg2'
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
 	],
 	'run pg_createsubscriber --dry-run on node S');
 
@@ -120,12 +121,13 @@ third row),
 
 # Check result on database pg2
 $result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
-is( $result, qq(row 1),
-	'logical replication works on database pg2');
+is($result, qq(row 1), 'logical replication works on database pg2');
 
 # Different system identifier?
-my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
-my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
 ok($sysid_p != $sysid_s, 'system identifier was changed');
 
 # clean up
-- 
2.43.0

v19-0004-Fix-argument-for-get_base_conninfo.patchapplication/octet-stream; name=v19-0004-Fix-argument-for-get_base_conninfo.patchDownload
From eb4b4e6076e46751db9cccc2f7149754fc991271 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 8 Feb 2024 13:58:48 +0000
Subject: [PATCH v19 4/9] Fix argument for get_base_conninfo

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 0ef670ae6d..291fc3967f 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -62,7 +62,7 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname);
+static char *get_base_conninfo(char *conninfo, char **dbname);
 static char *get_bin_directory(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
@@ -205,7 +205,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char *dbname)
+get_base_conninfo(char *conninfo, char **dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -227,7 +227,7 @@ get_base_conninfo(char *conninfo, char *dbname)
 		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
 		{
 			if (dbname)
-				dbname = pg_strdup(conn_opt->val);
+				*dbname = pg_strdup(conn_opt->val);
 			continue;
 		}
 
@@ -1721,7 +1721,7 @@ main(int argc, char **argv)
 	}
 	pg_log_info("validating connection string on publisher");
 	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
-										  dbname_conninfo);
+										  &dbname_conninfo);
 	if (pub_base_conninfo == NULL)
 		exit(1);
 
-- 
2.43.0

v19-0005-Add-testcase.patchapplication/octet-stream; name=v19-0005-Add-testcase.patchDownload
From 634d649fa2d51a2f2c1a2eb0cad539ad88c8904f Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 8 Feb 2024 14:05:59 +0000
Subject: [PATCH v19 5/9] Add testcase

---
 .../t/041_pg_createsubscriber_standby.pl      | 53 ++++++++++++++++---
 1 file changed, 47 insertions(+), 6 deletions(-)

diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 58f9d95f3b..d7567ef8e9 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -13,6 +13,7 @@ my $node_p;
 my $node_f;
 my $node_s;
 my $result;
+my $slotname;
 
 # Set up node P as primary
 $node_p = PostgreSQL::Test::Cluster->new('node_p');
@@ -30,6 +31,7 @@ $node_f->start;
 # - create databases
 # - create test tables
 # - insert a row
+# - create a physical relication slot
 $node_p->safe_psql(
 	'postgres', q(
 	CREATE DATABASE pg1;
@@ -38,18 +40,19 @@ $node_p->safe_psql(
 $node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
 $node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
 $node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+$slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
 
 # Set up node S as standby linking to node P
 $node_p->backup('backup_1');
 $node_s = PostgreSQL::Test::Cluster->new('node_s');
 $node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
-$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->append_conf('postgresql.conf', qq[
+log_min_messages = debug2
+primary_slot_name = '$slotname'
+]);
 $node_s->set_standby_mode();
-$node_s->start;
-
-# Insert another row on node P and wait node S to catch up
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
-$node_p->wait_for_replay_catchup($node_s);
 
 # Run pg_createsubscriber on about-to-fail node F
 command_fails(
@@ -63,6 +66,25 @@ command_fails(
 	],
 	'subscriber data directory is not a copy of the source database cluster');
 
+# Run pg_createsubscriber on the stopped node
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'target server must be running');
+
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
 # dry run mode on node S
 command_ok(
 	[
@@ -80,6 +102,17 @@ command_ok(
 is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
 	't', 'standby is in recovery');
 
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1')
+	],
+	'run pg_createsubscriber without --databases');
+
 # Run pg_createsubscriber on node S
 command_ok(
 	[
@@ -92,6 +125,14 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');
 
+ok(-d $node_s->data_dir . "/pg_createsubscriber_output.d",
+	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success");
+
+# Confirm the physical slot has been removed
+$result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'");
+is ( $result, qq(0), 'the physical replication slot specifeid as primary_slot_name has been removed');
+
 # Insert rows on P
 $node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
 $node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
-- 
2.43.0

v19-0006-Update-comments-atop-global-variables.patchapplication/octet-stream; name=v19-0006-Update-comments-atop-global-variables.patchDownload
From 2f536b423bf70bc7f0eced7e975ffde259124b99 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Tue, 13 Feb 2024 11:07:31 +0000
Subject: [PATCH v19 6/9] Update comments atop global variables

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 291fc3967f..c21fd212e1 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -103,7 +103,7 @@ static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 #define	USEC_PER_SEC	1000000
 #define	WAIT_INTERVAL	1		/* 1 second */
 
-/* Options */
+/* Global Variables */
 static const char *progname;
 
 static char *primary_slot_name = NULL;
-- 
2.43.0

v19-0007-Address-comments-from-Vignesh.patchapplication/octet-stream; name=v19-0007-Address-comments-from-Vignesh.patchDownload
From 0be661e54b28025fcbf7e62100d59313f51ce097 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Tue, 13 Feb 2024 11:57:21 +0000
Subject: [PATCH v19 7/9] Address comments from Vignesh

---
 src/bin/pg_basebackup/.gitignore            |  2 +-
 src/bin/pg_basebackup/pg_createsubscriber.c | 41 ++++++++++++---------
 2 files changed, 24 insertions(+), 19 deletions(-)

diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index b3a6f5a2fe..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,6 +1,6 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
-/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index c21fd212e1..a20cec8312 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -10,27 +10,21 @@
  *
  *-------------------------------------------------------------------------
  */
+
 #include "postgres_fe.h"
 
-#include <signal.h>
-#include <sys/stat.h>
 #include <sys/time.h>
-#include <sys/wait.h>
 #include <time.h>
 
-#include "access/xlogdefs.h"
 #include "catalog/pg_authid_d.h"
-#include "catalog/pg_control.h"
 #include "common/connect.h"
 #include "common/controldata_utils.h"
 #include "common/file_perm.h"
-#include "common/file_utils.h"
 #include "common/logging.h"
 #include "common/restricted_token.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
-#include "utils/pidfile.h"
 
 #define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
 
@@ -114,12 +108,10 @@ static bool success = false;
 static LogicalRepInfo *dbinfo;
 static int	num_dbs = 0;
 
-enum WaitPMResult
+enum PCS_WaitPMResult
 {
-	POSTMASTER_READY,
-	POSTMASTER_STANDBY,
-	POSTMASTER_STILL_STARTING,
-	POSTMASTER_FAILED
+	PCS_READY,
+	PCS_STILL_STARTING,
 };
 
 
@@ -148,8 +140,6 @@ cleanup_objects_atexit(void)
 			if (conn != NULL)
 			{
 				drop_subscription(conn, &dbinfo[i]);
-				if (dbinfo[i].made_publication)
-					drop_publication(conn, &dbinfo[i]);
 				disconnect_database(conn);
 			}
 		}
@@ -181,7 +171,7 @@ usage(void)
 	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
-	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -n, --dry-run                       check clusters only, don't change target server\n"));
 	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
 	printf(_(" -r, --retain                        retain log file after success\n"));
 	printf(_(" -v, --verbose                       output verbose messages\n"));
@@ -400,6 +390,7 @@ connect_database(const char *conninfo)
 	{
 		pg_log_error("could not clear search_path: %s",
 					 PQresultErrorMessage(res));
+		PQfinish(conn);
 		return NULL;
 	}
 	PQclear(res);
@@ -1045,6 +1036,9 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 						 slot_name, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
+
+		/* Reset a flag accordingly */
+		dbinfo->made_replslot = false;
 	}
 
 	destroyPQExpBuffer(str);
@@ -1168,7 +1162,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	int			status = POSTMASTER_STILL_STARTING;
+	int			status = PCS_STILL_STARTING;
 	int			timer = 0;
 
 	pg_log_info("waiting the postmaster to reach the consistent state");
@@ -1199,7 +1193,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
 		 */
 		if (!in_recovery || dry_run)
 		{
-			status = POSTMASTER_READY;
+			status = PCS_READY;
 			break;
 		}
 
@@ -1218,7 +1212,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
 
 	disconnect_database(conn);
 
-	if (status == POSTMASTER_STILL_STARTING)
+	if (status == PCS_STILL_STARTING)
 		pg_fatal("server did not end recovery");
 
 	pg_log_info("postmaster reached the consistent state");
@@ -1336,6 +1330,14 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
+
+		/*
+		 * XXX Apart from other dropping functions, we must not reset a flag
+		 * for publication, because the flag indicates the status of both
+		 * nodes. Even if current execution drops a publication on subscriber,
+		 * the primary still has it. This flag must be kept to remember it.
+		 */
+		dbinfo->made_publication = false;
 	}
 
 	destroyPQExpBuffer(str);
@@ -1418,6 +1420,9 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
+
+		/* Reset a flag accordingly */
+		dbinfo->made_subscription = false;
 	}
 
 	destroyPQExpBuffer(str);
-- 
2.43.0

#128Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: vignesh C (#124)
RE: speed up a logical replica setup

Dear Vignesh,

Since the original author seems bit busy, I updated the patch set.

1) Cleanup function handler flag should be reset, i.e.
dbinfo->made_replslot = false; should be there else there will be an
error during  drop replication slot cleanup in error flow:
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const
char *slot_name)
+{
+       PQExpBuffer str = createPQExpBuffer();
+       PGresult   *res;
+
+       Assert(conn != NULL);
+
+       pg_log_info("dropping the replication slot \"%s\" on database
\"%s\"", slot_name, dbinfo->dbname);
+
+       appendPQExpBuffer(str, "SELECT
pg_drop_replication_slot('%s')", slot_name);
+
+       pg_log_debug("command is: %s", str->data);

Fixed.

2) Cleanup function handler flag should be reset, i.e.
dbinfo->made_publication = false; should be there else there will be
an error during drop publication cleanup in error flow:
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+       PQExpBuffer str = createPQExpBuffer();
+       PGresult   *res;
+
+       Assert(conn != NULL);
+
+       pg_log_info("dropping publication \"%s\" on database \"%s\"",
dbinfo->pubname, dbinfo->dbname);
+
+       appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+       pg_log_debug("command is: %s", str->data);
3) Cleanup function handler flag should be reset, i.e.
dbinfo->made_subscription = false; should be there else there will be
an error during drop publication cleanup in error flow:
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+       PQExpBuffer str = createPQExpBuffer();
+       PGresult   *res;
+
+       Assert(conn != NULL);
+
+       pg_log_info("dropping subscription \"%s\" on database \"%s\"",
dbinfo->subname, dbinfo->dbname);
+
+       appendPQExpBuffer(str, "DROP SUBSCRIPTION %s",
dbinfo->subname);
+
+       pg_log_debug("command is: %s", str->data);

Fixed.

4) I was not sure if drop_publication is required here, as we will not
create any publication in subscriber node:
+               if (dbinfo[i].made_subscription)
+               {
+                       conn = connect_database(dbinfo[i].subconninfo);
+                       if (conn != NULL)
+                       {
+                               drop_subscription(conn, &dbinfo[i]);
+                               if (dbinfo[i].made_publication)
+                                       drop_publication(conn, &dbinfo[i]);
+                               disconnect_database(conn);
+                       }
+               }

Removed. But I'm not sure the cleanup is really meaningful.
See [1]/messages/by-id/TYCPR01MB1207713BEC5C379A05D65E342F54B2@TYCPR01MB12077.jpnprd01.prod.outlook.com.

5) The connection should be disconnected in case of error case:
+       /* secure search_path */
+       res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               pg_log_error("could not clear search_path: %s",
PQresultErrorMessage(res));
+               return NULL;
+       }
+       PQclear(res);

PQfisnih() was added.

6) There should be a line break before postgres_fe inclusion, to keep
it consistent:
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>

Added.

7) These includes are not required:
7.a) #include <signal.h>
7.b) #include <sys/stat.h>
7.c) #include <sys/wait.h>
7.d) #include "access/xlogdefs.h"
7.e) #include "catalog/pg_control.h"
7.f) #include "common/file_utils.h"
7.g) #include "utils/pidfile.h"

Removed.

+ *             src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_authid_d.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
8) POSTMASTER_STANDBY and POSTMASTER_FAILED are not being used, is it
required or kept for future purpose:
+enum WaitPMResult
+{
+       POSTMASTER_READY,
+       POSTMASTER_STANDBY,
+       POSTMASTER_STILL_STARTING,
+       POSTMASTER_FAILED
+};

I think they are here because WaitPMResult is just ported from pg_ctl.c.
I renamed the enumeration and removed non-necessary attributes.

9) pg_createsubscriber should be kept after pg_basebackup to maintain
the consistent order:
diff --git a/src/bin/pg_basebackup/.gitignore
b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
/pg_basebackup
/pg_receivewal
/pg_recvlogical
+/pg_createsubscriber

Addressed.

10) dry-run help message is not very clear, how about something
similar to pg_upgrade's message like "check clusters only, don't
change any data":
+       printf(_(" -d, --database=DBNAME               database to
create a subscription\n"));
+       printf(_(" -n, --dry-run                       stop before
modifying anything\n"));
+       printf(_(" -t, --recovery-timeout=SECS         seconds to wait
for recovery to end\n"));
+       printf(_(" -r, --retain                        retain log file
after success\n"));
+       printf(_(" -v, --verbose                       output verbose
messages\n"));
+       printf(_(" -V, --version                       output version
information, then exit\n"));

Changed.

New patch is available in [2]/messages/by-id/TYCPR01MB12077A6BB424A025F04A8243DF54F2@TYCPR01MB12077.jpnprd01.prod.outlook.com.

[1]: /messages/by-id/TYCPR01MB1207713BEC5C379A05D65E342F54B2@TYCPR01MB12077.jpnprd01.prod.outlook.com
[2]: /messages/by-id/TYCPR01MB12077A6BB424A025F04A8243DF54F2@TYCPR01MB12077.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#129Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#126)
RE: speed up a logical replica setup

Dear hackers,

I've replied for trackability.

Further comments for v17.

01.
This program assumes that the target server has same major version with this.
Because the target server would be restarted by same version's pg_ctl command.
I felt it should be ensured by reading the PG_VERSION.

Still investigating.

02.
pg_upgrade checked the version of using executables, like pg_ctl, postgres, and
pg_resetwal. I felt it should be as well.

Still investigating.

03. get_bin_directory
```
if (find_my_exec(path, full_path) < 0)
{
pg_log_error("The program \"%s\" is needed by %s but was not
found in the\n"
"same directory as \"%s\".\n",
"pg_ctl", progname, full_path);
```

s/"pg_ctl"/progname

The message was updated.

04.
Missing canonicalize_path()?

I found find_my_exec() calls canonicalize_path(). No need to do.

05.
Assuming that the target server is a cascade standby, i.e., it has a role as
another primary. In this case, I thought the child node would not work. Because
pg_createsubcriber runs pg_resetwal and all WAL files would be discarded at that
time. I have not tested, but should the program detect it and exit earlier?

Still investigating.

06.
wait_for_end_recovery() waits forever even if the standby has been disconnected
from the primary, right? should we check the status of the replication via
pg_stat_wal_receiver?

Still investigating.

07.
The cleanup function has couple of bugs.

* If subscriptions have been created on the database, the function also tries to
drop a publication. But it leads an ERROR because it has been already dropped.
See setup_subscriber().
* If the subscription has been created, drop_replication_slot() leads an ERROR.
Because the subscriber tried to drop the subscription while executing DROP
SUBSCRIPTION.

Only drop_publication() was removed.

08.
I found that all messages (ERROR, WARNING, INFO, etc...) would output to stderr,
but I felt it should be on stdout. Is there a reason? pg_dump outputs messages to
stderr, but the motivation might be to avoid confusion with dumps.

Still investigating.

09.
I'm not sure the cleanup for subscriber is really needed. Assuming that there
are two databases, e.g., pg1 pg2 , and we fail to create a subscription on pg2.
This can happen when the subscription which has the same name has been
already
created on the primary server.
In this case a subscirption pn pg1 would be removed. But what is a next step?
Since a timelineID on the standby server is larger than the primary (note that
the standby has been promoted once), we cannot resume the physical replication
as-is. IIUC the easiest method to retry is removing a cluster once and restarting
from pg_basebackup. If so, no need to cleanup the standby because it is
corrupted.
We just say "Please remove the cluster and recreate again".

I still think it should be, but not done yet.

New patch can be available in [1]/messages/by-id/TYCPR01MB12077A6BB424A025F04A8243DF54F2@TYCPR01MB12077.jpnprd01.prod.outlook.com.

[1]: /messages/by-id/TYCPR01MB12077A6BB424A025F04A8243DF54F2@TYCPR01MB12077.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#130Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Shubham Khanna (#125)
RE: speed up a logical replica setup

Dear Shubham,

Thanks for testing the patch!

I tried verifying few scenarios by using 5 databases and came across
the following errors:

./pg_createsubscriber -D ../new_standby -P "host=localhost port=5432
dbname=postgres" -S "host=localhost port=9000 dbname=postgres" -d db1
-d db2 -d db3 -d db4 -d db5

pg_createsubscriber: error: publisher requires 6 wal sender
processes, but only 5 remain
pg_createsubscriber: hint: Consider increasing max_wal_senders to at least 7.

It is successful only with 7 wal senders, so we can change error
messages accordingly.

pg_createsubscriber: error: publisher requires 6 replication slots,
but only 5 remain
pg_createsubscriber: hint: Consider increasing max_replication_slots
to at least 7.

It is successful only with 7 replication slots, so we can change error
messages accordingly.

I'm not a original author but I don't think it is needed. The hint message has
already suggested you to change to 7. According to the doc [1]https://www.postgresql.org/docs/devel/error-style-guide.html, the primary
message should be factual and hint message should be used for suggestions. I felt
current code followed the style. Thought?

New patch is available in [2]/messages/by-id/TYCPR01MB12077A6BB424A025F04A8243DF54F2@TYCPR01MB12077.jpnprd01.prod.outlook.com.

[1]: https://www.postgresql.org/docs/devel/error-style-guide.html
[2]: /messages/by-id/TYCPR01MB12077A6BB424A025F04A8243DF54F2@TYCPR01MB12077.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#131Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#122)
Re: speed up a logical replica setup

On Thu, Feb 8, 2024, at 12:04 AM, Hayato Kuroda (Fujitsu) wrote:

Remember the target server was a standby (read only access). I don't expect an
application trying to modify it; unless it is a buggy application.

What if the client modifies the data just after the promotion?
Naively considered, all the changes can be accepted, but are there any issues?

If someone modifies data after promotion, fine; she has to deal with conflicts,
if any. IMO it is solved adding one or two sentences in the documentation.

Regarding
GUCs, almost all of them is PGC_POSTMASTER (so it cannot be modified unless the
server is restarted). The ones that are not PGC_POSTMASTER, does not affect the
pg_createsubscriber execution [1].

IIUC, primary_conninfo and primary_slot_name is PGC_SIGHUP.

Ditto.

I'm just pointing out that this case is a different from pg_upgrade (from which
this idea was taken). I'm not saying that's a bad idea. I'm just arguing that
you might be preventing some access read only access (monitoring) when it is
perfectly fine to connect to the database and execute queries. As I said
before, the current UI allows anyone to setup the standby to accept only local
connections. Of course, it is an extra step but it is possible. However, once
you apply v16-0007, there is no option but use only local connection during the
transformation. Is it an acceptable limitation?

My remained concern is written above. If they do not problematic we may not have
to restrict them for now. At that time, changes

1) overwriting a port number,
2) setting listen_addresses = ''

It can be implemented later if people are excited by it.

are not needed, right? IIUC inconsistency of -P may be still problematic.

I still think we shouldn't have only the transformed primary_conninfo as
option.

pglogical_create_subscriber does nothing [2][3].

Oh, thanks.
Just to confirm - pglogical set shared_preload_libraries to '', should we follow or not?

The in-core logical replication does not require any library to be loaded.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#132Euler Taveira
euler@eulerto.com
In reply to: vignesh C (#124)
Re: speed up a logical replica setup

On Fri, Feb 9, 2024, at 3:27 AM, vignesh C wrote:

On Wed, 7 Feb 2024 at 10:24, Euler Taveira <euler@eulerto.com> wrote:

On Fri, Feb 2, 2024, at 6:41 AM, Hayato Kuroda (Fujitsu) wrote:

Thanks for updating the patch!

Thanks for the updated patch, few comments:
Few comments:
1) Cleanup function handler flag should be reset, i.e.
dbinfo->made_replslot = false; should be there else there will be an
error during drop replication slot cleanup in error flow:

Why? drop_replication_slot() is basically called by atexit().

2) Cleanup function handler flag should be reset, i.e.
dbinfo->made_publication = false; should be there else there will be
an error during drop publication cleanup in error flow:

Ditto. drop_publication() is basically called by atexit().

3) Cleanup function handler flag should be reset, i.e.
dbinfo->made_subscription = false; should be there else there will be
an error during drop publication cleanup in error flow:

Ditto. drop_subscription() is only called by atexit().

4) I was not sure if drop_publication is required here, as we will not
create any publication in subscriber node:
+               if (dbinfo[i].made_subscription)
+               {
+                       conn = connect_database(dbinfo[i].subconninfo);
+                       if (conn != NULL)
+                       {
+                               drop_subscription(conn, &dbinfo[i]);
+                               if (dbinfo[i].made_publication)
+                                       drop_publication(conn, &dbinfo[i]);
+                               disconnect_database(conn);
+                       }
+               }

setup_subscriber() explains the reason.

/*
* Since the publication was created before the consistent LSN, it is
* available on the subscriber when the physical replica is promoted.
* Remove publications from the subscriber because it has no use.
*/
drop_publication(conn, &dbinfo[i]);

I changed the referred code a bit because it is not reliable. Since
made_subscription was not set until we create the subscription, the
publications that were created on primary and replicated to standby won't be
removed on subscriber. Instead, it should rely on the recovery state to decide
if it should drop it.

5) The connection should be disconnected in case of error case:
+       /* secure search_path */
+       res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               pg_log_error("could not clear search_path: %s",
PQresultErrorMessage(res));
+               return NULL;
+       }
+       PQclear(res);

connect_database() is usually followed by a NULL test and exit(1) if it cannot
connect. It should be added for correctness but it is not a requirement.

7) These includes are not required:
7.a) #include <signal.h>
7.b) #include <sys/stat.h>
7.c) #include <sys/wait.h>
7.d) #include "access/xlogdefs.h"
7.e) #include "catalog/pg_control.h"
7.f) #include "common/file_utils.h"
7.g) #include "utils/pidfile.h"

Good catch. I was about to review the include files.

8) POSTMASTER_STANDBY and POSTMASTER_FAILED are not being used, is it
required or kept for future purpose:
+enum WaitPMResult
+{
+       POSTMASTER_READY,
+       POSTMASTER_STANDBY,
+       POSTMASTER_STILL_STARTING,
+       POSTMASTER_FAILED
+};

I just copied verbatim from pg_ctl. We should remove the superfluous states.

9) pg_createsubscriber should be kept after pg_basebackup to maintain
the consistent order:

Ok.

10) dry-run help message is not very clear, how about something
similar to pg_upgrade's message like "check clusters only, don't
change any data":

$ /tmp/pgdevel/bin/pg_archivecleanup --help | grep dry-run
-n, --dry-run dry run, show the names of the files that would be
$ /tmp/pgdevel/bin/pg_combinebackup --help | grep dry-run
-n, --dry-run don't actually do anything
$ /tmp/pgdevel/bin/pg_resetwal --help | grep dry-run
-n, --dry-run no update, just show what would be done
$ /tmp/pgdevel/bin/pg_rewind --help | grep dry-run
-n, --dry-run stop before modifying anything
$ /tmp/pgdevel/bin/pg_upgrade --help | grep check
-c, --check check clusters only, don't change any data

I used the same sentence as pg_rewind but I'm fine with pg_upgrade or
pg_combinebackup sentences.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#133Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#131)
RE: speed up a logical replica setup

Dear Euler,

If someone modifies data after promotion, fine; she has to deal with conflicts,
if any. IMO it is solved adding one or two sentences in the documentation.

OK. I could find issues, for now.

Regarding
GUCs, almost all of them is PGC_POSTMASTER (so it cannot be modified unless the
server is restarted). The ones that are not PGC_POSTMASTER, does not affect the
pg_createsubscriber execution [1].

IIUC, primary_conninfo and primary_slot_name is PGC_SIGHUP.

Ditto.

Just to confirm - even if the primary_slot_name would be changed during
the conversion, the slot initially set would be dropped. Currently we do not
find any issues.

I'm just pointing out that this case is a different from pg_upgrade (from which
this idea was taken). I'm not saying that's a bad idea. I'm just arguing that
you might be preventing some access read only access (monitoring) when it is
perfectly fine to connect to the database and execute queries. As I said
before, the current UI allows anyone to setup the standby to accept only local
connections. Of course, it is an extra step but it is possible. However, once
you apply v16-0007, there is no option but use only local connection during the
transformation. Is it an acceptable limitation?

My remained concern is written above. If they do not problematic we may not have
to restrict them for now. At that time, changes

1) overwriting a port number,
2) setting listen_addresses = ''

It can be implemented later if people are excited by it.

are not needed, right? IIUC inconsistency of -P may be still problematic.

I still think we shouldn't have only the transformed primary_conninfo as
option.

Hmm, OK. So let me summarize current status and discussions.

Policy)

Basically, we do not prohibit to connect to primary/standby.
primary_slot_name may be changed during the conversion and
tuples may be inserted on target just after the promotion, but it seems no issues.

API)

-D (data directory) and -d (databases) are definitively needed.

Regarding the -P (a connection string for source), we can require it for now.
But note that it may cause an inconsistency if the pointed not by -P is different
from the node pointde by primary_conninfo.

As for the connection string for the target server, we can choose two ways:
a)
accept native connection string as -S. This can reuse the same parsing mechanism as -P,
but there is a room that non-local server is specified.

b)
accept username/port as -U/-p
(Since the policy is like above, listen_addresses would not be overwritten. Also, the port just specify the listening port).
This can avoid connecting to non-local, but more options may be needed.
(E.g., Is socket directory needed? What about password?)

Other discussing point, reported issue)

Points raised by me [1]/messages/by-id/TYCPR01MB1207713BEC5C379A05D65E342F54B2@TYCPR01MB12077.jpnprd01.prod.outlook.com are not solved yet.

* What if the target version is PG16-?
* What if the found executables have diffent version with pg_createsubscriber?
* What if the target is sending WAL to another server?
I.e., there are clusters like `node1->node2-.node3`, and the target is node2.
* Can we really cleanup the standby in case of failure?
Shouldn't we suggest to remove the target once?
* Can we move outputs to stdout?

[1]: /messages/by-id/TYCPR01MB1207713BEC5C379A05D65E342F54B2@TYCPR01MB12077.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#134Shubham Khanna
khannashubham1197@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#123)
1 attachment(s)
Re: speed up a logical replica setup

On Thu, Feb 15, 2024 at 10:37 AM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear Euler,

Here are my minor comments for 17.

01.
```
/* Options */
static const char *progname;

static char *primary_slot_name = NULL;
static bool dry_run = false;

static bool success = false;

static LogicalRepInfo *dbinfo;
static int num_dbs = 0;
```

The comment seems out-of-date. There is only one option.

02. check_subscriber and check_publisher

Missing pg_catalog prefix in some lines.

03. get_base_conninfo

I think dbname would not be set. IIUC, dbname should be a pointer of the pointer.

04.

I check the coverage and found two functions have been never called:
- drop_subscription
- drop_replication_slot

Also, some cases were not tested. Below bullet showed notable ones for me.
(Some of them would not be needed based on discussions)

* -r is specified
* -t is specified
* -P option contains dbname
* -d is not specified
* GUC settings are wrong
* primary_slot_name is specified on the standby
* standby server is not working

In feature level, we may able to check the server log is surely removed in case
of success.

So, which tests should be added? drop_subscription() is called only when the
cleanup phase, so it may be difficult to test. According to others, it seems that
-r and -t are not tested. GUC-settings have many test cases so not sure they
should be. Based on this, others can be tested.

PSA my top-up patch set.

V18-0001: same as your patch, v17-0001.
V18-0002: modify the alignment of codes.
V18-0003: change an argument of get_base_conninfo. Per comment 3.
=== experimental patches ===
V18-0004: Add testcases per comment 4.
V18-0005: Remove -P option. I'm not sure it should be needed, but I made just in case.

I created a cascade Physical Replication system like
node1->node2->node3 and ran pg_createsubscriber for node2. After
running the script, I started the node2 again and found
pg_createsubscriber command was successful after which the physical
replication between node2 and node3 has been broken. I feel
pg_createsubscriber should check this scenario and throw an error in
this case to avoid breaking the cascaded replication setup. I have
attached the script which was used to verify this.

Thanks and Regards,
Shubham Khanna.

Attachments:

cascade_replication.shtext/x-sh; charset=US-ASCII; name=cascade_replication.shDownload
#135Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#133)
12 attachment(s)
RE: speed up a logical replica setup

Dear Euler,

Policy)

Basically, we do not prohibit to connect to primary/standby.
primary_slot_name may be changed during the conversion and
tuples may be inserted on target just after the promotion, but it seems no issues.

API)

-D (data directory) and -d (databases) are definitively needed.

Regarding the -P (a connection string for source), we can require it for now.
But note that it may cause an inconsistency if the pointed not by -P is different
from the node pointde by primary_conninfo.

As for the connection string for the target server, we can choose two ways:
a)
accept native connection string as -S. This can reuse the same parsing
mechanism as -P,
but there is a room that non-local server is specified.

b)
accept username/port as -U/-p
(Since the policy is like above, listen_addresses would not be overwritten. Also,
the port just specify the listening port).
This can avoid connecting to non-local, but more options may be needed.
(E.g., Is socket directory needed? What about password?)

Other discussing point, reported issue)

Points raised by me [1] are not solved yet.

* What if the target version is PG16-?
* What if the found executables have diffent version with pg_createsubscriber?
* What if the target is sending WAL to another server?
I.e., there are clusters like `node1->node2-.node3`, and the target is node2.
* Can we really cleanup the standby in case of failure?
Shouldn't we suggest to remove the target once?
* Can we move outputs to stdout?

Based on the discussion, I updated the patch set. Feel free to pick them and include.
Removing -P patch was removed, but removing -S still remained.

Also, while testing the patch set, I found some issues.

1.
Cfbot got angry [1]https://cirrus-ci.com/task/4619792833839104. This is because WIFEXITED and others are defined in <sys/wait.h>,
but the inclusion was removed per comment. Added the inclusion again.

2.
As Shubham pointed out [3]/messages/by-id/CAHv8RjJcUY23ieJc5xqg6-QeGr1Ppp4Jwbu7Mq29eqCBTDWfUw@mail.gmail.com, when we convert an intermediate node of cascading replication,
the last node would stuck. This is because a walreciever process requires nodes have the same
system identifier (in WalReceiverMain), but it would be changed by pg_createsubscriebr.

3.
Moreover, when we convert a last node of cascade, it won't work well. Because we cannot create
publications on the standby node.

4.
If the standby server was initialized as PG16-, this command would fail.
Because the API of pg_logical_create_replication_slot() were changed.

5.
Also, used pg_ctl commands must have same versions with the instance.
I think we should require all the executables and servers must be a same major version.

Based on them, below part describes attached ones:

V20-0001: same as Euler's patch, v17-0001.
V20-0002: Update docs per recent changes. Same as v19-0002
V20-0003: Modify the alignment of codes. Same as v19-0003
V20-0004: Change an argument of get_base_conninfo. Same as v19-0004
=== experimental patches ===
V20-0005: Add testcases. Same as v19-0004
V20-0006: Update a comment above global variables. Same as v19-0005
V20-0007: Address comments from Vignesh. Some parts you don't like
are reverted.
V20-0008: Fix error message in get_bin_directory(). Same as v19-0008
V20-0009: Remove -S option. Refactored from v16-0007
V20-0010: Add check versions of executables and the target, per above and [4]/messages/by-id/TYCPR01MB1207713BEC5C379A05D65E342F54B2@TYCPR01MB12077.jpnprd01.prod.outlook.com
V20-0011: Detect a disconnection while waiting the recovery, per [4]/messages/by-id/TYCPR01MB1207713BEC5C379A05D65E342F54B2@TYCPR01MB12077.jpnprd01.prod.outlook.com
V20-0012: Avoid running pg_createsubscriber for cascade physical replication, per above.

[1]: https://cirrus-ci.com/task/4619792833839104
[2]: /messages/by-id/CALDaNm1r9ZOwZamYsh6MHzb=_XvhjC_5XnTAsVecANvU9FOz6w@mail.gmail.com
[3]: /messages/by-id/CAHv8RjJcUY23ieJc5xqg6-QeGr1Ppp4Jwbu7Mq29eqCBTDWfUw@mail.gmail.com
[4]: /messages/by-id/TYCPR01MB1207713BEC5C379A05D65E342F54B2@TYCPR01MB12077.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

v20-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchapplication/octet-stream; name=v20-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchDownload
From 884280cbd41c8e6dc93088cecfc1570f195ed6d9 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v20 01/12] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica
into a logical replica. It runs on the target server and should be able
to connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.  Stop the
target server. Change the system identifier from the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  320 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1869 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   44 +
 .../t/041_pg_createsubscriber_standby.pl      |  135 ++
 src/tools/pgindent/typedefs.list              |    2 +
 10 files changed, 2399 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f5238771b7
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,320 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..b3a6f5a2fe 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,5 +1,6 @@
 /pg_basebackup
 /pg_receivewal
 /pg_recvlogical
+/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..9628f32a3e
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1869 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "access/xlogdefs.h"
+#include "catalog/pg_authid_d.h"
+#include "catalog/pg_control.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/file_utils.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+#include "utils/pidfile.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+/* Command-line options */
+typedef struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *sub_conninfo_str;	/* subscriber connection string */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+} CreateSubscriberOptions;
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name (also replication slot
+								 * name) */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char *dbname);
+static char *get_bin_directory(const char *path);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile);
+static void stop_standby_server(const char *pg_bin_dir, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+/* Options */
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STANDBY,
+	POSTMASTER_STILL_STARTING,
+	POSTMASTER_FAILED
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				drop_subscription(conn, &dbinfo[i]);
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection string.
+ * If the second argument (dbname) is not null, returns dbname if the provided
+ * connection string contains it. If option --database is not provided, uses
+ * dbname as the only database to setup the logical replica.
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Get the directory that the pg_createsubscriber is in. Since it uses other
+ * PostgreSQL binaries (pg_ctl and pg_resetwal), the directory is used to build
+ * the full path for it.
+ */
+static char *
+get_bin_directory(const char *path)
+{
+	char		full_path[MAXPGPATH];
+	char	   *dirname;
+	char	   *sep;
+
+	if (find_my_exec(path, full_path) < 0)
+	{
+		pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
+					 "same directory as \"%s\".\n",
+					 "pg_ctl", progname, full_path);
+		pg_log_error_hint("Check your installation.");
+		exit(1);
+	}
+
+	/*
+	 * Strip the file name from the path. It will be used to build the full
+	 * path for binaries used by this tool.
+	 */
+	dirname = pg_malloc(MAXPGPATH);
+	sep = strrchr(full_path, 'p');
+	Assert(sep != NULL);
+	strlcpy(dirname, full_path, sep - full_path);
+
+	pg_log_debug("pg_ctl path is:  %s/%s", dirname, "pg_ctl");
+	pg_log_debug("pg_resetwal path is:  %s/%s", dirname, "pg_resetwal");
+
+	return dirname;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Publisher. */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		dbinfo[i].made_subscription = false;
+		/* other struct fields will be filled later. */
+
+		/* Subscriber. */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: %s", PQresultErrorMessage(res));
+	}
+	if (PQntuples(res) != 1)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: got %d rows, expected %d row",
+				 PQntuples(res), 1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\" > \"%s\"", pg_bin_dir, opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID. */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher. */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	/*
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * wal_level = logical max_replication_slots >= current + number of dbs to
+	 * be converted max_wal_senders >= current + number of dbs to be converted
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn,
+				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
+				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
+				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
+				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
+				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("subscriber: wal_level: %s", wal_level);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: current replication slots: %d", cur_repslots);
+	pg_log_debug("subscriber: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("subscriber: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+		{
+			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
+		}
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/* The target server must be a standby */
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("The target server is not a standby");
+		return false;
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str, "SELECT pg_has_role(current_user, %u, 'MEMBER'), has_database_privilege(current_user, '%s', 'CREATE'), has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')", ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"", "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		return false;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * max_replication_slots >= number of dbs to be converted
+	 * max_logical_replication_workers >= number of dbs to be converted
+	 * max_worker_processes >= 1 + number of dbs to be converted
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical replication and
+ * enable the subscriptions. That's the last step for logical repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN. */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription. */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/*
+	 * This temporary replication slot is only used for catchup purposes.
+	 */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+	{
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+	}
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* for cleanup purposes */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+						 PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it is
+ * run.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* append timestamp with ISO 8601 format. */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"", pg_bin_dir, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_bin_dir, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", pg_bin_dir, datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		bool		in_recovery;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_fatal("could not obtain recovery progress");
+
+		if (PQntuples(res) != 1)
+			pg_fatal("unexpected result from pg_is_in_recovery function");
+
+		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+
+		PQclear(res);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			break;
+		}
+
+		/*
+		 * Bail out after recovery_timeout seconds if this option is set.
+		 */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_bin_dir, opt->subscriber_dir);
+			pg_fatal("recovery timed out");
+		}
+
+		/* Keep waiting. */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created. */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain publication information: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
+				 PQntuples(res), 1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not enable subscription \"%s\": %s", dbinfo->subname,
+					 PQerrorMessage(conn));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_bin_dir = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.sub_conninfo_str = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				opt.sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names. */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+	}
+
+	/*
+	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
+	 */
+	pg_bin_dir = get_bin_directory(argv[0]);
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(opt.subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber. */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure. */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/*
+	 * Create the output directory to store any data generated by this tool.
+	 */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/*
+		 * Check if the standby server is ready for logical replication.
+		 */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server. */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		if (!dry_run)
+			stop_standby_server(pg_bin_dir, opt.subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0], true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Start subscriber and wait until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	if (!dry_run)
+		start_standby_server(pg_bin_dir, opt.subscriber_dir, server_start_log);
+
+	/*
+	 * Waiting the subscriber to be promoted.
+	 */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_bin_dir, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/*
+	 * Stop the subscriber.
+	 */
+	pg_log_info("stopping the subscriber");
+	if (!dry_run)
+		stop_standby_server(pg_bin_dir, opt.subscriber_dir);
+
+	/*
+	 * Change system identifier from subscriber.
+	 */
+	modify_subscriber_sysid(pg_bin_dir, &opt);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..0f02b1bfac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,44 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--pgdata', $datadir
+	],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber',
+		'--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..2db41cbc9b
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,135 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $result;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# The extra option forces it to initialize a new cluster instead of copying a
+# previously initdb's cluster.
+$node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose', '--dry-run',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_s->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is( $result, qq(row 1),
+	'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d808aad8b0..08de2bf4e6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.43.0

v20-0002-Update-documentation.patchapplication/octet-stream; name=v20-0002-Update-documentation.patchDownload
From 77dde8b04c3b48abb72da966935e8eeafd5a70ff Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Tue, 13 Feb 2024 10:59:47 +0000
Subject: [PATCH v20 02/12] Update documentation

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 205 +++++++++++++++-------
 1 file changed, 142 insertions(+), 63 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index f5238771b7..7cdd047d67 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -48,19 +48,99 @@ PostgreSQL documentation
   </cmdsynopsis>
  </refsynopsisdiv>
 
- <refsect1>
+ <refsect1 id="r1-app-pg_createsubscriber-1">
   <title>Description</title>
   <para>
-    <application>pg_createsubscriber</application> creates a new logical
-    replica from a physical standby server.
+   The <application>pg_createsubscriber</application> creates a new <link
+   linkend="logical-replication-subscription">subscriber</link> from a physical
+   standby server.
   </para>
 
   <para>
-   The <application>pg_createsubscriber</application> should be run at the target
-   server. The source server (known as publisher server) should accept logical
-   replication connections from the target server (known as subscriber server).
-   The target server should accept local logical replication connection.
+   The <application>pg_createsubscriber</application> must be run at the target
+   server. The source server (known as publisher server) must accept both
+   normal and logical replication connections from the target server (known as
+   subscriber server). The target server must accept normal local connections.
   </para>
+
+  <para>
+   There are some prerequisites for both the source and target instance. If
+   these are not met an error will be reported.
+  </para>
+
+  <itemizedlist>
+   <listitem>
+    <para>
+     The given target data directory must have the same system identifier than the
+     source data directory.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must be used as a physical standby.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The given database user for the target instance must have privileges for
+     creating subscriptions and using functions for replication origin.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     and <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+     configured to a value greater than the number of target databases.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The source instance must have
+     <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+     <literal>logical</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases and replication slots.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases and walsenders.
+    </para>
+   </listitem>
+  </itemizedlist>
+
+  <note>
+   <para>
+    After the successful conversion, a physical replication slot configured as
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>
+    would be removed from a primary instance.
+   </para>
+
+   <para>
+    The <application>pg_createsubscriber</application> focuses on large-scale
+    systems that contain more data than 1GB.  For smaller systems, initial data
+    synchronization of <link linkend="logical-replication">logical
+    replication</link> is recommended.
+   </para>
+  </note>
  </refsect1>
 
  <refsect1>
@@ -191,7 +271,7 @@ PostgreSQL documentation
  </refsect1>
 
  <refsect1>
-  <title>Notes</title>
+  <title>How It Works</title>
 
   <para>
    The transformation proceeds in the following steps:
@@ -200,97 +280,89 @@ PostgreSQL documentation
   <procedure>
    <step>
     <para>
-     <application>pg_createsubscriber</application> checks if the given target data
-     directory has the same system identifier than the source data directory.
-     Since it uses the recovery process as one of the steps, it starts the
-     target server as a replica from the source server. If the system
-     identifier is not the same, <application>pg_createsubscriber</application> will
-     terminate with an error.
+     Checks the target can be converted.  In particular, things listed in
+     <link linkend="r1-app-pg_createsubscriber-1">above section</link> would be
+     checked.  If these are not met <application>pg_createsubscriber</application>
+     will terminate with an error.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> checks if the target data
-     directory is used by a physical replica. Stop the physical replica if it is
-     running. One of the next steps is to add some recovery parameters that
-     requires a server start. This step avoids an error.
+     Creates a publication and a logical replication slot for each specified
+     database on the source instance.  These publications and logical replication
+     slots have generated names:
+     <quote><literal>pg_createsubscriber_%u</literal></quote> (parameters:
+     Database <parameter>oid</parameter>) for publications,
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     Database <parameter>oid</parameter>, Pid <parameter>int</parameter>) for
+     replication slots.
     </para>
    </step>
-
    <step>
     <para>
-     <application>pg_createsubscriber</application> creates one replication slot for
-     each specified database on the source server. The replication slot name
-     contains a <literal>pg_createsubscriber</literal> prefix. These replication
-     slots will be used by the subscriptions in a future step.  A temporary
-     replication slot is used to get a consistent start location. This
-     consistent LSN will be used as a stopping point in the <xref
-     linkend="guc-recovery-target-lsn"/> parameter and by the
-     subscriptions as a replication starting point. It guarantees that no
-     transaction will be lost.
+     Stops the target instance.  This is needed to add some recovery parameters
+     during the conversion.
     </para>
    </step>
-
    <step>
     <para>
-     <application>pg_createsubscriber</application> writes recovery parameters into
-     the target data directory and start the target server. It specifies a LSN
-     (consistent LSN that was obtained in the previous step) of write-ahead
-     log location up to which recovery will proceed. It also specifies
-     <literal>promote</literal> as the action that the server should take once
-     the recovery target is reached. This step finishes once the server ends
-     standby mode and is accepting read-write operations.
+     Creates a temporary replication slot to get a consistent start location.
+     The slot has generated names:
+     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
+     (parameters: Pid <parameter>int</parameter>).  Got consistent LSN will be
+     used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
+     parameter and by the subscriptions as a replication starting point. It
+     guarantees that no transaction will be lost.
+    </para>
+   </step>
+   <step>
+    <para>
+     Writes recovery parameters into the target data directory and starts the
+     target instance.  It specifies a LSN (consistent LSN that was obtained in
+     the previous step) of write-ahead log location up to which recovery will
+     proceed. It also specifies <literal>promote</literal> as the action that
+     the server should take once the recovery target is reached. This step
+     finishes once the server ends standby mode and is accepting read-write
+     operations.
     </para>
    </step>
 
    <step>
     <para>
-     Next, <application>pg_createsubscriber</application> creates one publication
-     for each specified database on the source server. Each publication
-     replicates changes for all tables in the database. The publication name
-     contains a <literal>pg_createsubscriber</literal> prefix. These publication
-     will be used by a corresponding subscription in a next step.
+     Creates a subscription for each specified database on the target instance.
+     These subscriptions have generated name:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     Database <parameter>oid</parameter>, Pid <parameter>int</parameter>).
+     These subscription have same subscription options:
+     <quote><literal>create_slot = false, copy_data = false, enabled = false</literal></quote>.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> creates one subscription for
-     each specified database on the target server. Each subscription name
-     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
-     name is identical to the subscription name. It does not copy existing data
-     from the source server. It does not create a replication slot. Instead, it
-     uses the replication slot that was created in a previous step. The
-     subscription is created but it is not enabled yet. The reason is the
-     replication progress must be set to the consistent LSN but replication
-     origin name contains the subscription oid in its name. Hence, the
-     subscription will be enabled in a separate step.
+     Sets replication progress to the consistent LSN that was obtained in a
+     previous step.  This is the exact LSN to be used as a initial location for
+     each subscription.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> sets the replication progress to
-     the consistent LSN that was obtained in a previous step. When the target
-     server started the recovery process, it caught up to the consistent LSN.
-     This is the exact LSN to be used as a initial location for each
-     subscription.
+     Enables the subscription for each specified database on the target server.
+     The subscription starts streaming from the consistent LSN.
     </para>
    </step>
 
    <step>
     <para>
-     Finally, <application>pg_createsubscriber</application> enables the subscription
-     for each specified database on the target server. The subscription starts
-     streaming from the consistent LSN.
+     Stops the standby server.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> stops the target server to change
-     its system identifier.
+     Updates a system identifier on the target server.
     </para>
    </step>
   </procedure>
@@ -300,8 +372,15 @@ PostgreSQL documentation
   <title>Examples</title>
 
   <para>
-   To create a logical replica for databases <literal>hr</literal> and
-   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+   Here is an example of using <application>pg_createsubscriber</application>.
+   Before running the command, please make sure target server is stopped.
+<screen>
+<prompt>$</prompt> <userinput>pg_ctl -D /usr/local/pgsql/data stop</userinput>
+</screen>
+
+   Then run <application>pg_createsubscriber</application>. Below tries to
+   create subscriptions for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical standby:
 <screen>
 <prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
 </screen>
-- 
2.43.0

v20-0003-Follow-coding-conversions.patchapplication/octet-stream; name=v20-0003-Follow-coding-conversions.patchDownload
From a71e58b2d233cdc9c3571469d7f02571284cb084 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 8 Feb 2024 12:34:52 +0000
Subject: [PATCH v20 03/12] Follow coding conversions

---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 393 +++++++++++-------
 .../t/040_pg_createsubscriber.pl              |  11 +-
 .../t/041_pg_createsubscriber_standby.pl      |  24 +-
 3 files changed, 256 insertions(+), 172 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 9628f32a3e..0ef670ae6d 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -37,12 +37,12 @@
 /* Command-line options */
 typedef struct CreateSubscriberOptions
 {
-	char	   *subscriber_dir; /* standby/subscriber data directory */
-	char	   *pub_conninfo_str;	/* publisher connection string */
-	char	   *sub_conninfo_str;	/* subscriber connection string */
+	char	   *subscriber_dir; 		/* standby/subscriber data directory */
+	char	   *pub_conninfo_str;		/* publisher connection string */
+	char	   *sub_conninfo_str;		/* subscriber connection string */
 	SimpleStringList database_names;	/* list of database names */
-	bool		retain;			/* retain log file? */
-	int			recovery_timeout;	/* stop recovery after this time */
+	bool		retain;					/* retain log file? */
+	int			recovery_timeout;		/* stop recovery after this time */
 } CreateSubscriberOptions;
 
 typedef struct LogicalRepInfo
@@ -66,29 +66,38 @@ static char *get_base_conninfo(char *conninfo, char *dbname);
 static char *get_bin_directory(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
-static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+										  const char *pub_base_conninfo,
+										  const char *sub_base_conninfo);
 static PGconn *connect_database(const char *conninfo);
 static void disconnect_database(PGconn *conn);
 static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
-static void modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static void modify_subscriber_sysid(const char *pg_bin_dir,
+									CreateSubscriberOptions *opt);
 static bool check_publisher(LogicalRepInfo *dbinfo);
 static bool setup_publisher(LogicalRepInfo *dbinfo);
 static bool check_subscriber(LogicalRepInfo *dbinfo);
-static bool setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn);
-static char *create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+static bool setup_subscriber(LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn,
+											 LogicalRepInfo *dbinfo,
 											 bool temporary);
-static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								  const char *slot_name);
 static char *setup_server_logfile(const char *datadir);
-static void start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile);
+static void start_standby_server(const char *pg_bin_dir, const char *datadir,
+								 const char *logfile);
 static void stop_standby_server(const char *pg_bin_dir, const char *datadir);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
-static void wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
+								  CreateSubscriberOptions *opt);
 static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo,
+									 const char *lsn);
 static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 
 #define	USEC_PER_SEC	1000000
@@ -115,7 +124,8 @@ enum WaitPMResult
 
 
 /*
- * Cleanup objects that were created by pg_createsubscriber if there is an error.
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
  *
  * Replication slots, publications and subscriptions are created. Depending on
  * the step it failed, it should remove the already created objects if it is
@@ -184,11 +194,13 @@ usage(void)
 /*
  * Validate a connection string. Returns a base connection string that is a
  * connection string without a database name.
+ *
  * Since we might process multiple databases, each database name will be
- * appended to this base connection string to provide a final connection string.
- * If the second argument (dbname) is not null, returns dbname if the provided
- * connection string contains it. If option --database is not provided, uses
- * dbname as the only database to setup the logical replica.
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
  * It is the caller's responsibility to free the returned connection string and
  * dbname.
  */
@@ -291,7 +303,8 @@ check_data_directory(const char *datadir)
 		if (errno == ENOENT)
 			pg_log_error("data directory \"%s\" does not exist", datadir);
 		else
-			pg_log_error("could not access directory \"%s\": %s", datadir, strerror(errno));
+			pg_log_error("could not access directory \"%s\": %s", datadir,
+						 strerror(errno));
 
 		return false;
 	}
@@ -299,7 +312,8 @@ check_data_directory(const char *datadir)
 	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
 	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
 	{
-		pg_log_error("directory \"%s\" is not a database cluster directory", datadir);
+		pg_log_error("directory \"%s\" is not a database cluster directory",
+					 datadir);
 		return false;
 	}
 
@@ -334,7 +348,8 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
  * Store publication and subscription information.
  */
 static LogicalRepInfo *
-store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, const char *sub_base_conninfo)
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
 {
 	LogicalRepInfo *dbinfo;
 	SimpleStringListCell *cell;
@@ -346,7 +361,7 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, cons
 	{
 		char	   *conninfo;
 
-		/* Publisher. */
+		/* Fill attributes related with the publisher */
 		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
 		dbinfo[i].pubconninfo = conninfo;
 		dbinfo[i].dbname = cell->val;
@@ -355,7 +370,7 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo, cons
 		dbinfo[i].made_subscription = false;
 		/* other struct fields will be filled later. */
 
-		/* Subscriber. */
+		/* Same as subscriber */
 		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
 		dbinfo[i].subconninfo = conninfo;
 
@@ -374,15 +389,17 @@ connect_database(const char *conninfo)
 	conn = PQconnectdb(conninfo);
 	if (PQstatus(conn) != CONNECTION_OK)
 	{
-		pg_log_error("connection to database failed: %s", PQerrorMessage(conn));
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
 		return NULL;
 	}
 
-	/* secure search_path */
+	/* Secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not clear search_path: %s", PQresultErrorMessage(res));
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
 		return NULL;
 	}
 	PQclear(res);
@@ -420,7 +437,8 @@ get_primary_sysid(const char *conninfo)
 	{
 		PQclear(res);
 		disconnect_database(conn);
-		pg_fatal("could not get system identifier: %s", PQresultErrorMessage(res));
+		pg_fatal("could not get system identifier: %s",
+				 PQresultErrorMessage(res));
 	}
 	if (PQntuples(res) != 1)
 	{
@@ -432,7 +450,8 @@ get_primary_sysid(const char *conninfo)
 
 	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
 
-	pg_log_info("system identifier is %llu on publisher", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
 
 	PQclear(res);
 	disconnect_database(conn);
@@ -460,7 +479,8 @@ get_standby_sysid(const char *datadir)
 
 	sysid = cf->system_identifier;
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) sysid);
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
 
 	pfree(cf);
 
@@ -501,11 +521,13 @@ modify_subscriber_sysid(const char *pg_bin_dir, CreateSubscriberOptions *opt)
 	if (!dry_run)
 		update_controlfile(opt->subscriber_dir, cf, true);
 
-	pg_log_info("system identifier is %llu on subscriber", (unsigned long long) cf->system_identifier);
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
 
 	pg_log_info("running pg_resetwal on the subscriber");
 
-	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\" > \"%s\"", pg_bin_dir, opt->subscriber_dir, DEVNULL);
+	cmd_str = psprintf("\"%s/pg_resetwal\" -D \"%s\" > \"%s\"", pg_bin_dir,
+					   opt->subscriber_dir, DEVNULL);
 
 	pg_log_debug("command is: %s", cmd_str);
 
@@ -541,10 +563,12 @@ setup_publisher(LogicalRepInfo *dbinfo)
 			exit(1);
 
 		res = PQexec(conn,
-					 "SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database()");
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not obtain database OID: %s", PQresultErrorMessage(res));
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
 			return false;
 		}
 
@@ -555,7 +579,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
 			return false;
 		}
 
-		/* Remember database OID. */
+		/* Remember database OID */
 		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
 
 		PQclear(res);
@@ -565,7 +589,8 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
 		 * '\0').
 		 */
-		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u", dbinfo[i].oid);
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
 		dbinfo[i].pubname = pg_strdup(pubname);
 
 		/*
@@ -578,10 +603,10 @@ setup_publisher(LogicalRepInfo *dbinfo)
 
 		/*
 		 * Build the replication slot name. The name must not exceed
-		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
-		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
-		 * probability of collision. By default, subscription name is used as
-		 * replication slot name.
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42 characters
+		 * (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the probability
+		 * of collision. By default, subscription name is used as replication
+		 * slot name.
 		 */
 		snprintf(replslotname, sizeof(replslotname),
 				 "pg_createsubscriber_%u_%d",
@@ -589,9 +614,11 @@ setup_publisher(LogicalRepInfo *dbinfo)
 				 (int) getpid());
 		dbinfo[i].subname = pg_strdup(replslotname);
 
-		/* Create replication slot on publisher. */
-		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL || dry_run)
-			pg_log_info("create replication slot \"%s\" on publisher", replslotname);
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
 		else
 			return false;
 
@@ -624,24 +651,37 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * Since these parameters are not a requirement for physical replication,
 	 * we should check it to make sure it won't fail.
 	 *
-	 * wal_level = logical max_replication_slots >= current + number of dbs to
-	 * be converted max_wal_senders >= current + number of dbs to be converted
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo);
 	if (conn == NULL)
 		exit(1);
 
 	res = PQexec(conn,
-				 "WITH wl AS (SELECT setting AS wallevel FROM pg_settings WHERE name = 'wal_level'),"
-				 "     total_mrs AS (SELECT setting AS tmrs FROM pg_settings WHERE name = 'max_replication_slots'),"
-				 "     cur_mrs AS (SELECT count(*) AS cmrs FROM pg_replication_slots),"
-				 "     total_mws AS (SELECT setting AS tmws FROM pg_settings WHERE name = 'max_wal_senders'),"
-				 "     cur_mws AS (SELECT count(*) AS cmws FROM pg_stat_activity WHERE backend_type = 'walsender')"
-				 "SELECT wallevel, tmrs, cmrs, tmws, cmws FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+				 "WITH wl AS "
+				 " (SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "  WHERE name = 'wal_level'),"
+				 "total_mrs AS "
+				 " (SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "  WHERE name = 'max_replication_slots'),"
+				 "cur_mrs AS "
+				 " (SELECT count(*) AS cmrs "
+				 "  FROM pg_catalog.pg_replication_slots),"
+				 "total_mws AS "
+				 " (SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "  WHERE name = 'max_wal_senders'),"
+				 "cur_mws AS "
+				 " (SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "  WHERE backend_type = 'walsender')"
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not obtain publisher settings: %s", PQresultErrorMessage(res));
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
 		return false;
 	}
 
@@ -668,14 +708,17 @@ check_publisher(LogicalRepInfo *dbinfo)
 	if (primary_slot_name)
 	{
 		appendPQExpBuffer(str,
-						  "SELECT 1 FROM pg_replication_slots WHERE active AND slot_name = '%s'", primary_slot_name);
+						  "SELECT 1 FROM pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
 
 		pg_log_debug("command is: %s", str->data);
 
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not obtain replication slot information: %s", PQresultErrorMessage(res));
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
 			return false;
 		}
 
@@ -688,9 +731,8 @@ check_publisher(LogicalRepInfo *dbinfo)
 			return false;
 		}
 		else
-		{
-			pg_log_info("primary has replication slot \"%s\"", primary_slot_name);
-		}
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
 
 		PQclear(res);
 	}
@@ -705,15 +747,19 @@ check_publisher(LogicalRepInfo *dbinfo)
 
 	if (max_repslots - cur_repslots < num_dbs)
 	{
-		pg_log_error("publisher requires %d replication slots, but only %d remain", num_dbs, max_repslots - cur_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", cur_repslots + num_dbs);
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
 		return false;
 	}
 
 	if (max_walsenders - cur_walsenders < num_dbs)
 	{
-		pg_log_error("publisher requires %d wal sender processes, but only %d remain", num_dbs, max_walsenders - cur_walsenders);
-		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.", cur_walsenders + num_dbs);
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
 		return false;
 	}
 
@@ -760,7 +806,14 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	 * pg_create_subscription role and CREATE privileges on the specified
 	 * database.
 	 */
-	appendPQExpBuffer(str, "SELECT pg_has_role(current_user, %u, 'MEMBER'), has_database_privilege(current_user, '%s', 'CREATE'), has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')", ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "       pg_catalog.has_database_privilege(current_user, "
+					  "                                         '%s', 'CREATE'), "
+					  "       pg_catalog.has_function_privilege(current_user, "
+					  "                                         'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', "
+					  "                                         'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -768,7 +821,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not obtain access privilege information: %s", PQresultErrorMessage(res));
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
 		return false;
 	}
 
@@ -786,7 +840,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	}
 	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
 	{
-		pg_log_error("permission denied for function \"%s\"", "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
 		return false;
 	}
 
@@ -798,16 +853,22 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	 * Since these parameters are not a requirement for physical replication,
 	 * we should check it to make sure it won't fail.
 	 *
-	 * max_replication_slots >= number of dbs to be converted
-	 * max_logical_replication_workers >= number of dbs to be converted
-	 * max_worker_processes >= 1 + number of dbs to be converted
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
 	 */
 	res = PQexec(conn,
-				 "SELECT setting FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_replication_slots', 'max_worker_processes', 'primary_slot_name') ORDER BY name");
+				 "SELECT setting FROM pg_settings WHERE name IN ( "
+				 "       'max_logical_replication_workers', "
+				 "       'max_replication_slots', "
+				 "       'max_worker_processes', "
+				 "       'primary_slot_name') "
+				 "ORDER BY name");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		pg_log_error("could not obtain subscriber settings: %s", PQresultErrorMessage(res));
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
 		return false;
 	}
 
@@ -817,7 +878,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
 		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
 
-	pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers);
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
 	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
 	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
 	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
@@ -828,22 +890,28 @@ check_subscriber(LogicalRepInfo *dbinfo)
 
 	if (max_repslots < num_dbs)
 	{
-		pg_log_error("subscriber requires %d replication slots, but only %d remain", num_dbs, max_repslots);
-		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
 		return false;
 	}
 
 	if (max_lrworkers < num_dbs)
 	{
-		pg_log_error("subscriber requires %d logical replication workers, but only %d remain", num_dbs, max_lrworkers);
-		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.", num_dbs);
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
 		return false;
 	}
 
 	if (max_wprocs < num_dbs + 1)
 	{
-		pg_log_error("subscriber requires %d worker processes, but only %d remain", num_dbs + 1, max_wprocs);
-		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.", num_dbs + 1);
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
 		return false;
 	}
 
@@ -851,8 +919,9 @@ check_subscriber(LogicalRepInfo *dbinfo)
 }
 
 /*
- * Create the subscriptions, adjust the initial location for logical replication and
- * enable the subscriptions. That's the last step for logical repliation setup.
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
  */
 static bool
 setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
@@ -875,10 +944,10 @@ setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
 
 		create_subscription(conn, &dbinfo[i]);
 
-		/* Set the replication progress to the correct LSN. */
+		/* Set the replication progress to the correct LSN */
 		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
 
-		/* Enable subscription. */
+		/* Enable subscription */
 		enable_subscription(conn, &dbinfo[i]);
 
 		disconnect_database(conn);
@@ -904,22 +973,23 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 
 	Assert(conn != NULL);
 
-	/*
-	 * This temporary replication slot is only used for catchup purposes.
-	 */
+	/* This temporary replication slot is only used for catchup purposes */
 	if (temporary)
 	{
 		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
 				 (int) getpid());
 	}
 	else
-	{
 		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
-	}
 
-	pg_log_info("creating the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
 
-	appendPQExpBuffer(str, "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+	appendPQExpBuffer(str,
+					  "SELECT lsn "
+					  "FROM pg_create_logical_replication_slot('%s', '%s', "
+					  "                                        '%s', false, "
+					  "                                        false)",
 					  slot_name, "pgoutput", temporary ? "true" : "false");
 
 	pg_log_debug("command is: %s", str->data);
@@ -929,13 +999,14 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
 						 PQresultErrorMessage(res));
 			return lsn;
 		}
 	}
 
-	/* for cleanup purposes */
+	/* For cleanup purposes */
 	if (!temporary)
 		dbinfo->made_replslot = true;
 
@@ -951,14 +1022,16 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 }
 
 static void
-drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_name)
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+					  const char *slot_name)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"", slot_name, dbinfo->dbname);
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
 
@@ -968,8 +1041,8 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo, const char *slot_nam
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
-			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s", slot_name, dbinfo->dbname,
-						 PQerrorMessage(conn));
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1004,7 +1077,7 @@ setup_server_logfile(const char *datadir)
 	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
 		pg_fatal("could not create directory \"%s\": %m", base_dir);
 
-	/* append timestamp with ISO 8601 format. */
+	/* Append timestamp with ISO 8601 format */
 	gettimeofday(&time, NULL);
 	tt = (time_t) time.tv_sec;
 	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
@@ -1012,7 +1085,8 @@ setup_server_logfile(const char *datadir)
 			 ".%03d", (int) (time.tv_usec / 1000));
 
 	filename = (char *) pg_malloc0(MAXPGPATH);
-	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir, PGS_OUTPUT_DIR, timebuf);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir,
+				   PGS_OUTPUT_DIR, timebuf);
 	if (len >= MAXPGPATH)
 		pg_fatal("log file path is too long");
 
@@ -1020,12 +1094,14 @@ setup_server_logfile(const char *datadir)
 }
 
 static void
-start_standby_server(const char *pg_bin_dir, const char *datadir, const char *logfile)
+start_standby_server(const char *pg_bin_dir, const char *datadir,
+					 const char *logfile)
 {
 	char	   *pg_ctl_cmd;
 	int			rc;
 
-	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"", pg_bin_dir, datadir, logfile);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" start -D \"%s\" -s -l \"%s\"",
+						  pg_bin_dir, datadir, logfile);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 1);
 }
@@ -1036,7 +1112,8 @@ stop_standby_server(const char *pg_bin_dir, const char *datadir)
 	char	   *pg_ctl_cmd;
 	int			rc;
 
-	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", pg_bin_dir, datadir);
+	pg_ctl_cmd = psprintf("\"%s/pg_ctl\" stop -D \"%s\" -s", pg_bin_dir,
+						  datadir);
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc, 0);
 }
@@ -1056,7 +1133,8 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
 		else if (WIFSIGNALED(rc))
 		{
 #if defined(WIN32)
-			pg_log_error("pg_ctl was terminated by exception 0x%X", WTERMSIG(rc));
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
 			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
 #else
 			pg_log_error("pg_ctl was terminated by signal %d: %s",
@@ -1085,7 +1163,8 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscriberOptions *opt)
+wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
+					  CreateSubscriberOptions *opt)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -1124,16 +1203,14 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir, CreateSubscr
 			break;
 		}
 
-		/*
-		 * Bail out after recovery_timeout seconds if this option is set.
-		 */
+		/* Bail out after recovery_timeout seconds if this option is set */
 		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
 		{
 			stop_standby_server(pg_bin_dir, opt->subscriber_dir);
 			pg_fatal("recovery timed out");
 		}
 
-		/* Keep waiting. */
+		/* Keep waiting */
 		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
 
 		timer += WAIT_INTERVAL;
@@ -1158,9 +1235,10 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	/* Check if the publication needs to be created. */
+	/* Check if the publication needs to be created */
 	appendPQExpBuffer(str,
-					  "SELECT puballtables FROM pg_catalog.pg_publication WHERE pubname = '%s'",
+					  "SELECT puballtables FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
 					  dbinfo->pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1204,9 +1282,11 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	PQclear(res);
 	resetPQExpBuffer(str);
 
-	pg_log_info("creating publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
 
-	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", dbinfo->pubname);
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1241,7 +1321,8 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping publication \"%s\" on database \"%s\"", dbinfo->pubname, dbinfo->dbname);
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
 
@@ -1251,7 +1332,8 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s", dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1279,11 +1361,13 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("creating subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
-					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  "WITH (create_slot = false, copy_data = false, "
+					  "      enabled = false)",
 					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
@@ -1319,7 +1403,8 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
 
@@ -1329,7 +1414,8 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	{
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s", dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
 
 		PQclear(res);
 	}
@@ -1359,7 +1445,9 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	Assert(conn != NULL);
 
 	appendPQExpBuffer(str,
-					  "SELECT oid FROM pg_catalog.pg_subscription WHERE subname = '%s'", dbinfo->subname);
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
@@ -1381,7 +1469,8 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	if (dry_run)
 	{
 		suboid = InvalidOid;
-		snprintf(lsnstr, sizeof(lsnstr), "%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
 	}
 	else
 	{
@@ -1402,7 +1491,9 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 
 	resetPQExpBuffer(str);
 	appendPQExpBuffer(str,
-					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')", originname, lsnstr);
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', "
+					  "                                                '%s')",
+					  originname, lsnstr);
 
 	pg_log_debug("command is: %s", str->data);
 
@@ -1437,7 +1528,8 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("enabling subscription \"%s\" on database \"%s\"", dbinfo->subname, dbinfo->dbname);
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
 
@@ -1449,8 +1541,8 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 		{
 			PQfinish(conn);
-			pg_fatal("could not enable subscription \"%s\": %s", dbinfo->subname,
-					 PQerrorMessage(conn));
+			pg_fatal("could not enable subscription \"%s\": %s",
+					 dbinfo->subname, PQerrorMessage(conn));
 		}
 
 		PQclear(res);
@@ -1563,7 +1655,7 @@ main(int argc, char **argv)
 				opt.sub_conninfo_str = pg_strdup(optarg);
 				break;
 			case 'd':
-				/* Ignore duplicated database names. */
+				/* Ignore duplicated database names */
 				if (!simple_string_list_member(&opt.database_names, optarg))
 				{
 					simple_string_list_append(&opt.database_names, optarg);
@@ -1584,7 +1676,8 @@ main(int argc, char **argv)
 				break;
 			default:
 				/* getopt_long already emitted a complaint */
-				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				pg_log_error_hint("Try \"%s --help\" for more information.",
+								  progname);
 				exit(1);
 		}
 	}
@@ -1627,7 +1720,8 @@ main(int argc, char **argv)
 		exit(1);
 	}
 	pg_log_info("validating connection string on publisher");
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, dbname_conninfo);
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  dbname_conninfo);
 	if (pub_base_conninfo == NULL)
 		exit(1);
 
@@ -1662,24 +1756,24 @@ main(int argc, char **argv)
 		else
 		{
 			pg_log_error("no database name specified");
-			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
 			exit(1);
 		}
 	}
 
-	/*
-	 * Get the absolute path of pg_ctl and pg_resetwal on the subscriber.
-	 */
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
 	pg_bin_dir = get_bin_directory(argv[0]);
 
 	/* rudimentary check for a data directory. */
 	if (!check_data_directory(opt.subscriber_dir))
 		exit(1);
 
-	/* Store database information for publisher and subscriber. */
-	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo, sub_base_conninfo);
+	/* Store database information for publisher and subscriber */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
 
-	/* Register a function to clean up objects in case of failure. */
+	/* Register a function to clean up objects in case of failure */
 	atexit(cleanup_objects_atexit);
 
 	/*
@@ -1691,9 +1785,7 @@ main(int argc, char **argv)
 	if (pub_sysid != sub_sysid)
 		pg_fatal("subscriber data directory is not a copy of the source database cluster");
 
-	/*
-	 * Create the output directory to store any data generated by this tool.
-	 */
+	/* Create the output directory to store any data generated by this tool */
 	server_start_log = setup_server_logfile(opt.subscriber_dir);
 
 	/* subscriber PID file. */
@@ -1707,9 +1799,7 @@ main(int argc, char **argv)
 	 */
 	if (stat(pidfile, &statbuf) == 0)
 	{
-		/*
-		 * Check if the standby server is ready for logical replication.
-		 */
+		/* Check if the standby server is ready for logical replication */
 		if (!check_subscriber(dbinfo))
 			exit(1);
 
@@ -1731,7 +1821,7 @@ main(int argc, char **argv)
 		if (!setup_publisher(dbinfo))
 			exit(1);
 
-		/* Stop the standby server. */
+		/* Stop the standby server */
 		pg_log_info("standby is up and running");
 		pg_log_info("stopping the server to start the transformation steps");
 		if (!dry_run)
@@ -1776,9 +1866,12 @@ main(int argc, char **argv)
 	 */
 	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
-	appendPQExpBuffer(recoveryconfcontents, "recovery_target_timeline = 'latest'\n");
-	appendPQExpBuffer(recoveryconfcontents, "recovery_target_inclusive = true\n");
-	appendPQExpBuffer(recoveryconfcontents, "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
@@ -1786,7 +1879,8 @@ main(int argc, char **argv)
 	if (dry_run)
 	{
 		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
-		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%X/%X'\n",
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
 						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
 	}
 	else
@@ -1799,16 +1893,12 @@ main(int argc, char **argv)
 
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
 
-	/*
-	 * Start subscriber and wait until accepting connections.
-	 */
+	/* Start subscriber and wait until accepting connections */
 	pg_log_info("starting the subscriber");
 	if (!dry_run)
 		start_standby_server(pg_bin_dir, opt.subscriber_dir, server_start_log);
 
-	/*
-	 * Waiting the subscriber to be promoted.
-	 */
+	/* Waiting the subscriber to be promoted */
 	wait_for_end_recovery(dbinfo[0].subconninfo, pg_bin_dir, &opt);
 
 	/*
@@ -1836,22 +1926,19 @@ main(int argc, char **argv)
 		}
 		else
 		{
-			pg_log_warning("could not drop replication slot \"%s\" on primary", primary_slot_name);
+			pg_log_warning("could not drop replication slot \"%s\" on primary",
+						   primary_slot_name);
 			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
 		}
 		disconnect_database(conn);
 	}
 
-	/*
-	 * Stop the subscriber.
-	 */
+	/* Stop the subscriber */
 	pg_log_info("stopping the subscriber");
 	if (!dry_run)
 		stop_standby_server(pg_bin_dir, opt.subscriber_dir);
 
-	/*
-	 * Change system identifier from subscriber.
-	 */
+	/* Change system identifier from subscriber */
 	modify_subscriber_sysid(pg_bin_dir, &opt);
 
 	/*
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 0f02b1bfac..95eb4e70ac 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -18,23 +18,18 @@ my $datadir = PostgreSQL::Test::Utils::tempdir;
 command_fails(['pg_createsubscriber'],
 	'no subscriber data directory specified');
 command_fails(
-	[
-		'pg_createsubscriber',
-		'--pgdata', $datadir
-	],
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
 	'no publisher connection string specified');
 command_fails(
 	[
-		'pg_createsubscriber',
-		'--dry-run',
+		'pg_createsubscriber', '--dry-run',
 		'--pgdata', $datadir,
 		'--publisher-server', 'dbname=postgres'
 	],
 	'no subscriber connection string specified');
 command_fails(
 	[
-		'pg_createsubscriber',
-		'--verbose',
+		'pg_createsubscriber', '--verbose',
 		'--pgdata', $datadir,
 		'--publisher-server', 'dbname=postgres',
 		'--subscriber-server', 'dbname=postgres'
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 2db41cbc9b..58f9d95f3b 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -23,7 +23,7 @@ $node_p->start;
 # The extra option forces it to initialize a new cluster instead of copying a
 # previously initdb's cluster.
 $node_f = PostgreSQL::Test::Cluster->new('node_f');
-$node_f->init(allows_streaming => 'logical', extra => [ '--no-instructions' ]);
+$node_f->init(allows_streaming => 'logical', extra => ['--no-instructions']);
 $node_f->start;
 
 # On node P
@@ -66,12 +66,13 @@ command_fails(
 # dry run mode on node S
 command_ok(
 	[
-		'pg_createsubscriber', '--verbose', '--dry-run',
-		'--pgdata', $node_s->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
-		'--subscriber-server', $node_s->connstr('pg1'),
-		'--database', 'pg1',
-		'--database', 'pg2'
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
 	],
 	'run pg_createsubscriber --dry-run on node S');
 
@@ -120,12 +121,13 @@ third row),
 
 # Check result on database pg2
 $result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
-is( $result, qq(row 1),
-	'logical replication works on database pg2');
+is($result, qq(row 1), 'logical replication works on database pg2');
 
 # Different system identifier?
-my $sysid_p = $node_p->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
-my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()');
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
 ok($sysid_p != $sysid_s, 'system identifier was changed');
 
 # clean up
-- 
2.43.0

v20-0004-Fix-argument-for-get_base_conninfo.patchapplication/octet-stream; name=v20-0004-Fix-argument-for-get_base_conninfo.patchDownload
From 3f80d67f918deaba8ab2e36e9dcb0c5caddc5508 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 8 Feb 2024 13:58:48 +0000
Subject: [PATCH v20 04/12] Fix argument for get_base_conninfo

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 0ef670ae6d..291fc3967f 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -62,7 +62,7 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char *dbname);
+static char *get_base_conninfo(char *conninfo, char **dbname);
 static char *get_bin_directory(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
@@ -205,7 +205,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char *dbname)
+get_base_conninfo(char *conninfo, char **dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -227,7 +227,7 @@ get_base_conninfo(char *conninfo, char *dbname)
 		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
 		{
 			if (dbname)
-				dbname = pg_strdup(conn_opt->val);
+				*dbname = pg_strdup(conn_opt->val);
 			continue;
 		}
 
@@ -1721,7 +1721,7 @@ main(int argc, char **argv)
 	}
 	pg_log_info("validating connection string on publisher");
 	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
-										  dbname_conninfo);
+										  &dbname_conninfo);
 	if (pub_base_conninfo == NULL)
 		exit(1);
 
-- 
2.43.0

v20-0005-Add-testcase.patchapplication/octet-stream; name=v20-0005-Add-testcase.patchDownload
From 07d2b209ee9f9a9dd4adf1452666daac5a3e6d54 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 8 Feb 2024 14:05:59 +0000
Subject: [PATCH v20 05/12] Add testcase

---
 .../t/041_pg_createsubscriber_standby.pl      | 53 ++++++++++++++++---
 1 file changed, 47 insertions(+), 6 deletions(-)

diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 58f9d95f3b..d7567ef8e9 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -13,6 +13,7 @@ my $node_p;
 my $node_f;
 my $node_s;
 my $result;
+my $slotname;
 
 # Set up node P as primary
 $node_p = PostgreSQL::Test::Cluster->new('node_p');
@@ -30,6 +31,7 @@ $node_f->start;
 # - create databases
 # - create test tables
 # - insert a row
+# - create a physical relication slot
 $node_p->safe_psql(
 	'postgres', q(
 	CREATE DATABASE pg1;
@@ -38,18 +40,19 @@ $node_p->safe_psql(
 $node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
 $node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
 $node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+$slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
 
 # Set up node S as standby linking to node P
 $node_p->backup('backup_1');
 $node_s = PostgreSQL::Test::Cluster->new('node_s');
 $node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
-$node_s->append_conf('postgresql.conf', 'log_min_messages = debug2');
+$node_s->append_conf('postgresql.conf', qq[
+log_min_messages = debug2
+primary_slot_name = '$slotname'
+]);
 $node_s->set_standby_mode();
-$node_s->start;
-
-# Insert another row on node P and wait node S to catch up
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
-$node_p->wait_for_replay_catchup($node_s);
 
 # Run pg_createsubscriber on about-to-fail node F
 command_fails(
@@ -63,6 +66,25 @@ command_fails(
 	],
 	'subscriber data directory is not a copy of the source database cluster');
 
+# Run pg_createsubscriber on the stopped node
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'target server must be running');
+
+$node_s->start;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
 # dry run mode on node S
 command_ok(
 	[
@@ -80,6 +102,17 @@ command_ok(
 is($node_s->safe_psql('postgres', 'SELECT pg_is_in_recovery()'),
 	't', 'standby is in recovery');
 
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1')
+	],
+	'run pg_createsubscriber without --databases');
+
 # Run pg_createsubscriber on node S
 command_ok(
 	[
@@ -92,6 +125,14 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');
 
+ok(-d $node_s->data_dir . "/pg_createsubscriber_output.d",
+	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success");
+
+# Confirm the physical slot has been removed
+$result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'");
+is ( $result, qq(0), 'the physical replication slot specifeid as primary_slot_name has been removed');
+
 # Insert rows on P
 $node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
 $node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
-- 
2.43.0

v20-0006-Update-comments-atop-global-variables.patchapplication/octet-stream; name=v20-0006-Update-comments-atop-global-variables.patchDownload
From cbb0e26c8edf444f043d852a309603b9c779a4c1 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Tue, 13 Feb 2024 11:07:31 +0000
Subject: [PATCH v20 06/12] Update comments atop global variables

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 291fc3967f..c21fd212e1 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -103,7 +103,7 @@ static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 #define	USEC_PER_SEC	1000000
 #define	WAIT_INTERVAL	1		/* 1 second */
 
-/* Options */
+/* Global Variables */
 static const char *progname;
 
 static char *primary_slot_name = NULL;
-- 
2.43.0

v20-0007-Address-comments-from-Vignesh-round-two.patchapplication/octet-stream; name=v20-0007-Address-comments-from-Vignesh-round-two.patchDownload
From 5bf640f79150a488026ae7dea303c0322e7206aa Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Tue, 13 Feb 2024 11:57:21 +0000
Subject: [PATCH v20 07/12] Address comments from Vignesh, round two

---
 src/bin/pg_basebackup/.gitignore            |  2 +-
 src/bin/pg_basebackup/pg_createsubscriber.c | 25 +++++++--------------
 2 files changed, 9 insertions(+), 18 deletions(-)

diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index b3a6f5a2fe..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,6 +1,6 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
-/pg_createsubscriber
 
 /tmp_check/
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index c21fd212e1..a81654ebc8 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -10,27 +10,22 @@
  *
  *-------------------------------------------------------------------------
  */
+
 #include "postgres_fe.h"
 
-#include <signal.h>
-#include <sys/stat.h>
 #include <sys/time.h>
 #include <sys/wait.h>
 #include <time.h>
 
-#include "access/xlogdefs.h"
 #include "catalog/pg_authid_d.h"
-#include "catalog/pg_control.h"
 #include "common/connect.h"
 #include "common/controldata_utils.h"
 #include "common/file_perm.h"
-#include "common/file_utils.h"
 #include "common/logging.h"
 #include "common/restricted_token.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
-#include "utils/pidfile.h"
 
 #define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
 
@@ -114,12 +109,10 @@ static bool success = false;
 static LogicalRepInfo *dbinfo;
 static int	num_dbs = 0;
 
-enum WaitPMResult
+enum PCS_WaitPMResult
 {
-	POSTMASTER_READY,
-	POSTMASTER_STANDBY,
-	POSTMASTER_STILL_STARTING,
-	POSTMASTER_FAILED
+	PCS_READY,
+	PCS_STILL_STARTING,
 };
 
 
@@ -148,8 +141,6 @@ cleanup_objects_atexit(void)
 			if (conn != NULL)
 			{
 				drop_subscription(conn, &dbinfo[i]);
-				if (dbinfo[i].made_publication)
-					drop_publication(conn, &dbinfo[i]);
 				disconnect_database(conn);
 			}
 		}
@@ -181,7 +172,7 @@ usage(void)
 	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
-	printf(_(" -n, --dry-run                       stop before modifying anything\n"));
+	printf(_(" -n, --dry-run                       check clusters only, don't change target server\n"));
 	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
 	printf(_(" -r, --retain                        retain log file after success\n"));
 	printf(_(" -v, --verbose                       output verbose messages\n"));
@@ -1168,7 +1159,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
 {
 	PGconn	   *conn;
 	PGresult   *res;
-	int			status = POSTMASTER_STILL_STARTING;
+	int			status = PCS_STILL_STARTING;
 	int			timer = 0;
 
 	pg_log_info("waiting the postmaster to reach the consistent state");
@@ -1199,7 +1190,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
 		 */
 		if (!in_recovery || dry_run)
 		{
-			status = POSTMASTER_READY;
+			status = PCS_READY;
 			break;
 		}
 
@@ -1218,7 +1209,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
 
 	disconnect_database(conn);
 
-	if (status == POSTMASTER_STILL_STARTING)
+	if (status == PCS_STILL_STARTING)
 		pg_fatal("server did not end recovery");
 
 	pg_log_info("postmaster reached the consistent state");
-- 
2.43.0

v20-0008-Fix-error-message-for-get_bin_directory.patchapplication/octet-stream; name=v20-0008-Fix-error-message-for-get_bin_directory.patchDownload
From 425c5bafce09e0a133e26bec0b8209b9fdd253d8 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Tue, 13 Feb 2024 12:08:40 +0000
Subject: [PATCH v20 08/12] Fix error message for get_bin_directory

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index a81654ebc8..5ccab80032 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -253,9 +253,7 @@ get_bin_directory(const char *path)
 
 	if (find_my_exec(path, full_path) < 0)
 	{
-		pg_log_error("The program \"%s\" is needed by %s but was not found in the\n"
-					 "same directory as \"%s\".\n",
-					 "pg_ctl", progname, full_path);
+		pg_log_error("invalid binary directory");
 		pg_log_error_hint("Check your installation.");
 		exit(1);
 	}
-- 
2.43.0

v20-0009-Remove-S-option-to-force-unix-domain-connection.patchapplication/octet-stream; name=v20-0009-Remove-S-option-to-force-unix-domain-connection.patchDownload
From 19e845327134f499cacb01779b9f6debe2db95f6 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 6 Feb 2024 14:45:03 +0530
Subject: [PATCH v20 09/12] Remove -S option to force unix domain connection

With this patch removed -S option and added option for username(-u), port(-p)
and socket directory(-s) for standby. This helps to force standby to use
unix domain connection.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     | 36 ++++++--
 src/bin/pg_basebackup/pg_createsubscriber.c   | 91 ++++++++++++++-----
 .../t/041_pg_createsubscriber_standby.pl      | 21 +++--
 3 files changed, 109 insertions(+), 39 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 7cdd047d67..63086a8e98 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -34,11 +34,6 @@ PostgreSQL documentation
      <arg choice="plain"><option>--publisher-server</option></arg>
     </group>
     <replaceable>connstr</replaceable>
-    <group choice="req">
-     <arg choice="plain"><option>-S</option></arg>
-     <arg choice="plain"><option>--subscriber-server</option></arg>
-    </group>
-    <replaceable>connstr</replaceable>
     <group choice="req">
      <arg choice="plain"><option>-d</option></arg>
      <arg choice="plain"><option>--database</option></arg>
@@ -173,11 +168,36 @@ PostgreSQL documentation
      </varlistentry>
 
      <varlistentry>
-      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
-      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        A port number on which the target server is listening for connections.
+        Defaults to the <envar>PGPORT</envar> environment variable, if set, or
+        a compiled-in default.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable>username</replaceable></option></term>
+      <term><option>--username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        Target's user name. Defaults to the <envar>PGUSER</envar> environment
+        variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s</option> <replaceable>dir</replaceable></term>
+      <term><option>--socketdir=</option><replaceable>dir</replaceable></term>
       <listitem>
        <para>
-        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+        A directory which locales a temporary Unix socket files. If not
+        specified, <application>pg_createsubscriber</application> tries to
+        connect via TCP/IP to <literal>localhost</literal>.
        </para>
       </listitem>
      </varlistentry>
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 5ccab80032..0a70f00252 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -34,7 +34,9 @@ typedef struct CreateSubscriberOptions
 {
 	char	   *subscriber_dir; 		/* standby/subscriber data directory */
 	char	   *pub_conninfo_str;		/* publisher connection string */
-	char	   *sub_conninfo_str;		/* subscriber connection string */
+	unsigned short subport;				/* port number listen()'d by the standby */
+	char	   *subuser;				/* database user of the standby */
+	char	   *socketdir;				/* socket directory */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;					/* retain log file? */
 	int			recovery_timeout;		/* stop recovery after this time */
@@ -57,7 +59,9 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_pub_base_conninfo(char *conninfo, char **dbname);
+static char *construct_sub_conninfo(char *username, unsigned short subport,
+									char *socketdir);
 static char *get_bin_directory(const char *path);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
@@ -170,7 +174,10 @@ usage(void)
 	printf(_("\nOptions:\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
 	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
-	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -p, --port=PORT                     subscriber port number\n"));
+	printf(_(" -U, --username=NAME                 subscriber user\n"));
+	printf(_(" -s, --socketdir=DIR                 socket directory to use\n"));
+	printf(_("                                     If not specified, localhost would be used\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -n, --dry-run                       check clusters only, don't change target server\n"));
 	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
@@ -183,8 +190,8 @@ usage(void)
 }
 
 /*
- * Validate a connection string. Returns a base connection string that is a
- * connection string without a database name.
+ * Validate a connection string for the publisher. Returns a base connection
+ * string that is a connection string without a database name.
  *
  * Since we might process multiple databases, each database name will be
  * appended to this base connection string to provide a final connection
@@ -196,7 +203,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char **dbname)
+get_pub_base_conninfo(char *conninfo, char **dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -1540,6 +1547,40 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	destroyPQExpBuffer(str);
 }
 
+/*
+ * Construct a connection string toward a target server, from argument options.
+ *
+ * If inputs are the zero, default value would be used.
+ * - username: PGUSER environment value (it would not be parsed)
+ * - port: PGPORT environment value (it would not be parsed)
+ * - socketdir: localhost connection (unix-domain would not be used)
+ */
+static char *
+construct_sub_conninfo(char *username, unsigned short subport, char *sockdir)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	if (username)
+		appendPQExpBuffer(buf, "user=%s ", username);
+
+	if (subport != 0)
+		appendPQExpBuffer(buf, "port=%u ", subport);
+
+	if (sockdir)
+		appendPQExpBuffer(buf, "host=%s ", sockdir);
+	else
+		appendPQExpBuffer(buf, "host=localhost ");
+
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
 int
 main(int argc, char **argv)
 {
@@ -1549,7 +1590,9 @@ main(int argc, char **argv)
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
 		{"publisher-server", required_argument, NULL, 'P'},
-		{"subscriber-server", required_argument, NULL, 'S'},
+		{"port", required_argument, NULL, 'p'},
+		{"username", required_argument, NULL, 'U'},
+		{"socketdir", required_argument, NULL, 's'},
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
 		{"recovery-timeout", required_argument, NULL, 't'},
@@ -1605,7 +1648,9 @@ main(int argc, char **argv)
 	/* Default settings */
 	opt.subscriber_dir = NULL;
 	opt.pub_conninfo_str = NULL;
-	opt.sub_conninfo_str = NULL;
+	opt.subport = 0;
+	opt.subuser = NULL;
+	opt.socketdir = NULL;
 	opt.database_names = (SimpleStringList)
 	{
 		NULL, NULL
@@ -1629,7 +1674,7 @@ main(int argc, char **argv)
 
 	get_restricted_token();
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+	while ((c = getopt_long(argc, argv, "D:P:p:U:s:S:d:nrt:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1640,8 +1685,17 @@ main(int argc, char **argv)
 			case 'P':
 				opt.pub_conninfo_str = pg_strdup(optarg);
 				break;
-			case 'S':
-				opt.sub_conninfo_str = pg_strdup(optarg);
+			case 'p':
+				if ((opt.subport = atoi(optarg)) <= 0)
+					pg_fatal("invalid old port number");
+				break;
+			case 'U':
+				pfree(opt.subuser);
+				opt.subuser = pg_strdup(optarg);
+				break;
+			case 's':
+				pfree(opt.socketdir);
+				opt.socketdir = pg_strdup(optarg);
 				break;
 			case 'd':
 				/* Ignore duplicated database names */
@@ -1709,21 +1763,12 @@ main(int argc, char **argv)
 		exit(1);
 	}
 	pg_log_info("validating connection string on publisher");
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
-										  &dbname_conninfo);
+	pub_base_conninfo = get_pub_base_conninfo(opt.pub_conninfo_str,
+											  &dbname_conninfo);
 	if (pub_base_conninfo == NULL)
 		exit(1);
 
-	if (opt.sub_conninfo_str == NULL)
-	{
-		pg_log_error("no subscriber connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	pg_log_info("validating connection string on subscriber");
-	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
-	if (sub_base_conninfo == NULL)
-		exit(1);
+	sub_base_conninfo = construct_sub_conninfo(opt.subuser, opt.subport, opt.socketdir);
 
 	if (opt.database_names.head == NULL)
 	{
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index d7567ef8e9..55781423cf 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -60,7 +60,8 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
 		'--publisher-server', $node_p->connstr('pg1'),
-		'--subscriber-server', $node_f->connstr('pg1'),
+		'--port', $node_f->port,
+		'--host', $node_f->host,
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
@@ -72,8 +73,9 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1'), '--database',
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--host',
+		$node_s->host, '--database',
 		'pg1', '--database',
 		'pg2'
 	],
@@ -91,8 +93,9 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1'), '--database',
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--socketdir',
+		$node_s->host, '--database',
 		'pg1', '--database',
 		'pg2'
 	],
@@ -108,8 +111,9 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1')
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--socketdir',
+		$node_s->host,
 	],
 	'run pg_createsubscriber without --databases');
 
@@ -119,7 +123,8 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_s->data_dir,
 		'--publisher-server', $node_p->connstr('pg1'),
-		'--subscriber-server', $node_s->connstr('pg1'),
+		'--port', $node_s->port,
+		'--socketdir', $node_s->host,
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
-- 
2.43.0

v20-0010-Add-version-check-for-executables-and-standby-se.patchapplication/octet-stream; name=v20-0010-Add-version-check-for-executables-and-standby-se.patchDownload
From 54098217b07bb49f724f57aa29f5da36e85ee0af Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 14 Feb 2024 16:27:15 +0530
Subject: [PATCH v20 10/12] Add version check for executables and standby
 server

Add version check for executables and standby server
---
 doc/src/sgml/ref/pg_createsubscriber.sgml   |  6 ++
 src/bin/pg_basebackup/pg_createsubscriber.c | 67 +++++++++++++++++++++
 2 files changed, 73 insertions(+)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 63086a8e98..579e50a0a0 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -120,6 +120,12 @@ PostgreSQL documentation
      databases and walsenders.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     Both the target and source instances must have same major versions with
+     <application>pg_createsubscriber</application>.
+    </para>
+   </listitem>
   </itemizedlist>
 
   <note>
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 0a70f00252..db088024d3 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -23,6 +23,7 @@
 #include "common/file_perm.h"
 #include "common/logging.h"
 #include "common/restricted_token.h"
+#include "common/string.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
@@ -63,6 +64,7 @@ static char *get_pub_base_conninfo(char *conninfo, char **dbname);
 static char *construct_sub_conninfo(char *username, unsigned short subport,
 									char *socketdir);
 static char *get_bin_directory(const char *path);
+static void check_exec(const char *dir, const char *program);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
 static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
@@ -277,6 +279,11 @@ get_bin_directory(const char *path)
 	pg_log_debug("pg_ctl path is:  %s/%s", dirname, "pg_ctl");
 	pg_log_debug("pg_resetwal path is:  %s/%s", dirname, "pg_resetwal");
 
+	/* Check version of binaries */
+	check_exec(dirname, "postgres");
+	check_exec(dirname, "pg_ctl");
+	check_exec(dirname, "pg_resetwal");
+
 	return dirname;
 }
 
@@ -290,6 +297,8 @@ check_data_directory(const char *datadir)
 {
 	struct stat statbuf;
 	char		versionfile[MAXPGPATH];
+	FILE	   *ver_fd;
+	char		rawline[64];
 
 	pg_log_info("checking if directory \"%s\" is a cluster data directory",
 				datadir);
@@ -313,6 +322,31 @@ check_data_directory(const char *datadir)
 		return false;
 	}
 
+	/* Check standby server version */
+	if ((ver_fd = fopen(versionfile, "r")) == NULL)
+		pg_fatal("could not open file \"%s\" for reading: %m", versionfile);
+
+	/* Version number has to be the first line read */
+	if (!fgets(rawline, sizeof(rawline), ver_fd))
+	{
+		if (!ferror(ver_fd))
+			pg_fatal("unexpected empty file \"%s\"", versionfile);
+		else
+			pg_fatal("could not read file \"%s\": %m", versionfile);
+	}
+
+	/* Strip trailing newline and carriage return */
+	(void) pg_strip_crlf(rawline);
+
+	if (strcmp(rawline, PG_MAJORVERSION) != 0)
+	{
+		pg_log_error("standby server is of wrong version");
+		pg_log_error_detail("File \"%s\" contains \"%s\", which is not compatible with this program's version \"%s\".",
+							versionfile, rawline, PG_MAJORVERSION);
+		exit(1);
+	}
+
+	fclose(ver_fd);
 	return true;
 }
 
@@ -1581,6 +1615,39 @@ construct_sub_conninfo(char *username, unsigned short subport, char *sockdir)
 	return ret;
 }
 
+/*
+ * Make sure the given program has the same version with pg_createsubscriber.
+ */
+static void
+check_exec(const char *dir, const char *program)
+{
+	char		path[MAXPGPATH];
+	char	   *line;
+	char		cmd[MAXPGPATH];
+	char		versionstr[128];
+
+	snprintf(path, sizeof(path), "%s/%s", dir, program);
+
+	if (validate_exec(path) != 0)
+		pg_fatal("check for \"%s\" failed: %m", path);
+
+	snprintf(cmd, sizeof(cmd), "\"%s\" -V", path);
+
+	if ((line = pipe_read_line(cmd)) == NULL)
+		pg_fatal("check for \"%s\" failed: cannot execute", path);
+
+	pg_strip_crlf(line);
+
+	snprintf(versionstr, sizeof(versionstr), "%s (PostgreSQL) " PG_VERSION,
+			 program);
+
+	if (strcmp(line, versionstr) != 0)
+		pg_fatal("check for \"%s\" failed: incorrect version: found \"%s\", expected \"%s\"",
+				 path, line, versionstr);
+
+	pg_free(line);
+}
+
 int
 main(int argc, char **argv)
 {
-- 
2.43.0

v20-0011-Detect-the-disconnection-from-the-primary-during.patchapplication/octet-stream; name=v20-0011-Detect-the-disconnection-from-the-primary-during.patchDownload
From f04540339130d0c06998dd5f2bc9b7eedba5d3f3 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 15 Feb 2024 02:47:38 +0000
Subject: [PATCH v20 11/12] Detect the disconnection from the primary during
 the recovery

Previously, the wait_for_end_recovery() function would indefinitely wait for a
server to exit recovery mode, without considering scenarios where the server
might be disconnected from the primary. This could lead to situations where the
server never reaches a consistent state, as it remains unaware of its
disconnection.

This patch introduces a new check within the wait_for_end_recovery() process,
leveraging the pg_stat_wal_receiver system view to verify the presence of an
active walreceiver process. While this method does not account for potential
frequent restarts of the walreceiver, it provides a straightforward and effective
means to detect disconnections from the primary server during the recovery phase.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index db088024d3..2458c874e5 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1209,9 +1209,12 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
 
 	for (;;)
 	{
-		bool		in_recovery;
+		bool		in_recovery,
+					still_alive;
 
-		res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+		res = PQexec(conn,
+					"SELECT pg_catalog.pg_is_in_recovery(), count(pid) "
+					"FROM pg_catalog.pg_stat_wal_receiver;");
 
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 			pg_fatal("could not obtain recovery progress");
@@ -1220,6 +1223,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
 			pg_fatal("unexpected result from pg_is_in_recovery function");
 
 		in_recovery = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
+		still_alive = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
 
 		PQclear(res);
 
@@ -1233,6 +1237,13 @@ wait_for_end_recovery(const char *conninfo, const char *pg_bin_dir,
 			break;
 		}
 
+		/* Bail out if we have disconnected from the primary */
+		if (!still_alive)
+		{
+			stop_standby_server(pg_bin_dir, opt->subscriber_dir);
+			pg_fatal("disconnected from the primary while waiting the end of recovery");
+		}
+
 		/* Bail out after recovery_timeout seconds if this option is set */
 		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
 		{
-- 
2.43.0

v20-0012-Avoid-running-pg_createsubscriber-for-cascade-ph.patchapplication/octet-stream; name=v20-0012-Avoid-running-pg_createsubscriber-for-cascade-ph.patchDownload
From 4dd92f56f110fa7fea3727c945368e8902d3b9b5 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 15 Feb 2024 15:24:11 +0530
Subject: [PATCH v20 12/12] Avoid running pg_createsubscriber for cascade
 physical replication

pg_createsubscriber will throw error when run on a node which is
part of cascade physical replication.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml   |  8 +++-
 src/bin/pg_basebackup/pg_createsubscriber.c | 45 +++++++++++++++++++--
 2 files changed, 48 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 579e50a0a0..115e6a2210 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -72,7 +72,8 @@ PostgreSQL documentation
    </listitem>
    <listitem>
     <para>
-     The target instance must be used as a physical standby.
+     The target instance must be used as a physical standby, and must not do
+     the cascading replication.
     </para>
    </listitem>
    <listitem>
@@ -97,6 +98,11 @@ PostgreSQL documentation
      configured to a value greater than the number of target databases.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     The target instance must not be used as a physical standby.
+    </para>
+   </listitem>
    <listitem>
     <para>
      The source instance must have
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 2458c874e5..05b4783f70 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -675,6 +675,27 @@ check_publisher(LogicalRepInfo *dbinfo)
 	int			cur_walsenders;
 
 	pg_log_info("checking settings on publisher");
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/*
+	 * The primary server must not be a cascading standby to other node because
+	 * publications would be created.
+	 */
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress");
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+	{
+		pg_log_error("the primary server is a standby to other server");
+		return false;
+	}
 
 	/*
 	 * Logical replication requires a few parameters to be set on publisher.
@@ -685,10 +706,6 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * - max_replication_slots >= current + number of dbs to be converted
 	 * - max_wal_senders >= current + number of dbs to be converted
 	 */
-	conn = connect_database(dbinfo[0].pubconninfo);
-	if (conn == NULL)
-		exit(1);
-
 	res = PQexec(conn,
 				 "WITH wl AS "
 				 " (SELECT setting AS wallevel FROM pg_catalog.pg_settings "
@@ -831,6 +848,26 @@ check_subscriber(LogicalRepInfo *dbinfo)
 		return false;
 	}
 
+	/*
+	 * The target server must not be primary for other server. Because the
+	 * pg_createsubscriber would modify the system_identifier at the end of
+	 * run, but walreceiver of another standby would not accept the difference.
+	 */
+	res = PQexec(conn,
+				 "SELECT count(*) from pg_stat_activity where backend_type = 'walsender'");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain walsender information");
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "0") != 0)
+	{
+		pg_log_error("the target server is primary to other server");
+		return false;
+	}
+
 	/*
 	 * Subscriptions can only be created by roles that have the privileges of
 	 * pg_create_subscription role and CREATE privileges on the specified
-- 
2.43.0

#136Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#135)
1 attachment(s)
Re: speed up a logical replica setup

On Thu, Feb 15, 2024, at 8:23 AM, Hayato Kuroda (Fujitsu) wrote:

Points raised by me [1] are not solved yet.

* What if the target version is PG16-?

pg_ctl and pg_resetwal won't work.

$ pg_ctl start -D /tmp/blah
waiting for server to start....
2024-02-15 23:50:03.448 -03 [364610] FATAL: database files are incompatible with server
2024-02-15 23:50:03.448 -03 [364610] DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17devel.
stopped waiting
pg_ctl: could not start server
Examine the log output.

$ pg_resetwal -D /tmp/blah
pg_resetwal: error: data directory is of wrong version
pg_resetwal: detail: File "PG_VERSION" contains "16", which is not compatible with this program's version "17".

* What if the found executables have diffent version with pg_createsubscriber?

The new code take care of it.

* What if the target is sending WAL to another server?
I.e., there are clusters like `node1->node2-.node3`, and the target is node2.

The new code detects if the server is in recovery and aborts as you suggested.
A new option can be added to ignore the fact there are servers receiving WAL
from it.

* Can we really cleanup the standby in case of failure?
Shouldn't we suggest to remove the target once?

If it finishes the promotion, no. I adjusted the cleanup routine a bit to avoid
it. However, we should provide instructions to inform the user that it should
create a fresh standby and try again.

* Can we move outputs to stdout?

Are you suggesting to use another logging framework? It is not a good idea
because each client program is already using common/logging.c.

1.
Cfbot got angry [1]. This is because WIFEXITED and others are defined in <sys/wait.h>,
but the inclusion was removed per comment. Added the inclusion again.

Ok.

2.
As Shubham pointed out [3], when we convert an intermediate node of cascading replication,
the last node would stuck. This is because a walreciever process requires nodes have the same
system identifier (in WalReceiverMain), but it would be changed by pg_createsubscriebr.

Hopefully it was fixed.

3.
Moreover, when we convert a last node of cascade, it won't work well. Because we cannot create
publications on the standby node.

Ditto.

4.
If the standby server was initialized as PG16-, this command would fail.
Because the API of pg_logical_create_replication_slot() were changed.

See comment above.

5.
Also, used pg_ctl commands must have same versions with the instance.
I think we should require all the executables and servers must be a same major version.

It is enforced by the new code. See find_other_exec() in get_exec_path().

Based on them, below part describes attached ones:

Thanks for another review. I'm sharing a new patch to merge a bunch of
improvements and fixes. Comments are below.

v20-0002: I did some extensive documentation changes (including some of them
related to the changes in the new patch). I will defer its update to check
v20-0002. It will be included in the next one.

v20-0003: I included most of it. There are a few things that pgindent reverted
so I didn't apply. I also didn't like some SQL commands that were broken into
multiple lines with spaces at the beginning. It seems nice in the code but it
is not in the output.

v20-0004: Nice catch. Applied.

v20-0005: Applied.

v20-0006: I prefer to remove the comment.

v20-0007: I partially applied it. I only removed the states that were not used
and propose another dry run mode message. Maybe it is clear than it was.

v20-0008: I refactored the get_bin_directory code. Under reflection, I reverted
the unified binary directory that we agreed a few days ago. The main reason is
to provide a specific error message for each program it is using. The
get_exec_path will check if the program is available in the same directory as
pg_createsubscriber and if it has the same version. An absolute path is
returned and is used by some functions.

v20-0009: to be reviewed.

v20-0010: As I said above, this code was refactored so I didn't apply this one.

v20-0011: Do we really want to interrupt the recovery if the network was
momentarily interrupted or if the OS killed walsender? Recovery is critical for
the process. I think we should do our best to be resilient and deliver all
changes required by the new subscriber. The proposal is not correct because the
query return no tuples if it is disconnected so you cannot PQgetvalue(). The
retry interval (the time that ServerLoop() will create another walreceiver) is
determined by DetermineSleepTime() and it is a maximum of 5 seconds
(SIGKILL_CHILDREN_AFTER_SECS). One idea is to retry 2 or 3 times before give up
using the pg_stat_wal_receiver query. Do you have a better plan?

v20-0012: I applied a different patch to accomplish the same thing. I included
a refactor around pg_is_in_recovery() function to be used in other 2 points.

Besides that, I changed some SQL commands to avoid having superfluous
whitespace in it. I also added a test for cascaded replication scenario. And
clean up 041 test a bit.

I didn't provide an updated documentation because I want to check v20-0002. It
is on my list to check v20-0009.

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v21-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchtext/x-patch; name="=?UTF-8?Q?v21-0001-Creates-a-new-logical-replica-from-a-standby-ser.patc?= =?UTF-8?Q?h?="Download
From 0073e1f7ea5b1c225e773707cbbe67cb1593823e Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v21] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica
into a logical replica. It runs on the target server and should be able
to connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.  Stop the
target server. Change the system identifier from the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  320 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1972 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   39 +
 .../t/041_pg_createsubscriber_standby.pl      |  217 ++
 src/tools/pgindent/typedefs.list              |    2 +
 10 files changed, 2579 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f5238771b7
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,320 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..205a835d36
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1972 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+/* Command-line options */
+typedef struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *sub_conninfo_str;	/* subscriber connection string */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+} CreateSubscriberOptions;
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name / replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_exec_path(const char *argv0, const char *progname);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+										  const char *pub_base_conninfo,
+										  const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path,
+									CreateSubscriberOptions *opt);
+static int	server_is_in_recovery(PGconn *conn);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn,
+											 LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void start_standby_server(const char *pg_ctl_path, const char *datadir,
+								 const char *logfile);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+								  CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static bool recovery_ended = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription || recovery_ended)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_subscription)
+					drop_subscription(conn, &dbinfo[i]);
+
+				/*
+				 * Publications are created on publisher before promotion so
+				 * it might exist on subscriber after recovery ends.
+				 */
+				if (recovery_ended)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir,
+						 strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory",
+					 datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		dbinfo[i].made_subscription = false;
+		/* Other fields will be filled later */
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: %s",
+				 PQresultErrorMessage(res));
+	}
+	if (PQntuples(res) != 1)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: got %d rows, expected %d row",
+				 PQntuples(res), 1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is recovery still in progress?
+ * If the answer is yes, it returns 1, otherwise, returns 0. If an error occurs
+ * while executing the query, it returns -1.
+ */
+static int
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		pg_log_error("could not obtain recovery progress");
+		return -1;
+	}
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	if (ret == 0)
+		return 1;
+	else if (ret > 0)
+		return 0;
+	else
+		return -1;				/* should not happen */
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn) == 1)
+		pg_fatal("primary server cannot be in recovery");
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/* The target server must be a standby */
+	if (server_is_in_recovery(conn) == 0)
+	{
+		pg_log_error("The target server is not a standby");
+		return false;
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		return false;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/* This temporary replication slot is only used for catchup purposes */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* Cleanup if there is any failure */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it is
+ * run.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* Append timestamp with ISO 8601 format */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir,
+				   PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_ctl_path, const char *datadir,
+					 const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"",
+						  pg_ctl_path, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+					  CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		int			in_recovery;
+
+		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (in_recovery == 0 || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			pg_fatal("recovery timed out");
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain publication information: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
+				 PQntuples(res), 1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not enable subscription \"%s\": %s",
+					 dbinfo->subname, PQerrorMessage(conn));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_ctl_path = NULL;
+	char	   *pg_resetwal_path = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.sub_conninfo_str = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(opt.subscriber_dir);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				opt.sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(opt.subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Create the output directory to store any data generated by this tool */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/* Check if the standby server is ready for logical replication */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		if (!dry_run)
+			stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0], true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/* Start subscriber and wait until accepting connections */
+	pg_log_info("starting the subscriber");
+	if (!dry_run)
+		start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary",
+						   primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	if (!dry_run)
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(pg_resetwal_path, &opt);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..95eb4e70ac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,39 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..e2807d3fac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,217 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $node_c;
+my $result;
+my $slotname;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster.
+{
+	local $ENV{'INITDB_TEMPLATE'} = undef;
+
+	$node_f = PostgreSQL::Test::Cluster->new('node_f');
+	$node_f->init(allows_streaming => 'logical');
+	$node_f->start;
+}
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+$slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+log_min_messages = debug2
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Run pg_createsubscriber on the stopped node
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'target server must be running');
+
+$node_s->start;
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+$node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->append_conf(
+	'postgresql.conf', qq[
+log_min_messages = debug2
+]);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'), '--subscriber-server',
+		$node_c->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'primary server is in recovery');
+
+# Stop node C
+$node_c->teardown_node;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1')
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+ok( -d $node_s->data_dir . "/pg_createsubscriber_output.d",
+	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success"
+);
+
+# Confirm the physical replication slot has been removed
+$result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d808aad8b0..08de2bf4e6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.30.2

#137Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#136)
RE: speed up a logical replica setup

Dear Euler,

Thanks for updating the patch!
Before reviewing deeply, here are replies for your comments.

Points raised by me [1] are not solved yet.

* What if the target version is PG16-?

pg_ctl and pg_resetwal won't work.
$ pg_ctl start -D /tmp/blah
waiting for server to start....
2024-02-15 23:50:03.448 -03 [364610] FATAL: database files are incompatible with server
2024-02-15 23:50:03.448 -03 [364610] DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17devel.
stopped waiting
pg_ctl: could not start server
Examine the log output.

$ pg_resetwal -D /tmp/blah
pg_resetwal: error: data directory is of wrong version
pg_resetwal: detail: File "PG_VERSION" contains "16", which is not compatible with this program's version "17".

* What if the found executables have diffent version with pg_createsubscriber?

The new code take care of it.

I preferred to have a common path and test one by one, but agreed this worked well.
Let's keep it and hear opinions from others.

* What if the target is sending WAL to another server?
I.e., there are clusters like `node1->node2-.node3`, and the target is node2.

The new code detects if the server is in recovery and aborts as you suggested.
A new option can be added to ignore the fact there are servers receiving WAL
from it.

Confirmed it can detect.

* Can we really cleanup the standby in case of failure?
Shouldn't we suggest to remove the target once?

If it finishes the promotion, no. I adjusted the cleanup routine a bit to avoid
it. However, we should provide instructions to inform the user that it should
create a fresh standby and try again.

I think the cleanup function looks not sufficient. In v21, recovery_ended is kept
to true even after drop_publication() is done in setup_subscriber(). I think that
made_subscription is not needed, and the function should output some messages
when recovery_ended is on.
Besides, in case of pg_upgrade, they always report below messages before starting
the migration. I think this is more helpful for users.

```
pg_log(PG_REPORT, "\n"
"If pg_upgrade fails after this point, you must re-initdb the\n"
"new cluster before continuing.");
```

* Can we move outputs to stdout?

Are you suggesting to use another logging framework? It is not a good idea
because each client program is already using common/logging.c.

Hmm, indeed. Other programs in pg_basebackup seem to use the framework.

v20-0011: Do we really want to interrupt the recovery if the network was
momentarily interrupted or if the OS killed walsender? Recovery is critical for
the process. I think we should do our best to be resilient and deliver all
changes required by the new subscriber.

It might be too strict to raise an ERROR as soon as we met a disconnection.
And at least 0011 was wrong - it should be PQgetvalue(res, 0, 1) for still_alive.

The proposal is not correct because the
query return no tuples if it is disconnected so you cannot PQgetvalue().

Sorry for misunderstanding, but you might be confused. pg_createsubcriber
sends a query to target, and we are discussing the disconnection between the
target and source instances. I think the connection which pg_createsubscriber
has is stil alive so PQgetvalue() can get a value.

(BTW, callers of server_is_in_recovery() has not considered a disconnection from
the target...)

The
retry interval (the time that ServerLoop() will create another walreceiver) is
determined by DetermineSleepTime() and it is a maximum of 5 seconds
(SIGKILL_CHILDREN_AFTER_SECS). One idea is to retry 2 or 3 times before give up
using the pg_stat_wal_receiver query. Do you have a better plan?

It's good to determine the threshold. It can define the number of acceptable
loss of walreceiver during the loop.
I think we should retry at least the postmaster revives the walreceiver.
The checking would work once per seconds, so more than 5 (or 10) may be better.
Thought?

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#138Shubham Khanna
khannashubham1197@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#135)
2 attachment(s)
Re: speed up a logical replica setup

On Thu, Feb 15, 2024 at 4:53 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear Euler,

Policy)

Basically, we do not prohibit to connect to primary/standby.
primary_slot_name may be changed during the conversion and
tuples may be inserted on target just after the promotion, but it seems no issues.

API)

-D (data directory) and -d (databases) are definitively needed.

Regarding the -P (a connection string for source), we can require it for now.
But note that it may cause an inconsistency if the pointed not by -P is different
from the node pointde by primary_conninfo.

As for the connection string for the target server, we can choose two ways:
a)
accept native connection string as -S. This can reuse the same parsing
mechanism as -P,
but there is a room that non-local server is specified.

b)
accept username/port as -U/-p
(Since the policy is like above, listen_addresses would not be overwritten. Also,
the port just specify the listening port).
This can avoid connecting to non-local, but more options may be needed.
(E.g., Is socket directory needed? What about password?)

Other discussing point, reported issue)

Points raised by me [1] are not solved yet.

* What if the target version is PG16-?
* What if the found executables have diffent version with pg_createsubscriber?
* What if the target is sending WAL to another server?
I.e., there are clusters like `node1->node2-.node3`, and the target is node2.
* Can we really cleanup the standby in case of failure?
Shouldn't we suggest to remove the target once?
* Can we move outputs to stdout?

Based on the discussion, I updated the patch set. Feel free to pick them and include.
Removing -P patch was removed, but removing -S still remained.

Also, while testing the patch set, I found some issues.

1.
Cfbot got angry [1]. This is because WIFEXITED and others are defined in <sys/wait.h>,
but the inclusion was removed per comment. Added the inclusion again.

2.
As Shubham pointed out [3], when we convert an intermediate node of cascading replication,
the last node would stuck. This is because a walreciever process requires nodes have the same
system identifier (in WalReceiverMain), but it would be changed by pg_createsubscriebr.

3.
Moreover, when we convert a last node of cascade, it won't work well. Because we cannot create
publications on the standby node.

4.
If the standby server was initialized as PG16-, this command would fail.
Because the API of pg_logical_create_replication_slot() were changed.

5.
Also, used pg_ctl commands must have same versions with the instance.
I think we should require all the executables and servers must be a same major version.

Based on them, below part describes attached ones:

V20-0001: same as Euler's patch, v17-0001.
V20-0002: Update docs per recent changes. Same as v19-0002
V20-0003: Modify the alignment of codes. Same as v19-0003
V20-0004: Change an argument of get_base_conninfo. Same as v19-0004
=== experimental patches ===
V20-0005: Add testcases. Same as v19-0004
V20-0006: Update a comment above global variables. Same as v19-0005
V20-0007: Address comments from Vignesh. Some parts you don't like
are reverted.
V20-0008: Fix error message in get_bin_directory(). Same as v19-0008
V20-0009: Remove -S option. Refactored from v16-0007
V20-0010: Add check versions of executables and the target, per above and [4]
V20-0011: Detect a disconnection while waiting the recovery, per [4]
V20-0012: Avoid running pg_createsubscriber for cascade physical replication, per above.

[1]: https://cirrus-ci.com/task/4619792833839104
[2]: /messages/by-id/CALDaNm1r9ZOwZamYsh6MHzb=_XvhjC_5XnTAsVecANvU9FOz6w@mail.gmail.com
[3]: /messages/by-id/CAHv8RjJcUY23ieJc5xqg6-QeGr1Ppp4Jwbu7Mq29eqCBTDWfUw@mail.gmail.com
[4]: /messages/by-id/TYCPR01MB1207713BEC5C379A05D65E342F54B2@TYCPR01MB12077.jpnprd01.prod.outlook.com

I found a couple of issues, while verifying the cascaded replication
with the following scenarios:
Scenario 1) Create cascade replication like node1->node2->node3
without using replication slots (attached
cascade_3node_setup_without_slots has the script for this):
Then I ran pg_createsubscriber by specifying primary as node1 and
standby as node3, this scenario runs successfully. I was not sure if
this should be supported or not?
Scenario 2) Create cascade replication like node1->node2->node3 using
replication slots (attached cascade_3node_setup_with_slots has the
script for this):
Here, slot name was used as slot1 for node1 to node2 and slot2 for
node2 to node3. Then I ran pg_createsubscriber by specifying primary
as node1 and standby as node3. In this case pg_createsubscriber fails
with the following error:
pg_createsubscriber: error: could not obtain replication slot
information: got 0 rows, expected 1 row
[Inferior 1 (process 2623483) exited with code 01]

This is failing because slot name slot2 is used between node2->node3
but pg_createsubscriber is checked for slot1, the slot which is used
for replication between node1->node2.
Thoughts?

Thanks and Regards,
Shubham Khanna.

Attachments:

cascade_3node_setup_without_slots.shtext/x-sh; charset=US-ASCII; name=cascade_3node_setup_without_slots.shDownload
cascade_3node_setup_with_slots.shtext/x-sh; charset=US-ASCII; name=cascade_3node_setup_with_slots.shDownload
#139Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Shubham Khanna (#138)
RE: speed up a logical replica setup

Dear Shubham,

Thanks for testing. It seems you ran with v20 patch, but I confirmed
It could reproduce with v21.

I found a couple of issues, while verifying the cascaded replication
with the following scenarios:
Scenario 1) Create cascade replication like node1->node2->node3
without using replication slots (attached
cascade_3node_setup_without_slots has the script for this):
Then I ran pg_createsubscriber by specifying primary as node1 and
standby as node3, this scenario runs successfully. I was not sure if
this should be supported or not?

Hmm. After the script, the cascading would be broken. The replication would be:

```
Node1 -> node2
|
Node3
```

And the operation is bit strange. The consistent LSN is gotten from the node1,
but node3 waits until it receives the record from NODE2.
Can we always success it?

Scenario 2) Create cascade replication like node1->node2->node3 using
replication slots (attached cascade_3node_setup_with_slots has the
script for this):
Here, slot name was used as slot1 for node1 to node2 and slot2 for
node2 to node3. Then I ran pg_createsubscriber by specifying primary
as node1 and standby as node3. In this case pg_createsubscriber fails
with the following error:
pg_createsubscriber: error: could not obtain replication slot
information: got 0 rows, expected 1 row
[Inferior 1 (process 2623483) exited with code 01]

This is failing because slot name slot2 is used between node2->node3
but pg_createsubscriber is checked for slot1, the slot which is used
for replication between node1->node2.
Thoughts?

Right. The inconsistency is quite strange.

Overall, I felt such a case must be rejected. How should we detect at checking phase?

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#140Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#136)
RE: speed up a logical replica setup

Dear Euler,

Here are comments for v21.

01. main
```
/* rudimentary check for a data directory. */
...
/* subscriber PID file. */
```

Initial char must be upper, and period is not needed.

02. check_data_directory
```
snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
```

You removed the version checking from PG_VERSION, but I think it is still needed.
Indeed v21 can detect the pg_ctl/pg_resetwal/pg_createsubscriber has different
verson, but this cannot ditect the started instance has the differnet version.
I.e., v20-0010 is partially needed.

03. store_pub_sub_info()
```
SimpleStringListCell *cell;
```

This definition can be in loop variable.

04. get_standby_sysid()
```
pfree(cf);
```

This can be pg_pfree().

05. check_subscriber
```
/* The target server must be a standby */
if (server_is_in_recovery(conn) == 0)
{
pg_log_error("The target server is not a standby");
return false;
}
```

What if the the function returns -1? Should we ditect (maybe the disconnection) here?

06. server_is_in_recovery
```
ret = strcmp("t", PQgetvalue(res, 0, 0));

PQclear(res);

if (ret == 0)
return 1;
else if (ret > 0)
return 0;
else
return -1; /* should not happen */
```

But strcmp may return a negative value, right? Based on the comment atop function,
we should not do it. I think we can use ternary operator instead.

07. server_is_in_recovery

As the fisrt place, no one consider this returns -1. So can we change the bool
function and raise pg_fatal() in case of the error?

08. check_subscriber
```
if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
{
pg_log_error("permission denied for function \"%s\"",
"pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
return false;
}
```

I think the third argument must be 2.

09. check_subscriber
```
pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
```

The output seems strange if the primary_slot_name is not set.

10. setup_publisher()
```
PGconn *conn;
PGresult *res;
```

Definitions can be in the loop.

11. create_publication()
```
if (PQntuples(res) == 1)
{
/*
* If publication name already exists and puballtables is true, let's
* use it. A previous run of pg_createsubscriber must have created
* this publication. Bail out.
*/
```

Hmm, but pre-existing publications may not send INSERT/UPDATE/DELETE/TRUNCATE.
They should be checked if we really want to reuse.
(I think it is OK to just raise ERROR)

12. create_publication()

Based on above, we do not have to check before creating publicatios. The publisher
can detect the duplication. I prefer it.

13. create_logical_replication_slot()
```
if (PQresultStatus(res) != PGRES_TUPLES_OK)
{
pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
slot_name, dbinfo->dbname,
PQresultErrorMessage(res));
return lsn;
}
```

I know lsn is always NULL, but can we use `return NULL`?

14. setup_subscriber()
```
PGconn *conn;

```

This definition can be in the loop.

15.

You said in case of failure, cleanups is not needed if the process exits soon [1]/messages/by-id/89ccf72b-59f9-4317-b9fd-1e6d20a0c3b1@app.fastmail.com.
But some functions call PQfinish() then exit(1) or pg_fatal(). Should we follow?

16.

Some places refer PGresult or PGConn even after the cleanup. They must be fixed.
```
PQclear(res);
disconnect_database(conn);
pg_fatal("could not get system identifier: %s",
PQresultErrorMessage(res));
```

I think this is a root cause why sometimes the wrong error message has output.

17.

Some places call PQerrorMessage() and other places call PQresultErrorMessage().
I think it PQerrorMessage() should be used only after the connection establishment
functions. Thought?

18. 041_pg_createsubscriber_standby.pl
```
use warnings;
```

We must set "FATAL = all";

19.
```
my $node_p;
my $node_f;
my $node_s;
my $node_c;
my $result;
my $slotname;
```

I could not find forward declarations in perl file.
The node name might be bit a consuging, but I could not find better name.

20.
```
# On node P
# - create databases
# - create test tables
# - insert a row
# - create a physical replication slot
$node_p->safe_psql(
'postgres', q(
CREATE DATABASE pg1;
CREATE DATABASE pg2;
));
$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
my $slotname = 'physical_slot';
$node_p->safe_psql('pg2',
"SELECT pg_create_physical_replication_slot('$slotname')");
```

I think setting of the same node can be gathered into one place.
Also, any settings and definitions should be done just before they are used.

21.
```
$node_s->append_conf(
'postgresql.conf', qq[
log_min_messages = debug2
primary_slot_name = '$slotname'
]);
```

I could not find a reason why we set to debug2.

22.
```
command_fails
```

command_checks_all() can check returned value and outputs.
Should we use it?

23.

Can you add headers for each testcases? E.g.,

```
# ------------------------------
# Check pg_createsubscriber fails when the target server is not a
# standby of the source.
...
# ------------------------------
# Check pg_createsubscriber fails when the target server is not running
...
# ------------------------------
# Check pg_createsubscriber fails when the target server is a member of
# the cascading standby.
...
# ------------------------------
# Check successful dry-run
...
# ------------------------------
# Check successful conversion
```

24.
```
# Stop node C
$node_c->teardown_node;
...
$node_p->stop;
$node_s->stop;
$node_f->stop;
```
Why you choose the teardown?

25.

The creation of subscriptions are not directory tested. @subnames contains the
name of subscriptions, but it just assumes the number of them is more than two.

Since it may be useful, I will post top-up patch on Monday, if there are no updating.

[1]: /messages/by-id/89ccf72b-59f9-4317-b9fd-1e6d20a0c3b1@app.fastmail.com
[2]: /messages/by-id/TYCPR01MB1207713BEC5C379A05D65E342F54B2@TYCPR01MB12077.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#141Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#140)
11 attachment(s)
RE: speed up a logical replica setup

Dear hackers,

Since it may be useful, I will post top-up patch on Monday, if there are no
updating.

And here are top-up patches. Feel free to check and include.

v22-0001: Same as v21-0001.
=== rebased patches ===
v22-0002: Update docs per recent changes. Same as v20-0002.
v22-0003: Add check versions of the target. Extracted from v20-0003.
v22-0004: Remove -S option. Mostly same as v20-0009, but commit massage was
slightly changed.
=== Newbie ===
V22-0005: Addressed my comments which seems to be trivial[1]/messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com.
Comments #1, 3, 4, 8, 10, 14, 17 were addressed here.
v22-0006: Consider the scenario when commands are failed after the recovery.
drop_subscription() is removed and some messages are added per [2]/messages/by-id/TYCPR01MB12077E98F930C3DE6BD304D0DF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com.
V22-0007: Revise server_is_in_recovery() per [1]/messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com. Comments #5, 6, 7, were addressed here.
V22-0008: Fix a strange report when physical_primary_slot is null. Per comment #9 [1]/messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com.
V22-0009: Prohibit reuse publications when it has already existed. Per comments #11 and 12 [1]/messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com.
V22-0010: Avoid to call PQclear()/PQfinish()/pg_free() if the process exits soon. Per comment #15 [1]/messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com.
V22-0011: Update testcode. Per comments #17- [1]/messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com.

I did not handle below points because I have unclear points.

a.
This patch set cannot detect the disconnection between the target (standby) and
source (primary) during the catch up. Because the connection status must be gotten
at the same time (=in the same query) with the recovery status, but now it is now an
independed function (server_is_in_recovery()).

b.
This patch set cannot detect the inconsistency reported by Shubham [3]/messages/by-id/CAHv8Rj+5mzK9Jt+7ECogJzfm5czvDCCd5jO1_rCx0bTEYpBE5g@mail.gmail.com. I could not
come up with solutions without removing -P...

[1]: /messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com
[2]: /messages/by-id/TYCPR01MB12077E98F930C3DE6BD304D0DF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com
[3]: /messages/by-id/CAHv8Rj+5mzK9Jt+7ECogJzfm5czvDCCd5jO1_rCx0bTEYpBE5g@mail.gmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

v22-0006-Fix-cleanup-functions.patchapplication/octet-stream; name=v22-0006-Fix-cleanup-functions.patchDownload
From 10985a2dc754c915bd51c5ca2e6da71bd36ef9a5 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 16 Feb 2024 07:34:41 +0000
Subject: [PATCH v22 06/11] Fix cleanup functions

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 60 +++------------------
 1 file changed, 8 insertions(+), 52 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 968d0ae6bd..252d541472 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -54,7 +54,6 @@ typedef struct LogicalRepInfo
 
 	bool		made_replslot;	/* replication slot was created */
 	bool		made_publication;	/* publication was created */
-	bool		made_subscription;	/* subscription was created */
 } LogicalRepInfo;
 
 static void cleanup_objects_atexit(void);
@@ -95,7 +94,6 @@ static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo,
 									 const char *lsn);
 static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
@@ -141,22 +139,11 @@ cleanup_objects_atexit(void)
 
 	for (i = 0; i < num_dbs; i++)
 	{
-		if (dbinfo[i].made_subscription || recovery_ended)
+		if (recovery_ended)
 		{
-			conn = connect_database(dbinfo[i].subconninfo);
-			if (conn != NULL)
-			{
-				if (dbinfo[i].made_subscription)
-					drop_subscription(conn, &dbinfo[i]);
-
-				/*
-				 * Publications are created on publisher before promotion so
-				 * it might exist on subscriber after recovery ends.
-				 */
-				if (recovery_ended)
-					drop_publication(conn, &dbinfo[i]);
-				disconnect_database(conn);
-			}
+			pg_log_warning("pg_createsubscriber failed after the end of recovery");
+			pg_log_warning("Target server could not be usable as physical standby anymore.");
+			pg_log_warning_hint("You must re-create the physical standby again.");
 		}
 
 		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
@@ -404,7 +391,6 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
 		/* Fill subscriber attributes */
 		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
 		dbinfo[i].subconninfo = conninfo;
-		dbinfo[i].made_subscription = false;
 		/* Other fields will be filled later */
 
 		i++;
@@ -1430,46 +1416,12 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		}
 	}
 
-	/* for cleanup purposes */
-	dbinfo->made_subscription = true;
-
 	if (!dry_run)
 		PQclear(res);
 
 	destroyPQExpBuffer(str);
 }
 
-/*
- * Remove subscription if it couldn't finish all steps.
- */
-static void
-drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
-{
-	PQExpBuffer str = createPQExpBuffer();
-	PGresult   *res;
-
-	Assert(conn != NULL);
-
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
-				dbinfo->subname, dbinfo->dbname);
-
-	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
-
-	pg_log_debug("command is: %s", str->data);
-
-	if (!dry_run)
-	{
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
-
-		PQclear(res);
-	}
-
-	destroyPQExpBuffer(str);
-}
-
 /*
  * Sets the replication progress to the consistent LSN.
  *
@@ -1986,6 +1938,10 @@ main(int argc, char **argv)
 	/* Waiting the subscriber to be promoted */
 	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
 
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must re-create the new physical standby before continuing.");
+
 	/*
 	 * Create the subscription for each database on subscriber. It does not
 	 * enable it immediately because it needs to adjust the logical
-- 
2.43.0

v22-0007-Fix-server_is_in_recovery.patchapplication/octet-stream; name=v22-0007-Fix-server_is_in_recovery.patchDownload
From 9243b50eb2480e6b04b2ac2adfc51e86378203f8 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:12:32 +0000
Subject: [PATCH v22 07/11] Fix server_is_in_recovery

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 25 +++++++--------------
 1 file changed, 8 insertions(+), 17 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 252d541472..ea4eb7e621 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -73,7 +73,7 @@ static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
 static void modify_subscriber_sysid(const char *pg_resetwal_path,
 									CreateSubscriberOptions *opt);
-static int	server_is_in_recovery(PGconn *conn);
+static bool	server_is_in_recovery(PGconn *conn);
 static bool check_publisher(LogicalRepInfo *dbinfo);
 static bool setup_publisher(LogicalRepInfo *dbinfo);
 static bool check_subscriber(LogicalRepInfo *dbinfo);
@@ -651,7 +651,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
  * If the answer is yes, it returns 1, otherwise, returns 0. If an error occurs
  * while executing the query, it returns -1.
  */
-static int
+static bool
 server_is_in_recovery(PGconn *conn)
 {
 	PGresult   *res;
@@ -660,22 +660,13 @@ server_is_in_recovery(PGconn *conn)
 	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		PQclear(res);
-		pg_log_error("could not obtain recovery progress");
-		return -1;
-	}
+		pg_fatal("could not obtain recovery progress");
 
 	ret = strcmp("t", PQgetvalue(res, 0, 0));
 
 	PQclear(res);
 
-	if (ret == 0)
-		return 1;
-	else if (ret > 0)
-		return 0;
-	else
-		return -1;				/* should not happen */
+	return ret == 0;
 }
 
 /*
@@ -704,7 +695,7 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * If the primary server is in recovery (i.e. cascading replication),
 	 * objects (publication) cannot be created because it is read only.
 	 */
-	if (server_is_in_recovery(conn) == 1)
+	if (server_is_in_recovery(conn))
 		pg_fatal("primary server cannot be in recovery");
 
 	/*------------------------------------------------------------------------
@@ -845,7 +836,7 @@ check_subscriber(LogicalRepInfo *dbinfo)
 		exit(1);
 
 	/* The target server must be a standby */
-	if (server_is_in_recovery(conn) == 0)
+	if (!server_is_in_recovery(conn))
 	{
 		pg_log_error("The target server is not a standby");
 		return false;
@@ -1223,7 +1214,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 
 	for (;;)
 	{
-		int			in_recovery;
+		bool			in_recovery;
 
 		in_recovery = server_is_in_recovery(conn);
 
@@ -1231,7 +1222,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 		 * Does the recovery process finish? In dry run mode, there is no
 		 * recovery mode. Bail out as the recovery process has ended.
 		 */
-		if (in_recovery == 0 || dry_run)
+		if (!in_recovery || dry_run)
 		{
 			status = POSTMASTER_READY;
 			recovery_ended = true;
-- 
2.43.0

v22-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchapplication/octet-stream; name=v22-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchDownload
From 8b7257a01cf0e86453cd8e3594160946c920c66d Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v22 01/11] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica
into a logical replica. It runs on the target server and should be able
to connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.  Stop the
target server. Change the system identifier from the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  320 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1972 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   39 +
 .../t/041_pg_createsubscriber_standby.pl      |  217 ++
 src/tools/pgindent/typedefs.list              |    2 +
 10 files changed, 2579 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f5238771b7
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,320 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..205a835d36
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1972 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+/* Command-line options */
+typedef struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *sub_conninfo_str;	/* subscriber connection string */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+} CreateSubscriberOptions;
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name / replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_exec_path(const char *argv0, const char *progname);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+										  const char *pub_base_conninfo,
+										  const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path,
+									CreateSubscriberOptions *opt);
+static int	server_is_in_recovery(PGconn *conn);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn,
+											 LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void start_standby_server(const char *pg_ctl_path, const char *datadir,
+								 const char *logfile);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+								  CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static bool recovery_ended = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription || recovery_ended)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_subscription)
+					drop_subscription(conn, &dbinfo[i]);
+
+				/*
+				 * Publications are created on publisher before promotion so
+				 * it might exist on subscriber after recovery ends.
+				 */
+				if (recovery_ended)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir,
+						 strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory",
+					 datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		dbinfo[i].made_subscription = false;
+		/* Other fields will be filled later */
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: %s",
+				 PQresultErrorMessage(res));
+	}
+	if (PQntuples(res) != 1)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: got %d rows, expected %d row",
+				 PQntuples(res), 1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is recovery still in progress?
+ * If the answer is yes, it returns 1, otherwise, returns 0. If an error occurs
+ * while executing the query, it returns -1.
+ */
+static int
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		pg_log_error("could not obtain recovery progress");
+		return -1;
+	}
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	if (ret == 0)
+		return 1;
+	else if (ret > 0)
+		return 0;
+	else
+		return -1;				/* should not happen */
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn) == 1)
+		pg_fatal("primary server cannot be in recovery");
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/* The target server must be a standby */
+	if (server_is_in_recovery(conn) == 0)
+	{
+		pg_log_error("The target server is not a standby");
+		return false;
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		return false;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/* This temporary replication slot is only used for catchup purposes */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* Cleanup if there is any failure */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it is
+ * run.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* Append timestamp with ISO 8601 format */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir,
+				   PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_ctl_path, const char *datadir,
+					 const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"",
+						  pg_ctl_path, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+					  CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		int			in_recovery;
+
+		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (in_recovery == 0 || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			pg_fatal("recovery timed out");
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain publication information: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
+				 PQntuples(res), 1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not enable subscription \"%s\": %s",
+					 dbinfo->subname, PQerrorMessage(conn));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_ctl_path = NULL;
+	char	   *pg_resetwal_path = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.sub_conninfo_str = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(opt.subscriber_dir);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				opt.sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(opt.subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Create the output directory to store any data generated by this tool */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/* Check if the standby server is ready for logical replication */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		if (!dry_run)
+			stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0], true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/* Start subscriber and wait until accepting connections */
+	pg_log_info("starting the subscriber");
+	if (!dry_run)
+		start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary",
+						   primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	if (!dry_run)
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(pg_resetwal_path, &opt);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..95eb4e70ac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,39 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..e2807d3fac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,217 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $node_c;
+my $result;
+my $slotname;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster.
+{
+	local $ENV{'INITDB_TEMPLATE'} = undef;
+
+	$node_f = PostgreSQL::Test::Cluster->new('node_f');
+	$node_f->init(allows_streaming => 'logical');
+	$node_f->start;
+}
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+$slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+log_min_messages = debug2
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Run pg_createsubscriber on the stopped node
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'target server must be running');
+
+$node_s->start;
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+$node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->append_conf(
+	'postgresql.conf', qq[
+log_min_messages = debug2
+]);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'), '--subscriber-server',
+		$node_c->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'primary server is in recovery');
+
+# Stop node C
+$node_c->teardown_node;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1')
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+ok( -d $node_s->data_dir . "/pg_createsubscriber_output.d",
+	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success"
+);
+
+# Confirm the physical replication slot has been removed
+$result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d808aad8b0..08de2bf4e6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.43.0

v22-0002-Update-documentation.patchapplication/octet-stream; name=v22-0002-Update-documentation.patchDownload
From 0411bbf0cfe4bec6e4905421717fa097d055ce89 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Tue, 13 Feb 2024 10:59:47 +0000
Subject: [PATCH v22 02/11] Update documentation

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 205 +++++++++++++++-------
 1 file changed, 142 insertions(+), 63 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index f5238771b7..7cdd047d67 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -48,19 +48,99 @@ PostgreSQL documentation
   </cmdsynopsis>
  </refsynopsisdiv>
 
- <refsect1>
+ <refsect1 id="r1-app-pg_createsubscriber-1">
   <title>Description</title>
   <para>
-    <application>pg_createsubscriber</application> creates a new logical
-    replica from a physical standby server.
+   The <application>pg_createsubscriber</application> creates a new <link
+   linkend="logical-replication-subscription">subscriber</link> from a physical
+   standby server.
   </para>
 
   <para>
-   The <application>pg_createsubscriber</application> should be run at the target
-   server. The source server (known as publisher server) should accept logical
-   replication connections from the target server (known as subscriber server).
-   The target server should accept local logical replication connection.
+   The <application>pg_createsubscriber</application> must be run at the target
+   server. The source server (known as publisher server) must accept both
+   normal and logical replication connections from the target server (known as
+   subscriber server). The target server must accept normal local connections.
   </para>
+
+  <para>
+   There are some prerequisites for both the source and target instance. If
+   these are not met an error will be reported.
+  </para>
+
+  <itemizedlist>
+   <listitem>
+    <para>
+     The given target data directory must have the same system identifier than the
+     source data directory.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must be used as a physical standby.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The given database user for the target instance must have privileges for
+     creating subscriptions and using functions for replication origin.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     and <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+     configured to a value greater than the number of target databases.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The source instance must have
+     <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+     <literal>logical</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases and replication slots.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases and walsenders.
+    </para>
+   </listitem>
+  </itemizedlist>
+
+  <note>
+   <para>
+    After the successful conversion, a physical replication slot configured as
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>
+    would be removed from a primary instance.
+   </para>
+
+   <para>
+    The <application>pg_createsubscriber</application> focuses on large-scale
+    systems that contain more data than 1GB.  For smaller systems, initial data
+    synchronization of <link linkend="logical-replication">logical
+    replication</link> is recommended.
+   </para>
+  </note>
  </refsect1>
 
  <refsect1>
@@ -191,7 +271,7 @@ PostgreSQL documentation
  </refsect1>
 
  <refsect1>
-  <title>Notes</title>
+  <title>How It Works</title>
 
   <para>
    The transformation proceeds in the following steps:
@@ -200,97 +280,89 @@ PostgreSQL documentation
   <procedure>
    <step>
     <para>
-     <application>pg_createsubscriber</application> checks if the given target data
-     directory has the same system identifier than the source data directory.
-     Since it uses the recovery process as one of the steps, it starts the
-     target server as a replica from the source server. If the system
-     identifier is not the same, <application>pg_createsubscriber</application> will
-     terminate with an error.
+     Checks the target can be converted.  In particular, things listed in
+     <link linkend="r1-app-pg_createsubscriber-1">above section</link> would be
+     checked.  If these are not met <application>pg_createsubscriber</application>
+     will terminate with an error.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> checks if the target data
-     directory is used by a physical replica. Stop the physical replica if it is
-     running. One of the next steps is to add some recovery parameters that
-     requires a server start. This step avoids an error.
+     Creates a publication and a logical replication slot for each specified
+     database on the source instance.  These publications and logical replication
+     slots have generated names:
+     <quote><literal>pg_createsubscriber_%u</literal></quote> (parameters:
+     Database <parameter>oid</parameter>) for publications,
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     Database <parameter>oid</parameter>, Pid <parameter>int</parameter>) for
+     replication slots.
     </para>
    </step>
-
    <step>
     <para>
-     <application>pg_createsubscriber</application> creates one replication slot for
-     each specified database on the source server. The replication slot name
-     contains a <literal>pg_createsubscriber</literal> prefix. These replication
-     slots will be used by the subscriptions in a future step.  A temporary
-     replication slot is used to get a consistent start location. This
-     consistent LSN will be used as a stopping point in the <xref
-     linkend="guc-recovery-target-lsn"/> parameter and by the
-     subscriptions as a replication starting point. It guarantees that no
-     transaction will be lost.
+     Stops the target instance.  This is needed to add some recovery parameters
+     during the conversion.
     </para>
    </step>
-
    <step>
     <para>
-     <application>pg_createsubscriber</application> writes recovery parameters into
-     the target data directory and start the target server. It specifies a LSN
-     (consistent LSN that was obtained in the previous step) of write-ahead
-     log location up to which recovery will proceed. It also specifies
-     <literal>promote</literal> as the action that the server should take once
-     the recovery target is reached. This step finishes once the server ends
-     standby mode and is accepting read-write operations.
+     Creates a temporary replication slot to get a consistent start location.
+     The slot has generated names:
+     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
+     (parameters: Pid <parameter>int</parameter>).  Got consistent LSN will be
+     used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
+     parameter and by the subscriptions as a replication starting point. It
+     guarantees that no transaction will be lost.
+    </para>
+   </step>
+   <step>
+    <para>
+     Writes recovery parameters into the target data directory and starts the
+     target instance.  It specifies a LSN (consistent LSN that was obtained in
+     the previous step) of write-ahead log location up to which recovery will
+     proceed. It also specifies <literal>promote</literal> as the action that
+     the server should take once the recovery target is reached. This step
+     finishes once the server ends standby mode and is accepting read-write
+     operations.
     </para>
    </step>
 
    <step>
     <para>
-     Next, <application>pg_createsubscriber</application> creates one publication
-     for each specified database on the source server. Each publication
-     replicates changes for all tables in the database. The publication name
-     contains a <literal>pg_createsubscriber</literal> prefix. These publication
-     will be used by a corresponding subscription in a next step.
+     Creates a subscription for each specified database on the target instance.
+     These subscriptions have generated name:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     Database <parameter>oid</parameter>, Pid <parameter>int</parameter>).
+     These subscription have same subscription options:
+     <quote><literal>create_slot = false, copy_data = false, enabled = false</literal></quote>.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> creates one subscription for
-     each specified database on the target server. Each subscription name
-     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
-     name is identical to the subscription name. It does not copy existing data
-     from the source server. It does not create a replication slot. Instead, it
-     uses the replication slot that was created in a previous step. The
-     subscription is created but it is not enabled yet. The reason is the
-     replication progress must be set to the consistent LSN but replication
-     origin name contains the subscription oid in its name. Hence, the
-     subscription will be enabled in a separate step.
+     Sets replication progress to the consistent LSN that was obtained in a
+     previous step.  This is the exact LSN to be used as a initial location for
+     each subscription.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> sets the replication progress to
-     the consistent LSN that was obtained in a previous step. When the target
-     server started the recovery process, it caught up to the consistent LSN.
-     This is the exact LSN to be used as a initial location for each
-     subscription.
+     Enables the subscription for each specified database on the target server.
+     The subscription starts streaming from the consistent LSN.
     </para>
    </step>
 
    <step>
     <para>
-     Finally, <application>pg_createsubscriber</application> enables the subscription
-     for each specified database on the target server. The subscription starts
-     streaming from the consistent LSN.
+     Stops the standby server.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> stops the target server to change
-     its system identifier.
+     Updates a system identifier on the target server.
     </para>
    </step>
   </procedure>
@@ -300,8 +372,15 @@ PostgreSQL documentation
   <title>Examples</title>
 
   <para>
-   To create a logical replica for databases <literal>hr</literal> and
-   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+   Here is an example of using <application>pg_createsubscriber</application>.
+   Before running the command, please make sure target server is stopped.
+<screen>
+<prompt>$</prompt> <userinput>pg_ctl -D /usr/local/pgsql/data stop</userinput>
+</screen>
+
+   Then run <application>pg_createsubscriber</application>. Below tries to
+   create subscriptions for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical standby:
 <screen>
 <prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
 </screen>
-- 
2.43.0

v22-0003-Add-version-check-for-standby-server.patchapplication/octet-stream; name=v22-0003-Add-version-check-for-standby-server.patchDownload
From d126dc9cada9ce1e66e19373f581e476d8eace54 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 14 Feb 2024 16:27:15 +0530
Subject: [PATCH v22 03/11] Add version check for standby server

Add version check for standby server
---
 doc/src/sgml/ref/pg_createsubscriber.sgml   |  6 +++++
 src/bin/pg_basebackup/pg_createsubscriber.c | 28 +++++++++++++++++++++
 2 files changed, 34 insertions(+)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 7cdd047d67..9d0c6c764c 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -125,6 +125,12 @@ PostgreSQL documentation
      databases and walsenders.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     Both the target and source instances must have same major versions with
+     <application>pg_createsubscriber</application>.
+    </para>
+   </listitem>
   </itemizedlist>
 
   <note>
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 205a835d36..b15769c75b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -23,6 +23,7 @@
 #include "common/file_perm.h"
 #include "common/logging.h"
 #include "common/restricted_token.h"
+#include "common/string.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
@@ -294,6 +295,8 @@ check_data_directory(const char *datadir)
 {
 	struct stat statbuf;
 	char		versionfile[MAXPGPATH];
+	FILE	   *ver_fd;
+	char		rawline[64];
 
 	pg_log_info("checking if directory \"%s\" is a cluster data directory",
 				datadir);
@@ -317,6 +320,31 @@ check_data_directory(const char *datadir)
 		return false;
 	}
 
+	/* Check standby server version */
+	if ((ver_fd = fopen(versionfile, "r")) == NULL)
+		pg_fatal("could not open file \"%s\" for reading: %m", versionfile);
+
+	/* Version number has to be the first line read */
+	if (!fgets(rawline, sizeof(rawline), ver_fd))
+	{
+		if (!ferror(ver_fd))
+			pg_fatal("unexpected empty file \"%s\"", versionfile);
+		else
+			pg_fatal("could not read file \"%s\": %m", versionfile);
+	}
+
+	/* Strip trailing newline and carriage return */
+	(void) pg_strip_crlf(rawline);
+
+	if (strcmp(rawline, PG_MAJORVERSION) != 0)
+	{
+		pg_log_error("standby server is of wrong version");
+		pg_log_error_detail("File \"%s\" contains \"%s\", which is not compatible with this program's version \"%s\".",
+							versionfile, rawline, PG_MAJORVERSION);
+		exit(1);
+	}
+
+	fclose(ver_fd);
 	return true;
 }
 
-- 
2.43.0

v22-0004-Remove-S-option-to-force-unix-domain-connection.patchapplication/octet-stream; name=v22-0004-Remove-S-option-to-force-unix-domain-connection.patchDownload
From a73ba34168a6d51ae392daee4f5847863fa6317d Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 6 Feb 2024 14:45:03 +0530
Subject: [PATCH v22 04/11] Remove -S option to force unix domain connection

With this patch removed -S option and added option for username(-U), port(-p)
and socket directory(-s) for standby. This helps to force standby to use
unix domain connection.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     | 36 ++++++--
 src/bin/pg_basebackup/pg_createsubscriber.c   | 91 ++++++++++++++-----
 .../t/041_pg_createsubscriber_standby.pl      | 33 ++++---
 3 files changed, 115 insertions(+), 45 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 9d0c6c764c..579e50a0a0 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -34,11 +34,6 @@ PostgreSQL documentation
      <arg choice="plain"><option>--publisher-server</option></arg>
     </group>
     <replaceable>connstr</replaceable>
-    <group choice="req">
-     <arg choice="plain"><option>-S</option></arg>
-     <arg choice="plain"><option>--subscriber-server</option></arg>
-    </group>
-    <replaceable>connstr</replaceable>
     <group choice="req">
      <arg choice="plain"><option>-d</option></arg>
      <arg choice="plain"><option>--database</option></arg>
@@ -179,11 +174,36 @@ PostgreSQL documentation
      </varlistentry>
 
      <varlistentry>
-      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
-      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        A port number on which the target server is listening for connections.
+        Defaults to the <envar>PGPORT</envar> environment variable, if set, or
+        a compiled-in default.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable>username</replaceable></option></term>
+      <term><option>--username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        Target's user name. Defaults to the <envar>PGUSER</envar> environment
+        variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s</option> <replaceable>dir</replaceable></term>
+      <term><option>--socketdir=</option><replaceable>dir</replaceable></term>
       <listitem>
        <para>
-        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+        A directory which locales a temporary Unix socket files. If not
+        specified, <application>pg_createsubscriber</application> tries to
+        connect via TCP/IP to <literal>localhost</literal>.
        </para>
       </listitem>
      </varlistentry>
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index b15769c75b..1ad7de9190 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -35,7 +35,9 @@ typedef struct CreateSubscriberOptions
 {
 	char	   *subscriber_dir; /* standby/subscriber data directory */
 	char	   *pub_conninfo_str;	/* publisher connection string */
-	char	   *sub_conninfo_str;	/* subscriber connection string */
+	unsigned short subport;			/* port number listen()'d by the standby */
+	char	   *subuser;			/* database user of the standby */
+	char	   *socketdir;			/* socket directory */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;			/* retain log file? */
 	int			recovery_timeout;	/* stop recovery after this time */
@@ -57,7 +59,9 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_pub_base_conninfo(char *conninfo, char **dbname);
+static char *construct_sub_conninfo(char *username, unsigned short subport,
+									char *socketdir);
 static char *get_exec_path(const char *argv0, const char *progname);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
@@ -180,7 +184,10 @@ usage(void)
 	printf(_("\nOptions:\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
 	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
-	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -p, --port=PORT                     subscriber port number\n"));
+	printf(_(" -U, --username=NAME                 subscriber user\n"));
+	printf(_(" -s, --socketdir=DIR                 socket directory to use\n"));
+	printf(_("                                     If not specified, localhost would be used\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
 	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
@@ -193,8 +200,8 @@ usage(void)
 }
 
 /*
- * Validate a connection string. Returns a base connection string that is a
- * connection string without a database name.
+ * Validate a connection string for the publisher. Returns a base connection
+ * string that is a connection string without a database name.
  *
  * Since we might process multiple databases, each database name will be
  * appended to this base connection string to provide a final connection
@@ -206,7 +213,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char **dbname)
+get_pub_base_conninfo(char *conninfo, char **dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -1593,6 +1600,40 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	destroyPQExpBuffer(str);
 }
 
+/*
+ * Construct a connection string toward a target server, from argument options.
+ *
+ * If inputs are the zero, default value would be used.
+ * - username: PGUSER environment value (it would not be parsed)
+ * - port: PGPORT environment value (it would not be parsed)
+ * - socketdir: localhost connection (unix-domain would not be used)
+ */
+static char *
+construct_sub_conninfo(char *username, unsigned short subport, char *sockdir)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	if (username)
+		appendPQExpBuffer(buf, "user=%s ", username);
+
+	if (subport != 0)
+		appendPQExpBuffer(buf, "port=%u ", subport);
+
+	if (sockdir)
+		appendPQExpBuffer(buf, "host=%s ", sockdir);
+	else
+		appendPQExpBuffer(buf, "host=localhost ");
+
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
 int
 main(int argc, char **argv)
 {
@@ -1602,7 +1643,9 @@ main(int argc, char **argv)
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
 		{"publisher-server", required_argument, NULL, 'P'},
-		{"subscriber-server", required_argument, NULL, 'S'},
+		{"port", required_argument, NULL, 'p'},
+		{"username", required_argument, NULL, 'U'},
+		{"socketdir", required_argument, NULL, 's'},
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
 		{"recovery-timeout", required_argument, NULL, 't'},
@@ -1659,7 +1702,9 @@ main(int argc, char **argv)
 	/* Default settings */
 	opt.subscriber_dir = NULL;
 	opt.pub_conninfo_str = NULL;
-	opt.sub_conninfo_str = NULL;
+	opt.subport = 0;
+	opt.subuser = NULL;
+	opt.socketdir = NULL;
 	opt.database_names = (SimpleStringList)
 	{
 		NULL, NULL
@@ -1683,7 +1728,7 @@ main(int argc, char **argv)
 
 	get_restricted_token();
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+	while ((c = getopt_long(argc, argv, "D:P:p:U:s:S:d:nrt:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1695,8 +1740,17 @@ main(int argc, char **argv)
 			case 'P':
 				opt.pub_conninfo_str = pg_strdup(optarg);
 				break;
-			case 'S':
-				opt.sub_conninfo_str = pg_strdup(optarg);
+			case 'p':
+				if ((opt.subport = atoi(optarg)) <= 0)
+					pg_fatal("invalid old port number");
+				break;
+			case 'U':
+				pfree(opt.subuser);
+				opt.subuser = pg_strdup(optarg);
+				break;
+			case 's':
+				pfree(opt.socketdir);
+				opt.socketdir = pg_strdup(optarg);
 				break;
 			case 'd':
 				/* Ignore duplicated database names */
@@ -1763,21 +1817,12 @@ main(int argc, char **argv)
 		exit(1);
 	}
 	pg_log_info("validating connection string on publisher");
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
-										  &dbname_conninfo);
+	pub_base_conninfo = get_pub_base_conninfo(opt.pub_conninfo_str,
+											  &dbname_conninfo);
 	if (pub_base_conninfo == NULL)
 		exit(1);
 
-	if (opt.sub_conninfo_str == NULL)
-	{
-		pg_log_error("no subscriber connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	pg_log_info("validating connection string on subscriber");
-	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
-	if (sub_base_conninfo == NULL)
-		exit(1);
+	sub_base_conninfo = construct_sub_conninfo(opt.subuser, opt.subport, opt.socketdir);
 
 	if (opt.database_names.head == NULL)
 	{
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index e2807d3fac..93148417db 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -66,7 +66,8 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
 		'--publisher-server', $node_p->connstr('pg1'),
-		'--subscriber-server', $node_f->connstr('pg1'),
+		'--port', $node_f->port,
+		'--host', $node_f->host,
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
@@ -78,8 +79,9 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1'), '--database',
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--host',
+		$node_s->host, '--database',
 		'pg1', '--database',
 		'pg2'
 	],
@@ -104,10 +106,11 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_c->data_dir, '--publisher-server',
-		$node_s->connstr('pg1'), '--subscriber-server',
-		$node_c->connstr('pg1'), '--database',
-		'pg1', '--database',
-		'pg2'
+		$node_s->connstr('pg1'),
+		'--port', $node_c->port,
+		'--socketdir', $node_c->host,
+		'--database', 'pg1',
+		'--database', 'pg2'
 	],
 	'primary server is in recovery');
 
@@ -124,8 +127,9 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1'), '--database',
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--socketdir',
+		$node_s->host, '--database',
 		'pg1', '--database',
 		'pg2'
 	],
@@ -141,8 +145,9 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1')
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--socketdir',
+		$node_s->host,
 	],
 	'run pg_createsubscriber without --databases');
 
@@ -152,9 +157,9 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--verbose', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1'), '--database',
-		'pg1', '--database',
+		$node_p->connstr('pg1'), '--port', $node_s->port,
+		'--socketdir', $node_s->host,
+		'--database', 'pg1', '--database',
 		'pg2'
 	],
 	'run pg_createsubscriber on node S');
-- 
2.43.0

v22-0005-Fix-some-trivial-issues.patchapplication/octet-stream; name=v22-0005-Fix-some-trivial-issues.patchDownload
From e286505af6547077a276555cf3f98d84008ef97c Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 03:59:19 +0000
Subject: [PATCH v22 05/11] Fix some trivial issues

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 44 ++++++++++-----------
 1 file changed, 20 insertions(+), 24 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1ad7de9190..968d0ae6bd 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -387,12 +387,11 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
 				   const char *sub_base_conninfo)
 {
 	LogicalRepInfo *dbinfo;
-	SimpleStringListCell *cell;
 	int			i = 0;
 
 	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
 
-	for (cell = dbnames.head; cell; cell = cell->next)
+	for (SimpleStringListCell *cell = dbnames.head; cell; cell = cell->next)
 	{
 		char	   *conninfo;
 
@@ -469,7 +468,6 @@ get_primary_sysid(const char *conninfo)
 	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		PQclear(res);
 		disconnect_database(conn);
 		pg_fatal("could not get system identifier: %s",
 				 PQresultErrorMessage(res));
@@ -516,7 +514,7 @@ get_standby_sysid(const char *datadir)
 	pg_log_info("system identifier is %llu on subscriber",
 				(unsigned long long) sysid);
 
-	pfree(cf);
+	pg_free(cf);
 
 	return sysid;
 }
@@ -534,7 +532,6 @@ modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *o
 	struct timeval tv;
 
 	char	   *cmd_str;
-	int			rc;
 
 	pg_log_info("modifying system identifier from subscriber");
 
@@ -567,14 +564,15 @@ modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *o
 
 	if (!dry_run)
 	{
-		rc = system(cmd_str);
+		int rc = system(cmd_str);
+
 		if (rc == 0)
 			pg_log_info("subscriber successfully changed the system identifier");
 		else
 			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
 	}
 
-	pfree(cf);
+	pg_free(cf);
 }
 
 /*
@@ -584,11 +582,11 @@ modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *o
 static bool
 setup_publisher(LogicalRepInfo *dbinfo)
 {
-	PGconn	   *conn;
-	PGresult   *res;
 
 	for (int i = 0; i < num_dbs; i++)
 	{
+		PGconn	   *conn;
+		PGresult   *res;
 		char		pubname[NAMEDATALEN];
 		char		replslotname[NAMEDATALEN];
 
@@ -901,7 +899,7 @@ check_subscriber(LogicalRepInfo *dbinfo)
 		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
 		return false;
 	}
-	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
 	{
 		pg_log_error("permission denied for function \"%s\"",
 					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
@@ -990,10 +988,10 @@ check_subscriber(LogicalRepInfo *dbinfo)
 static bool
 setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
 {
-	PGconn	   *conn;
-
 	for (int i = 0; i < num_dbs; i++)
 	{
+		PGconn	   *conn;
+
 		/* Connect to subscriber. */
 		conn = connect_database(dbinfo[i].subconninfo);
 		if (conn == NULL)
@@ -1103,7 +1101,7 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
-						 slot_name, dbinfo->dbname, PQerrorMessage(conn));
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
 
 		PQclear(res);
 	}
@@ -1294,7 +1292,6 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		PQclear(res);
 		PQfinish(conn);
 		pg_fatal("could not obtain publication information: %s",
 				 PQresultErrorMessage(res));
@@ -1348,7 +1345,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		{
 			PQfinish(conn);
 			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
-					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+					 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
 		}
 	}
 
@@ -1384,7 +1381,7 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
-						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
 
 		PQclear(res);
 	}
@@ -1429,7 +1426,7 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		{
 			PQfinish(conn);
 			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
-					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+					 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
 		}
 	}
 
@@ -1465,7 +1462,7 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
 
 		PQclear(res);
 	}
@@ -1502,7 +1499,6 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		PQclear(res);
 		PQfinish(conn);
 		pg_fatal("could not obtain subscription OID: %s",
 				 PQresultErrorMessage(res));
@@ -1591,7 +1587,7 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		{
 			PQfinish(conn);
 			pg_fatal("could not enable subscription \"%s\": %s",
-					 dbinfo->subname, PQerrorMessage(conn));
+					 dbinfo->subname, PQresultErrorMessage(res));
 		}
 
 		PQclear(res);
@@ -1745,11 +1741,11 @@ main(int argc, char **argv)
 					pg_fatal("invalid old port number");
 				break;
 			case 'U':
-				pfree(opt.subuser);
+				pg_free(opt.subuser);
 				opt.subuser = pg_strdup(optarg);
 				break;
 			case 's':
-				pfree(opt.socketdir);
+				pg_free(opt.socketdir);
 				opt.socketdir = pg_strdup(optarg);
 				break;
 			case 'd':
@@ -1854,7 +1850,7 @@ main(int argc, char **argv)
 	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
 	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
 
-	/* rudimentary check for a data directory. */
+	/* Rudimentary check for a data directory */
 	if (!check_data_directory(opt.subscriber_dir))
 		exit(1);
 
@@ -1877,7 +1873,7 @@ main(int argc, char **argv)
 	/* Create the output directory to store any data generated by this tool */
 	server_start_log = setup_server_logfile(opt.subscriber_dir);
 
-	/* subscriber PID file. */
+	/* Subscriber PID file */
 	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
 
 	/*
-- 
2.43.0

v22-0008-Avoid-possible-null-report.patchapplication/octet-stream; name=v22-0008-Avoid-possible-null-report.patchDownload
From dbe5da9efef70a22deb92bf54c9a76439daef04e Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:20:00 +0000
Subject: [PATCH v22 08/11] Avoid possible null report

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index ea4eb7e621..f10e8002c6 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -921,7 +921,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 				 max_lrworkers);
 	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
 	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
-	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
 
 	PQclear(res);
 
-- 
2.43.0

v22-0009-prohibit-to-reuse-publications.patchapplication/octet-stream; name=v22-0009-prohibit-to-reuse-publications.patchDownload
From b01c9e1d815a60748515566ecd7414f704e8712f Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:32:35 +0000
Subject: [PATCH v22 09/11] prohibit to reuse publications

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 38 +++++++--------------
 1 file changed, 12 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index f10e8002c6..e88b29ea3e 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1264,7 +1264,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	/* Check if the publication needs to be created */
 	appendPQExpBuffer(str,
-					  "SELECT puballtables FROM pg_catalog.pg_publication "
+					  "SELECT count(1) FROM pg_catalog.pg_publication "
 					  "WHERE pubname = '%s'",
 					  dbinfo->pubname);
 	res = PQexec(conn, str->data);
@@ -1275,34 +1275,20 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 				 PQresultErrorMessage(res));
 	}
 
-	if (PQntuples(res) == 1)
+	if (atoi(PQgetvalue(res, 0, 0)) == 1)
 	{
 		/*
-		 * If publication name already exists and puballtables is true, let's
-		 * use it. A previous run of pg_createsubscriber must have created
-		 * this publication. Bail out.
+		 * Unfortunately, if it reaches this code path, it will always
+		 * fail (unless you decide to change the existing publication
+		 * name). That's bad but it is very unlikely that the user will
+		 * choose a name with pg_createsubscriber_ prefix followed by the
+		 * exact database oid in which puballtables is false.
 		 */
-		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
-		{
-			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
-			return;
-		}
-		else
-		{
-			/*
-			 * Unfortunately, if it reaches this code path, it will always
-			 * fail (unless you decide to change the existing publication
-			 * name). That's bad but it is very unlikely that the user will
-			 * choose a name with pg_createsubscriber_ prefix followed by the
-			 * exact database oid in which puballtables is false.
-			 */
-			pg_log_error("publication \"%s\" does not replicate changes for all tables",
-						 dbinfo->pubname);
-			pg_log_error_hint("Consider renaming this publication.");
-			PQclear(res);
-			PQfinish(conn);
-			exit(1);
-		}
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication.");
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
 	}
 
 	PQclear(res);
-- 
2.43.0

v22-0010-Make-the-ERROR-handling-more-consistent.patchapplication/octet-stream; name=v22-0010-Make-the-ERROR-handling-more-consistent.patchDownload
From d69f0bfc3644ea99fa55791ec4b630d15e88254b Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:42:17 +0000
Subject: [PATCH v22 10/11] Make the ERROR handling more consistent

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 38 +++------------------
 1 file changed, 5 insertions(+), 33 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index e88b29ea3e..f5ccd479b6 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -453,18 +453,12 @@ get_primary_sysid(const char *conninfo)
 
 	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		disconnect_database(conn);
 		pg_fatal("could not get system identifier: %s",
 				 PQresultErrorMessage(res));
-	}
+
 	if (PQntuples(res) != 1)
-	{
-		PQclear(res);
-		disconnect_database(conn);
 		pg_fatal("could not get system identifier: got %d rows, expected %d row",
 				 PQntuples(res), 1);
-	}
 
 	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
 
@@ -775,8 +769,6 @@ check_publisher(LogicalRepInfo *dbinfo)
 		{
 			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
 						 PQntuples(res), 1);
-			pg_free(primary_slot_name); /* it is not being used. */
-			primary_slot_name = NULL;
 			return false;
 		}
 		else
@@ -1269,11 +1261,8 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 					  dbinfo->pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		PQfinish(conn);
 		pg_fatal("could not obtain publication information: %s",
 				 PQresultErrorMessage(res));
-	}
 
 	if (atoi(PQgetvalue(res, 0, 0)) == 1)
 	{
@@ -1286,8 +1275,6 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		 */
 		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
 		pg_log_error_hint("Consider renaming this publication.");
-		PQclear(res);
-		PQfinish(conn);
 		exit(1);
 	}
 
@@ -1305,12 +1292,10 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
 					 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
-		}
 	}
 
 	/* for cleanup purposes */
@@ -1386,12 +1371,10 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
 					 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
-		}
 	}
 
 	if (!dry_run)
@@ -1428,19 +1411,12 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		PQfinish(conn);
 		pg_fatal("could not obtain subscription OID: %s",
 				 PQresultErrorMessage(res));
-	}
 
 	if (PQntuples(res) != 1 && !dry_run)
-	{
-		PQclear(res);
-		PQfinish(conn);
 		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
 				 PQntuples(res), 1);
-	}
 
 	if (dry_run)
 	{
@@ -1475,12 +1451,10 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
 					 dbinfo->subname, PQresultErrorMessage(res));
-		}
 
 		PQclear(res);
 	}
@@ -1513,12 +1487,10 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not enable subscription \"%s\": %s",
 					 dbinfo->subname, PQresultErrorMessage(res));
-		}
 
 		PQclear(res);
 	}
-- 
2.43.0

v22-0011-Update-test-codes.patchapplication/octet-stream; name=v22-0011-Update-test-codes.patchDownload
From 8f13206d1bfc0963fc658a90fe45f760adc21f98 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 16 Feb 2024 09:04:47 +0000
Subject: [PATCH v22 11/11] Update test codes

---
 .../t/040_pg_createsubscriber.pl              |   2 +-
 .../t/041_pg_createsubscriber_standby.pl      | 197 +++++++++---------
 2 files changed, 105 insertions(+), 94 deletions(-)

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 95eb4e70ac..65eba6f623 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -5,7 +5,7 @@
 #
 
 use strict;
-use warnings;
+use warnings  FATAL => 'all';
 use PostgreSQL::Test::Utils;
 use Test::More;
 
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 93148417db..06ef05d5e8 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -4,26 +4,23 @@
 # Test using a standby server as the subscriber.
 
 use strict;
-use warnings;
+use warnings FATAL => 'all';
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
 
-my $node_p;
-my $node_f;
-my $node_s;
-my $node_c;
-my $result;
-my $slotname;
-
 # Set up node P as primary
-$node_p = PostgreSQL::Test::Cluster->new('node_p');
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
 $node_p->init(allows_streaming => 'logical');
 $node_p->start;
 
-# Set up node F as about-to-fail node
-# Force it to initialize a new cluster instead of copying a
-# previously initdb'd cluster.
+# ------------------------------
+# Check pg_createsubscriber fails when the target server is not a
+# standby of the source.
+#
+# Set up node F as about-to-fail node. Force it to initialize a new cluster
+# instead of copying a previously initdb'd cluster.
+my $node_f;
 {
 	local $ENV{'INITDB_TEMPLATE'} = undef;
 
@@ -32,112 +29,91 @@ $node_p->start;
 	$node_f->start;
 }
 
-# On node P
-# - create databases
-# - create test tables
-# - insert a row
-# - create a physical replication slot
-$node_p->safe_psql(
-	'postgres', q(
-	CREATE DATABASE pg1;
-	CREATE DATABASE pg2;
-));
-$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
-$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
-$slotname = 'physical_slot';
-$node_p->safe_psql('pg2',
-	"SELECT pg_create_physical_replication_slot('$slotname')");
+# Run pg_createsubscriber on about-to-fail node F
+command_checks_all(
+	[
+		'pg_createsubscriber', '--verbose', '--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('postgres'),
+		'--port', $node_f->port, '--socketdir', $node_f->host,
+		'--database', 'postgres'
+	],
+	1,
+	[qr//],
+	[
+		qr/subscriber data directory is not a copy of the source database cluster/
+	],
+	'subscriber data directory is not a copy of the source database cluster');
 
+# ------------------------------
+# Check pg_createsubscriber fails when the target server is not running
+#
 # Set up node S as standby linking to node P
 $node_p->backup('backup_1');
-$node_s = PostgreSQL::Test::Cluster->new('node_s');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
 $node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
-$node_s->append_conf(
-	'postgresql.conf', qq[
-log_min_messages = debug2
-primary_slot_name = '$slotname'
-]);
 $node_s->set_standby_mode();
 
-# Run pg_createsubscriber on about-to-fail node F
-command_fails(
-	[
-		'pg_createsubscriber', '--verbose',
-		'--pgdata', $node_f->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
-		'--port', $node_f->port,
-		'--host', $node_f->host,
-		'--database', 'pg1',
-		'--database', 'pg2'
-	],
-	'subscriber data directory is not a copy of the source database cluster');
-
 # Run pg_createsubscriber on the stopped node
-command_fails(
+command_checks_all(
 	[
-		'pg_createsubscriber', '--verbose',
-		'--dry-run', '--pgdata',
-		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--port',
-		$node_s->port, '--host',
-		$node_s->host, '--database',
-		'pg1', '--database',
-		'pg2'
+		'pg_createsubscriber', '--verbose', '--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('postgres'),
+		'--port', $node_s->port, '--socketdir', $node_s->host,
+		'--database', 'postgres'
 	],
+	1,
+	[qr//],
+	[qr/standby is not running/],
 	'target server must be running');
 
 $node_s->start;
 
+# ------------------------------
+# Check pg_createsubscriber fails when the target server is a member of
+# the cascading standby.
+#
 # Set up node C as standby linking to node S
 $node_s->backup('backup_2');
-$node_c = PostgreSQL::Test::Cluster->new('node_c');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
 $node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
-$node_c->append_conf(
-	'postgresql.conf', qq[
-log_min_messages = debug2
-]);
 $node_c->set_standby_mode();
 $node_c->start;
 
 # Run pg_createsubscriber on node C (P -> S -> C)
-command_fails(
+command_checks_all(
 	[
-		'pg_createsubscriber', '--verbose',
-		'--dry-run', '--pgdata',
-		$node_c->data_dir, '--publisher-server',
-		$node_s->connstr('pg1'),
-		'--port', $node_c->port,
-		'--socketdir', $node_c->host,
-		'--database', 'pg1',
-		'--database', 'pg2'
+		'pg_createsubscriber', '--verbose', '--pgdata', $node_c->data_dir,
+		'--publisher-server', $node_s->connstr('postgres'),
+		'--port', $node_c->port, '--socketdir', $node_c->host,
+		'--database', 'postgres'
 	],
-	'primary server is in recovery');
+	1,
+	[qr//],
+	[qr/primary server cannot be in recovery/],
+	'target server must be running');
 
 # Stop node C
-$node_c->teardown_node;
-
-# Insert another row on node P and wait node S to catch up
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
-$node_p->wait_for_replay_catchup($node_s);
+$node_c->stop;
 
-# dry run mode on node S
+# ------------------------------
+# Check successful dry-run
+#
+# Dry run mode on node S
 command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--port',
-		$node_s->port, '--socketdir',
-		$node_s->host, '--database',
-		'pg1', '--database',
-		'pg2'
+		$node_p->connstr('postgres'),
+		'--port', $node_s->port,
+		'--socketdir', $node_s->host,
+		'--database', 'postgres'
 	],
 	'run pg_createsubscriber --dry-run on node S');
 
 # Check if node S is still a standby
-is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
-	't', 'standby is in recovery');
+my $result = $node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()');
+is($result, 't', 'standby is in recovery');
 
 # pg_createsubscriber can run without --databases option
 command_ok(
@@ -145,12 +121,39 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--port',
+		$node_p->connstr('postgres'), '--port',
 		$node_s->port, '--socketdir',
 		$node_s->host,
 	],
 	'run pg_createsubscriber without --databases');
 
+# ------------------------------
+# Check successful conversion
+#
+# Prepare databases and a physical replication slot
+my $slotname = 'physical_slot';
+$node_p->safe_psql(
+	'postgres', qq[
+		CREATE DATABASE pg1;
+		CREATE DATABASE pg2;
+		SELECT pg_create_physical_replication_slot('$slotname');
+]);
+
+# Use the created slot for physical replication
+$node_s->append_conf('postgresql.conf', "primary_slot_name = $slotname");
+$node_s->reload;
+
+# Prepare tables and initial data on pg1 and pg2
+$node_p->safe_psql(
+	'pg1', qq[
+		CREATE TABLE tbl1 (a text);
+		INSERT INTO tbl1 VALUES('first row');
+		INSERT INTO tbl1 VALUES('second row')
+]);
+$node_p->safe_psql('pg2', "CREATE TABLE tbl2 (a text);");
+
+$node_p->wait_for_replay_catchup($node_s);
+
 # Run pg_createsubscriber on node S
 command_ok(
 	[
@@ -176,15 +179,23 @@ is($result, qq(0),
 	'the physical replication slot used as primary_slot_name has been removed'
 );
 
-# Insert rows on P
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
-$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
-
 # PID sets to undefined because subscriber was stopped behind the scenes.
 # Start subscriber
 $node_s->{_pid} = undef;
 $node_s->start;
 
+# Confirm two subscriptions has been created
+$result = $node_s->safe_psql('postgres',
+	"SELECT count(distinct subdbid) FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_';"
+);
+is($result, qq(2),
+	'Subscriptions has been created to all the specified databases'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
 # Get subscription names
 $result = $node_s->safe_psql(
 	'postgres', qq(
@@ -214,9 +225,9 @@ my $sysid_s = $node_s->safe_psql('postgres',
 	'SELECT system_identifier FROM pg_control_system()');
 ok($sysid_p != $sysid_s, 'system identifier was changed');
 
-# clean up
-$node_p->teardown_node;
-$node_s->teardown_node;
-$node_f->teardown_node;
+# Clean up
+$node_p->stop;
+$node_s->stop;
+$node_f->stop;
 
 done_testing();
-- 
2.43.0

#142Peter Eisentraut
peter@eisentraut.org
In reply to: Euler Taveira (#136)
Re: speed up a logical replica setup

Some review of the v21 patch:

- commit message

Mention pg_createsubscriber in the commit message title. That's the
most important thing that someone doing git log searches in the future
will be looking for.

- doc/src/sgml/ref/allfiles.sgml

Move the new entry to alphabetical order.

- doc/src/sgml/ref/pg_createsubscriber.sgml

+  <para>
+   The <application>pg_createsubscriber</application> should be run at 
the target
+   server. The source server (known as publisher server) should accept 
logical
+   replication connections from the target server (known as subscriber 
server).
+   The target server should accept local logical replication connection.
+  </para>

"should" -> "must" ?

+ <refsect1>
+ <title>Options</title>

Sort options alphabetically.

It would be good to indicate somewhere which options are mandatory.

+ <refsect1>
+ <title>Examples</title>

I suggest including a pg_basebackup call into this example, so it's
easier for readers to get the context of how this is supposed to be
used. You can add that pg_basebackup in this example is just an example
and that other base backups can also be used.

- doc/src/sgml/reference.sgml

Move the new entry to alphabetical order.

- src/bin/pg_basebackup/Makefile

Move the new sections to alphabetical order.

- src/bin/pg_basebackup/meson.build

Move the new sections to alphabetical order.

- src/bin/pg_basebackup/pg_createsubscriber.c

+typedef struct CreateSubscriberOptions
+typedef struct LogicalRepInfo

I think these kinds of local-use struct don't need to be typedef'ed.
(Then you also don't need to update typdefs.list.)

+static void
+usage(void)

Sort the options alphabetically.

+static char *
+get_exec_path(const char *argv0, const char *progname)

Can this not use find_my_exec() and find_other_exec()?

+int
+main(int argc, char **argv)

Sort the options alphabetically (long_options struct, getopt_long()
argument, switch cases).

- .../t/040_pg_createsubscriber.pl
- .../t/041_pg_createsubscriber_standby.pl

These two files could be combined into one.

+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster.

Explain why?

+$node_s->append_conf(
+	'postgresql.conf', qq[
+log_min_messages = debug2

Is this setting necessary for the test?

#143Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Euler Taveira (#136)
1 attachment(s)
Re: speed up a logical replica setup

Hi,

I have reviewed the v21 patch. And found an issue.

Initially I started the standby server with a new postgresql.conf file
(not the default postgresql.conf that is present in the instance).
pg_ctl -D ../standby start -o "-c config_file=/new_path/postgresql.conf"

And I have made 'max_replication_slots = 1' in new postgresql.conf and
made 'max_replication_slots = 0' in the default postgresql.conf file.
Now when we run pg_createsubscriber on standby we get error:
pg_createsubscriber: error: could not set replication progress for the
subscription "pg_createsubscriber_5_242843": ERROR: cannot query or
manipulate replication origin when max_replication_slots = 0
NOTICE: dropped replication slot "pg_createsubscriber_5_242843" on publisher
pg_createsubscriber: error: could not drop publication
"pg_createsubscriber_5" on database "postgres": ERROR: publication
"pg_createsubscriber_5" does not exist
pg_createsubscriber: error: could not drop replication slot
"pg_createsubscriber_5_242843" on database "postgres": ERROR:
replication slot "pg_createsubscriber_5_242843" does not exist

I observed that when we run the pg_createsubscriber command, it will
stop the standby instance (the non-default postgres configuration) and
restart the standby instance which will now be started with default
postgresql.conf, where the 'max_replication_slot = 0' and
pg_createsubscriber will now fail with the error given above.
I have added the script file with which we can reproduce this issue.
Also similar issues can happen with other configurations such as port, etc.

The possible solution would be
1) allow to run pg_createsubscriber if standby is initially stopped .
I observed that pg_logical_createsubscriber also uses this approach.
2) read GUCs via SHOW command and restore them when server restarts

I would prefer the 1st solution.

Thanks and Regards,
Shlok Kyal

Attachments:

debug.shtext/x-sh; charset=US-ASCII; name=debug.shDownload
#144Euler Taveira
euler@eulerto.com
In reply to: Peter Eisentraut (#142)
Re: speed up a logical replica setup

On Mon, Feb 19, 2024, at 6:47 AM, Peter Eisentraut wrote:

Some review of the v21 patch:

Thanks for checking.

- commit message

Mention pg_createsubscriber in the commit message title. That's the
most important thing that someone doing git log searches in the future
will be looking for.

Right. Done.

- doc/src/sgml/ref/allfiles.sgml

Move the new entry to alphabetical order.

Done.

- doc/src/sgml/ref/pg_createsubscriber.sgml

+  <para>
+   The <application>pg_createsubscriber</application> should be run at 
the target
+   server. The source server (known as publisher server) should accept 
logical
+   replication connections from the target server (known as subscriber 
server).
+   The target server should accept local logical replication connection.
+  </para>

"should" -> "must" ?

Done.

+ <refsect1>
+ <title>Options</title>

Sort options alphabetically.

Done.

It would be good to indicate somewhere which options are mandatory.

I'll add this information in the option description. AFAICT the synopsis kind
of indicates it.

+ <refsect1>
+ <title>Examples</title>

I suggest including a pg_basebackup call into this example, so it's
easier for readers to get the context of how this is supposed to be
used. You can add that pg_basebackup in this example is just an example
and that other base backups can also be used.

We can certainly add it but creating a standby isn't out of scope here? I will
make sure to include references to pg_basebackup and the "Setting up a Standby
Server" section.

- doc/src/sgml/reference.sgml

Move the new entry to alphabetical order.

Done.

- src/bin/pg_basebackup/Makefile

Move the new sections to alphabetical order.

Done.

- src/bin/pg_basebackup/meson.build

Move the new sections to alphabetical order.

Done.

- src/bin/pg_basebackup/pg_createsubscriber.c

+typedef struct CreateSubscriberOptions
+typedef struct LogicalRepInfo

I think these kinds of local-use struct don't need to be typedef'ed.
(Then you also don't need to update typdefs.list.)

Done.

+static void
+usage(void)

Sort the options alphabetically.

Are you referring to s/options/functions/?

+static char *
+get_exec_path(const char *argv0, const char *progname)

Can this not use find_my_exec() and find_other_exec()?

It is indeed using it. I created this function because it needs to run the same
code path twice (pg_ctl and pg_resetwal).

+int
+main(int argc, char **argv)

Sort the options alphabetically (long_options struct, getopt_long()
argument, switch cases).

Done.

- .../t/040_pg_createsubscriber.pl
- .../t/041_pg_createsubscriber_standby.pl

These two files could be combined into one.

Done.

+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster.

Explain why?

Ok. It needs a new cluster because it will have a different system identifier
so we can make sure the target cluster is a copy of the source server.

+$node_s->append_conf(
+ 'postgresql.conf', qq[
+log_min_messages = debug2

Is this setting necessary for the test?

No. It is here as a debugging aid. Better to include it in a separate patch.
There are a few messages that I don't intend to include in the final patch.

All of these modifications will be included in the next patch. I'm finishing to
integrate patches proposed by Hayato [1]/messages/by-id/TYCPR01MB12077A8421685E5515DE408EEF5512@TYCPR01MB12077.jpnprd01.prod.outlook.com and some additional fixes and
refactors.

[1]: /messages/by-id/TYCPR01MB12077A8421685E5515DE408EEF5512@TYCPR01MB12077.jpnprd01.prod.outlook.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

#145Euler Taveira
euler@eulerto.com
In reply to: Shlok Kyal (#143)
Re: speed up a logical replica setup

On Mon, Feb 19, 2024, at 7:22 AM, Shlok Kyal wrote:

I have reviewed the v21 patch. And found an issue.

Initially I started the standby server with a new postgresql.conf file
(not the default postgresql.conf that is present in the instance).
pg_ctl -D ../standby start -o "-c config_file=/new_path/postgresql.conf"

And I have made 'max_replication_slots = 1' in new postgresql.conf and
made 'max_replication_slots = 0' in the default postgresql.conf file.
Now when we run pg_createsubscriber on standby we get error:
pg_createsubscriber: error: could not set replication progress for the
subscription "pg_createsubscriber_5_242843": ERROR: cannot query or
manipulate replication origin when max_replication_slots = 0

That's by design. See [1]https://www.postgresql.org/docs/current/runtime-config-replication.html#RUNTIME-CONFIG-REPLICATION-SUBSCRIBER. The max_replication_slots parameter is used as the
maximum number of subscriptions on the server.

NOTICE: dropped replication slot "pg_createsubscriber_5_242843" on publisher
pg_createsubscriber: error: could not drop publication
"pg_createsubscriber_5" on database "postgres": ERROR: publication
"pg_createsubscriber_5" does not exist
pg_createsubscriber: error: could not drop replication slot
"pg_createsubscriber_5_242843" on database "postgres": ERROR:
replication slot "pg_createsubscriber_5_242843" does not exist

That's a bug and should be fixed.

[1]: https://www.postgresql.org/docs/current/runtime-config-replication.html#RUNTIME-CONFIG-REPLICATION-SUBSCRIBER

--
Euler Taveira
EDB https://www.enterprisedb.com/

#146Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Euler Taveira (#145)
Re: speed up a logical replica setup

Hi,

On Tue, 20 Feb 2024 at 06:59, Euler Taveira <euler@eulerto.com> wrote:

On Mon, Feb 19, 2024, at 7:22 AM, Shlok Kyal wrote:

I have reviewed the v21 patch. And found an issue.

Initially I started the standby server with a new postgresql.conf file
(not the default postgresql.conf that is present in the instance).
pg_ctl -D ../standby start -o "-c config_file=/new_path/postgresql.conf"

And I have made 'max_replication_slots = 1' in new postgresql.conf and
made 'max_replication_slots = 0' in the default postgresql.conf file.
Now when we run pg_createsubscriber on standby we get error:
pg_createsubscriber: error: could not set replication progress for the
subscription "pg_createsubscriber_5_242843": ERROR: cannot query or
manipulate replication origin when max_replication_slots = 0

That's by design. See [1]. The max_replication_slots parameter is used as the
maximum number of subscriptions on the server.

NOTICE: dropped replication slot "pg_createsubscriber_5_242843" on publisher
pg_createsubscriber: error: could not drop publication
"pg_createsubscriber_5" on database "postgres": ERROR: publication
"pg_createsubscriber_5" does not exist
pg_createsubscriber: error: could not drop replication slot
"pg_createsubscriber_5_242843" on database "postgres": ERROR:
replication slot "pg_createsubscriber_5_242843" does not exist

That's a bug and should be fixed.

[1] https://www.postgresql.org/docs/current/runtime-config-replication.html#RUNTIME-CONFIG-REPLICATION-SUBSCRIBER

I think you misunderstood the issue, I reported.

My main concern is that the standby server is using different
'postgresql.conf' file (the default file) after :
+   /* Start subscriber and wait until accepting connections */
+   pg_log_info("starting the subscriber");
+   if (!dry_run)
+       start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);

But we initially started the standby server (before running the
pg_createsubscriber) with a new postgresql.conf file (different from
the default file. for this example created inside the 'new_path'
folder).
pg_ctl -D ../standby -l standby.log start -o "-c
config_file=../new_path/postgresql.conf"

So, when we run the pg_createsubscriber, all the initial checks will
be run on the standby server started using the new postgresql.conf
file. But during pg_createsubscriber, it will restart the standby
server using the default postgresql.conf file. And this might create
some issues.

Thanks and Regards,
Shlok Kyal

#147vignesh C
vignesh21@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#141)
Re: speed up a logical replica setup

On Mon, 19 Feb 2024 at 11:15, Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear hackers,

Since it may be useful, I will post top-up patch on Monday, if there are no
updating.

And here are top-up patches. Feel free to check and include.

v22-0001: Same as v21-0001.
=== rebased patches ===
v22-0002: Update docs per recent changes. Same as v20-0002.
v22-0003: Add check versions of the target. Extracted from v20-0003.
v22-0004: Remove -S option. Mostly same as v20-0009, but commit massage was
slightly changed.
=== Newbie ===
V22-0005: Addressed my comments which seems to be trivial[1].
Comments #1, 3, 4, 8, 10, 14, 17 were addressed here.
v22-0006: Consider the scenario when commands are failed after the recovery.
drop_subscription() is removed and some messages are added per [2].
V22-0007: Revise server_is_in_recovery() per [1]. Comments #5, 6, 7, were addressed here.
V22-0008: Fix a strange report when physical_primary_slot is null. Per comment #9 [1].
V22-0009: Prohibit reuse publications when it has already existed. Per comments #11 and 12 [1].
V22-0010: Avoid to call PQclear()/PQfinish()/pg_free() if the process exits soon. Per comment #15 [1].
V22-0011: Update testcode. Per comments #17- [1].

I did not handle below points because I have unclear points.

a.
This patch set cannot detect the disconnection between the target (standby) and
source (primary) during the catch up. Because the connection status must be gotten
at the same time (=in the same query) with the recovery status, but now it is now an
independed function (server_is_in_recovery()).

b.
This patch set cannot detect the inconsistency reported by Shubham [3]. I could not
come up with solutions without removing -P...

Few comments for v22-0001 patch:
1) The second "if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)"" should
be if (strcmp(PQgetvalue(res, 0, 2), "t") != 0):
+       if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+       {
+               pg_log_error("permission denied for database %s",
dbinfo[0].dbname);
+               return false;
+       }
+       if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+       {
+               pg_log_error("permission denied for function \"%s\"",
+
"pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+               return false;
+       }

2) pg_createsubscriber fails if a table is parallely created in the
primary node:
2024-02-20 14:38:49.005 IST [277261] LOG: database system is ready to
accept connections
2024-02-20 14:38:54.346 IST [277270] ERROR: relation "public.tbl5"
does not exist
2024-02-20 14:38:54.346 IST [277270] STATEMENT: CREATE SUBSCRIPTION
pg_createsubscriber_5_277236 CONNECTION ' dbname=postgres' PUBLICATION
pg_createsubscriber_5 WITH (create_slot = false, copy_data = false,
enabled = false)

If we are not planning to fix this, at least it should be documented

3) Error conditions is verbose mode has invalid error message like
"out of memory" messages like in below:
pg_createsubscriber: waiting the postmaster to reach the consistent state
pg_createsubscriber: postmaster reached the consistent state
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: creating subscription
"pg_createsubscriber_5_278343" on database "postgres"
pg_createsubscriber: error: could not create subscription
"pg_createsubscriber_5_278343" on database "postgres": out of memory

4) In error cases we try to drop this publication again resulting in error:
+               /*
+                * Since the publication was created before the
consistent LSN, it is
+                * available on the subscriber when the physical
replica is promoted.
+                * Remove publications from the subscriber because it
has no use.
+                */
+               drop_publication(conn, &dbinfo[i]);

Which throws these errors(because of drop publication multiple times):
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: error: could not drop publication
"pg_createsubscriber_5" on database "postgres": ERROR: publication
"pg_createsubscriber_5" does not exist
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: dropping the replication slot
"pg_createsubscriber_5_278343" on database "postgres"

5) In error cases, wait_for_end_recovery waits even though it has
identified that the replication between primary and standby is
stopped:
+/*
+ * Is recovery still in progress?
+ * If the answer is yes, it returns 1, otherwise, returns 0. If an error occurs
+ * while executing the query, it returns -1.
+ */
+static int
+server_is_in_recovery(PGconn *conn)
+{
+       PGresult   *res;
+       int                     ret;
+
+       res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               PQclear(res);
+               pg_log_error("could not obtain recovery progress");
+               return -1;
+       }
+

You can simulate this by stopping the primary just before
wait_for_end_recovery and you will see these error messages, but
pg_createsubscriber will continue to wait:
pg_createsubscriber: error: could not obtain recovery progress
pg_createsubscriber: error: could not obtain recovery progress
pg_createsubscriber: error: could not obtain recovery progress
pg_createsubscriber: error: could not obtain recovery progress
...

Regards,
Vignesh

#148Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: vignesh C (#147)
RE: speed up a logical replica setup

Dear Vignesh,

Thanks for giving comments!

Few comments for v22-0001 patch:
1) The second "if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)"" should
be if (strcmp(PQgetvalue(res, 0, 2), "t") != 0):
+       if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+       {
+               pg_log_error("permission denied for database %s",
dbinfo[0].dbname);
+               return false;
+       }
+       if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+       {
+               pg_log_error("permission denied for function \"%s\"",
+
"pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+               return false;
+       }

I have already pointed out as comment #8 [1]/messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com and fixed in v22-0005.

2) pg_createsubscriber fails if a table is parallely created in the
primary node:
2024-02-20 14:38:49.005 IST [277261] LOG: database system is ready to
accept connections
2024-02-20 14:38:54.346 IST [277270] ERROR: relation "public.tbl5"
does not exist
2024-02-20 14:38:54.346 IST [277270] STATEMENT: CREATE SUBSCRIPTION
pg_createsubscriber_5_277236 CONNECTION ' dbname=postgres' PUBLICATION
pg_createsubscriber_5 WITH (create_slot = false, copy_data = false,
enabled = false)

If we are not planning to fix this, at least it should be documented

The error will be occurred when tables are created after the promotion, right?
I think it cannot be fixed until DDL logical replication would be implemented.
So, +1 to add descriptions.

3) Error conditions is verbose mode has invalid error message like
"out of memory" messages like in below:
pg_createsubscriber: waiting the postmaster to reach the consistent state
pg_createsubscriber: postmaster reached the consistent state
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: creating subscription
"pg_createsubscriber_5_278343" on database "postgres"
pg_createsubscriber: error: could not create subscription
"pg_createsubscriber_5_278343" on database "postgres": out of memory

Because some places use PQerrorMessage() wrongly. It should be
PQresultErrorMessage(). Fixed in v22-0005.

4) In error cases we try to drop this publication again resulting in error:
+               /*
+                * Since the publication was created before the
consistent LSN, it is
+                * available on the subscriber when the physical
replica is promoted.
+                * Remove publications from the subscriber because it
has no use.
+                */
+               drop_publication(conn, &dbinfo[i]);

Which throws these errors(because of drop publication multiple times):
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: error: could not drop publication
"pg_createsubscriber_5" on database "postgres": ERROR: publication
"pg_createsubscriber_5" does not exist
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: dropping the replication slot
"pg_createsubscriber_5_278343" on database "postgres"

Right. One approach is to use DROP PUBLICATION IF EXISTS statement.
Thought?

5) In error cases, wait_for_end_recovery waits even though it has
identified that the replication between primary and standby is
stopped:
+/*
+ * Is recovery still in progress?
+ * If the answer is yes, it returns 1, otherwise, returns 0. If an error occurs
+ * while executing the query, it returns -1.
+ */
+static int
+server_is_in_recovery(PGconn *conn)
+{
+       PGresult   *res;
+       int                     ret;
+
+       res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               PQclear(res);
+               pg_log_error("could not obtain recovery progress");
+               return -1;
+       }
+

You can simulate this by stopping the primary just before
wait_for_end_recovery and you will see these error messages, but
pg_createsubscriber will continue to wait:
pg_createsubscriber: error: could not obtain recovery progress
pg_createsubscriber: error: could not obtain recovery progress
pg_createsubscriber: error: could not obtain recovery progress
pg_createsubscriber: error: could not obtain recovery progress

Yeah, v22-0001 cannot detect the disconnection from primary and standby.
V22-0007 can detect the standby crash, but v22 set could not detect the
primary crash. Euler came up with an approach [2]/messages/by-id/2231a04b-f2d4-4a4e-b5cd-56be8b002427@app.fastmail.com for it but not implemented yet.

[1]: /messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com
[2]: /messages/by-id/2231a04b-f2d4-4a4e-b5cd-56be8b002427@app.fastmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#149vignesh C
vignesh21@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#148)
Re: speed up a logical replica setup

On Tue, 20 Feb 2024 at 15:47, Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear Vignesh,

Thanks for giving comments!

Few comments for v22-0001 patch:
1) The second "if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)"" should
be if (strcmp(PQgetvalue(res, 0, 2), "t") != 0):
+       if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+       {
+               pg_log_error("permission denied for database %s",
dbinfo[0].dbname);
+               return false;
+       }
+       if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+       {
+               pg_log_error("permission denied for function \"%s\"",
+
"pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+               return false;
+       }

I have already pointed out as comment #8 [1] and fixed in v22-0005.

2) pg_createsubscriber fails if a table is parallely created in the
primary node:
2024-02-20 14:38:49.005 IST [277261] LOG: database system is ready to
accept connections
2024-02-20 14:38:54.346 IST [277270] ERROR: relation "public.tbl5"
does not exist
2024-02-20 14:38:54.346 IST [277270] STATEMENT: CREATE SUBSCRIPTION
pg_createsubscriber_5_277236 CONNECTION ' dbname=postgres' PUBLICATION
pg_createsubscriber_5 WITH (create_slot = false, copy_data = false,
enabled = false)

If we are not planning to fix this, at least it should be documented

The error will be occurred when tables are created after the promotion, right?
I think it cannot be fixed until DDL logical replication would be implemented.
So, +1 to add descriptions.

3) Error conditions is verbose mode has invalid error message like
"out of memory" messages like in below:
pg_createsubscriber: waiting the postmaster to reach the consistent state
pg_createsubscriber: postmaster reached the consistent state
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: creating subscription
"pg_createsubscriber_5_278343" on database "postgres"
pg_createsubscriber: error: could not create subscription
"pg_createsubscriber_5_278343" on database "postgres": out of memory

Because some places use PQerrorMessage() wrongly. It should be
PQresultErrorMessage(). Fixed in v22-0005.

4) In error cases we try to drop this publication again resulting in error:
+               /*
+                * Since the publication was created before the
consistent LSN, it is
+                * available on the subscriber when the physical
replica is promoted.
+                * Remove publications from the subscriber because it
has no use.
+                */
+               drop_publication(conn, &dbinfo[i]);

Which throws these errors(because of drop publication multiple times):
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: error: could not drop publication
"pg_createsubscriber_5" on database "postgres": ERROR: publication
"pg_createsubscriber_5" does not exist
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: dropping the replication slot
"pg_createsubscriber_5_278343" on database "postgres"

Right. One approach is to use DROP PUBLICATION IF EXISTS statement.
Thought?

Another way would be to set made_publication to false in
drop_publication once the publication is dropped. This way after the
publication is dropped it will not try to drop the publication again
in cleanup_objects_atexit as the made_publication will be false now.

Regards,
Vignesh

#150Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Hayato Kuroda (Fujitsu) (#140)
Re: speed up a logical replica setup

On 2024-Feb-16, Hayato Kuroda (Fujitsu) wrote:

15.

You said in case of failure, cleanups is not needed if the process exits soon [1].
But some functions call PQfinish() then exit(1) or pg_fatal(). Should we follow?

Hmm, but doesn't this mean that the server will log an ugly message that
"client closed connection unexpectedly"? I think it's nicer to close
the connection before terminating the process (especially since the
code for that is already written).

--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"We’ve narrowed the problem down to the customer’s pants being in a situation
of vigorous combustion" (Robert Haas, Postgres expert extraordinaire)

#151vignesh C
vignesh21@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#141)
Re: speed up a logical replica setup

On Mon, 19 Feb 2024 at 11:15, Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear hackers,

Since it may be useful, I will post top-up patch on Monday, if there are no
updating.

And here are top-up patches. Feel free to check and include.

v22-0001: Same as v21-0001.
=== rebased patches ===
v22-0002: Update docs per recent changes. Same as v20-0002.
v22-0003: Add check versions of the target. Extracted from v20-0003.
v22-0004: Remove -S option. Mostly same as v20-0009, but commit massage was
slightly changed.
=== Newbie ===
V22-0005: Addressed my comments which seems to be trivial[1].
Comments #1, 3, 4, 8, 10, 14, 17 were addressed here.
v22-0006: Consider the scenario when commands are failed after the recovery.
drop_subscription() is removed and some messages are added per [2].
V22-0007: Revise server_is_in_recovery() per [1]. Comments #5, 6, 7, were addressed here.
V22-0008: Fix a strange report when physical_primary_slot is null. Per comment #9 [1].
V22-0009: Prohibit reuse publications when it has already existed. Per comments #11 and 12 [1].
V22-0010: Avoid to call PQclear()/PQfinish()/pg_free() if the process exits soon. Per comment #15 [1].
V22-0011: Update testcode. Per comments #17- [1].

I did not handle below points because I have unclear points.

a.
This patch set cannot detect the disconnection between the target (standby) and
source (primary) during the catch up. Because the connection status must be gotten
at the same time (=in the same query) with the recovery status, but now it is now an
independed function (server_is_in_recovery()).

b.
This patch set cannot detect the inconsistency reported by Shubham [3]. I could not
come up with solutions without removing -P...

Few comments regarding the documentation:
1) max_replication_slots information seems to be present couple of times:

+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     and <link
linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases.
+    </para>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases and replication slots.
+    </para>
+   </listitem>
2) Can we add an id to prerequisites and use it instead of referring
to r1-app-pg_createsubscriber-1:
-     <application>pg_createsubscriber</application> checks if the
given target data
-     directory has the same system identifier than the source data directory.
-     Since it uses the recovery process as one of the steps, it starts the
-     target server as a replica from the source server. If the system
-     identifier is not the same,
<application>pg_createsubscriber</application> will
-     terminate with an error.
+     Checks the target can be converted.  In particular, things listed in
+     <link linkend="r1-app-pg_createsubscriber-1">above section</link> would be
+     checked.  If these are not met
<application>pg_createsubscriber</application>
+     will terminate with an error.
     </para>

3) The code also checks the following:
Verify if a PostgreSQL binary (progname) is available in the same
directory as pg_createsubscriber.

But this is not present in the pre-requisites of documentation.

4) Here we mention that the target server should be stopped, but the
same is not mentioned in prerequisites:
+   Here is an example of using <application>pg_createsubscriber</application>.
+   Before running the command, please make sure target server is stopped.
+<screen>
+<prompt>$</prompt> <userinput>pg_ctl -D /usr/local/pgsql/data stop</userinput>
+</screen>
+

5) If there is an error during any of the pg_createsubscriber
operation like if create subscription fails, it might not be possible
to rollback to the earlier state which had physical-standby
replication. I felt we should document this and also add it to the
console message like how we do in case of pg_upgrade.

Regards,
Vignesh

#152vignesh C
vignesh21@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#141)
Re: speed up a logical replica setup

On Mon, 19 Feb 2024 at 11:15, Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear hackers,

Since it may be useful, I will post top-up patch on Monday, if there are no
updating.

And here are top-up patches. Feel free to check and include.

v22-0001: Same as v21-0001.
=== rebased patches ===
v22-0002: Update docs per recent changes. Same as v20-0002.
v22-0003: Add check versions of the target. Extracted from v20-0003.
v22-0004: Remove -S option. Mostly same as v20-0009, but commit massage was
slightly changed.
=== Newbie ===
V22-0005: Addressed my comments which seems to be trivial[1].
Comments #1, 3, 4, 8, 10, 14, 17 were addressed here.
v22-0006: Consider the scenario when commands are failed after the recovery.
drop_subscription() is removed and some messages are added per [2].
V22-0007: Revise server_is_in_recovery() per [1]. Comments #5, 6, 7, were addressed here.
V22-0008: Fix a strange report when physical_primary_slot is null. Per comment #9 [1].
V22-0009: Prohibit reuse publications when it has already existed. Per comments #11 and 12 [1].
V22-0010: Avoid to call PQclear()/PQfinish()/pg_free() if the process exits soon. Per comment #15 [1].
V22-0011: Update testcode. Per comments #17- [1].

I did not handle below points because I have unclear points.

a.
This patch set cannot detect the disconnection between the target (standby) and
source (primary) during the catch up. Because the connection status must be gotten
at the same time (=in the same query) with the recovery status, but now it is now an
independed function (server_is_in_recovery()).

b.
This patch set cannot detect the inconsistency reported by Shubham [3]. I could not
come up with solutions without removing -P...

Few comments:
1) The below code can lead to assertion failure if the publisher is
stopped while dropping the replication slot:
+       if (primary_slot_name != NULL)
+       {
+               conn = connect_database(dbinfo[0].pubconninfo);
+               if (conn != NULL)
+               {
+                       drop_replication_slot(conn, &dbinfo[0],
primary_slot_name);
+               }
+               else
+               {
+                       pg_log_warning("could not drop replication
slot \"%s\" on primary",
+                                                  primary_slot_name);
+                       pg_log_warning_hint("Drop this replication
slot soon to avoid retention of WAL files.");
+               }
+               disconnect_database(conn);
+       }

pg_createsubscriber: error: connection to database failed: connection
to server on socket "/tmp/.s.PGSQL.5432" failed: No such file or
directory
Is the server running locally and accepting connections on that socket?
pg_createsubscriber: warning: could not drop replication slot
"standby_1" on primary
pg_createsubscriber: hint: Drop this replication slot soon to avoid
retention of WAL files.
pg_createsubscriber: pg_createsubscriber.c:432: disconnect_database:
Assertion `conn != ((void *)0)' failed.
Aborted (core dumped)

This is happening because we are calling disconnect_database in case
of connection failure case too which has the following assert:
+static void
+disconnect_database(PGconn *conn)
+{
+       Assert(conn != NULL);
+
+       PQfinish(conn);
+}
2) There is a CheckDataVersion function which does exactly this, will
we be able to use this:
+       /* Check standby server version */
+       if ((ver_fd = fopen(versionfile, "r")) == NULL)
+               pg_fatal("could not open file \"%s\" for reading: %m",
versionfile);
+
+       /* Version number has to be the first line read */
+       if (!fgets(rawline, sizeof(rawline), ver_fd))
+       {
+               if (!ferror(ver_fd))
+                       pg_fatal("unexpected empty file \"%s\"", versionfile);
+               else
+                       pg_fatal("could not read file \"%s\": %m", versionfile);
+       }
+
+       /* Strip trailing newline and carriage return */
+       (void) pg_strip_crlf(rawline);
+
+       if (strcmp(rawline, PG_MAJORVERSION) != 0)
+       {
+               pg_log_error("standby server is of wrong version");
+               pg_log_error_detail("File \"%s\" contains \"%s\",
which is not compatible with this program's version \"%s\".",
+                                                       versionfile,
rawline, PG_MAJORVERSION);
+               exit(1);
+       }
+
+       fclose(ver_fd);
3) Should this be added to typedefs.list:
+enum WaitPMResult
+{
+       POSTMASTER_READY,
+       POSTMASTER_STILL_STARTING
+};
4) pgCreateSubscriber should be mentioned after pg_controldata to keep
the ordering consistency:
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
5) Here pg_replication_slots should be pg_catalog.pg_replication_slots:
+       if (primary_slot_name)
+       {
+               appendPQExpBuffer(str,
+                                                 "SELECT 1 FROM
pg_replication_slots "
+                                                 "WHERE active AND
slot_name = '%s'",
+                                                 primary_slot_name);
6) Here pg_settings should be pg_catalog.pg_settings:
+        * - max_worker_processes >= 1 + number of dbs to be converted
+        *------------------------------------------------------------------------
+        */
+       res = PQexec(conn,
+                                "SELECT setting FROM pg_settings
WHERE name IN ("
+                                "'max_logical_replication_workers', "
+                                "'max_replication_slots', "
+                                "'max_worker_processes', "
+                                "'primary_slot_name') "
+                                "ORDER BY name");

Regards,
Vignesh

#153Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#141)
13 attachment(s)
Re: speed up a logical replica setup

And here are top-up patches. Feel free to check and include.

v22-0001: Same as v21-0001.
=== rebased patches ===
v22-0002: Update docs per recent changes. Same as v20-0002.
v22-0003: Add check versions of the target. Extracted from v20-0003.
v22-0004: Remove -S option. Mostly same as v20-0009, but commit massage was
slightly changed.
=== Newbie ===
V22-0005: Addressed my comments which seems to be trivial[1].
Comments #1, 3, 4, 8, 10, 14, 17 were addressed here.
v22-0006: Consider the scenario when commands are failed after the recovery.
drop_subscription() is removed and some messages are added per [2].
V22-0007: Revise server_is_in_recovery() per [1]. Comments #5, 6, 7, were addressed here.
V22-0008: Fix a strange report when physical_primary_slot is null. Per comment #9 [1].
V22-0009: Prohibit reuse publications when it has already existed. Per comments #11 and 12 [1].
V22-0010: Avoid to call PQclear()/PQfinish()/pg_free() if the process exits soon. Per comment #15 [1].
V22-0011: Update testcode. Per comments #17- [1].

I found some issues and fixed those issues with top up patches
v23-0012 and v23-0013
1.
Suppose there is a cascade physical replication node1->node2->node3.
Now if we run pg_createsubscriber with node1 as primary and node2 as
standby, pg_createsubscriber will be successful but the connection
between node2 and node3 will not be retained and log og node3 will
give error:
2024-02-20 12:32:12.340 IST [277664] FATAL: database system
identifier differs between the primary and standby
2024-02-20 12:32:12.340 IST [277664] DETAIL: The primary's identifier
is 7337575856950914038, the standby's identifier is
7337575783125171076.
2024-02-20 12:32:12.341 IST [277491] LOG: waiting for WAL to become
available at 0/3000F10

To fix this I am avoiding pg_createsubscriber to run if the standby
node is primary to any other server.
Made the change in v23-0012 patch

2.
While checking 'max_replication_slots' in 'check_publisher' function,
we are not considering the temporary slot in the check:
+   if (max_repslots - cur_repslots < num_dbs)
+   {
+       pg_log_error("publisher requires %d replication slots, but
only %d remain",
+                    num_dbs, max_repslots - cur_repslots);
+       pg_log_error_hint("Consider increasing max_replication_slots
to at least %d.",
+                         cur_repslots + num_dbs);
+       return false;
+   }
Fixed this in v23-0013

v23-0001 to v23-0011 is same as v22-0001 to v22-0011

Thanks and Regards,
Shlok Kyal

Attachments:

v23-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchapplication/octet-stream; name=v23-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchDownload
From 80af1800fb3de03067e957cf4570b0a291ce5c66 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v23 01/13] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica
into a logical replica. It runs on the target server and should be able
to connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.  Stop the
target server. Change the system identifier from the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  320 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1972 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   39 +
 .../t/041_pg_createsubscriber_standby.pl      |  217 ++
 src/tools/pgindent/typedefs.list              |    2 +
 10 files changed, 2579 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f5238771b7
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,320 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..205a835d36
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1972 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+/* Command-line options */
+typedef struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *sub_conninfo_str;	/* subscriber connection string */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+} CreateSubscriberOptions;
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name / replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_exec_path(const char *argv0, const char *progname);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+										  const char *pub_base_conninfo,
+										  const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path,
+									CreateSubscriberOptions *opt);
+static int	server_is_in_recovery(PGconn *conn);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn,
+											 LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void start_standby_server(const char *pg_ctl_path, const char *datadir,
+								 const char *logfile);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+								  CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static bool recovery_ended = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription || recovery_ended)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_subscription)
+					drop_subscription(conn, &dbinfo[i]);
+
+				/*
+				 * Publications are created on publisher before promotion so
+				 * it might exist on subscriber after recovery ends.
+				 */
+				if (recovery_ended)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir,
+						 strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory",
+					 datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		dbinfo[i].made_subscription = false;
+		/* Other fields will be filled later */
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: %s",
+				 PQresultErrorMessage(res));
+	}
+	if (PQntuples(res) != 1)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: got %d rows, expected %d row",
+				 PQntuples(res), 1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is recovery still in progress?
+ * If the answer is yes, it returns 1, otherwise, returns 0. If an error occurs
+ * while executing the query, it returns -1.
+ */
+static int
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		pg_log_error("could not obtain recovery progress");
+		return -1;
+	}
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	if (ret == 0)
+		return 1;
+	else if (ret > 0)
+		return 0;
+	else
+		return -1;				/* should not happen */
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn) == 1)
+		pg_fatal("primary server cannot be in recovery");
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/* The target server must be a standby */
+	if (server_is_in_recovery(conn) == 0)
+	{
+		pg_log_error("The target server is not a standby");
+		return false;
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		return false;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/* This temporary replication slot is only used for catchup purposes */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* Cleanup if there is any failure */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it is
+ * run.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* Append timestamp with ISO 8601 format */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir,
+				   PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_ctl_path, const char *datadir,
+					 const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"",
+						  pg_ctl_path, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+					  CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		int			in_recovery;
+
+		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (in_recovery == 0 || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			pg_fatal("recovery timed out");
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain publication information: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
+				 PQntuples(res), 1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not enable subscription \"%s\": %s",
+					 dbinfo->subname, PQerrorMessage(conn));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_ctl_path = NULL;
+	char	   *pg_resetwal_path = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.sub_conninfo_str = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(opt.subscriber_dir);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				opt.sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(opt.subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Create the output directory to store any data generated by this tool */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/* Check if the standby server is ready for logical replication */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		if (!dry_run)
+			stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0], true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/* Start subscriber and wait until accepting connections */
+	pg_log_info("starting the subscriber");
+	if (!dry_run)
+		start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary",
+						   primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	if (!dry_run)
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(pg_resetwal_path, &opt);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..95eb4e70ac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,39 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..e2807d3fac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,217 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $node_c;
+my $result;
+my $slotname;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster.
+{
+	local $ENV{'INITDB_TEMPLATE'} = undef;
+
+	$node_f = PostgreSQL::Test::Cluster->new('node_f');
+	$node_f->init(allows_streaming => 'logical');
+	$node_f->start;
+}
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+$slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+log_min_messages = debug2
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Run pg_createsubscriber on the stopped node
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'target server must be running');
+
+$node_s->start;
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+$node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->append_conf(
+	'postgresql.conf', qq[
+log_min_messages = debug2
+]);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'), '--subscriber-server',
+		$node_c->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'primary server is in recovery');
+
+# Stop node C
+$node_c->teardown_node;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1')
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+ok( -d $node_s->data_dir . "/pg_createsubscriber_output.d",
+	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success"
+);
+
+# Confirm the physical replication slot has been removed
+$result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d808aad8b0..08de2bf4e6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.41.0.windows.3

v23-0002-Update-documentation.patchapplication/octet-stream; name=v23-0002-Update-documentation.patchDownload
From 8ea3d757b925ab2b3c6e06f0a72155e788430e4e Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Tue, 13 Feb 2024 10:59:47 +0000
Subject: [PATCH v23 02/13] Update documentation

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 205 +++++++++++++++-------
 1 file changed, 142 insertions(+), 63 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index f5238771b7..7cdd047d67 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -48,19 +48,99 @@ PostgreSQL documentation
   </cmdsynopsis>
  </refsynopsisdiv>
 
- <refsect1>
+ <refsect1 id="r1-app-pg_createsubscriber-1">
   <title>Description</title>
   <para>
-    <application>pg_createsubscriber</application> creates a new logical
-    replica from a physical standby server.
+   The <application>pg_createsubscriber</application> creates a new <link
+   linkend="logical-replication-subscription">subscriber</link> from a physical
+   standby server.
   </para>
 
   <para>
-   The <application>pg_createsubscriber</application> should be run at the target
-   server. The source server (known as publisher server) should accept logical
-   replication connections from the target server (known as subscriber server).
-   The target server should accept local logical replication connection.
+   The <application>pg_createsubscriber</application> must be run at the target
+   server. The source server (known as publisher server) must accept both
+   normal and logical replication connections from the target server (known as
+   subscriber server). The target server must accept normal local connections.
   </para>
+
+  <para>
+   There are some prerequisites for both the source and target instance. If
+   these are not met an error will be reported.
+  </para>
+
+  <itemizedlist>
+   <listitem>
+    <para>
+     The given target data directory must have the same system identifier than the
+     source data directory.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must be used as a physical standby.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The given database user for the target instance must have privileges for
+     creating subscriptions and using functions for replication origin.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     and <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+     configured to a value greater than the number of target databases.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The source instance must have
+     <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+     <literal>logical</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases and replication slots.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases and walsenders.
+    </para>
+   </listitem>
+  </itemizedlist>
+
+  <note>
+   <para>
+    After the successful conversion, a physical replication slot configured as
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>
+    would be removed from a primary instance.
+   </para>
+
+   <para>
+    The <application>pg_createsubscriber</application> focuses on large-scale
+    systems that contain more data than 1GB.  For smaller systems, initial data
+    synchronization of <link linkend="logical-replication">logical
+    replication</link> is recommended.
+   </para>
+  </note>
  </refsect1>
 
  <refsect1>
@@ -191,7 +271,7 @@ PostgreSQL documentation
  </refsect1>
 
  <refsect1>
-  <title>Notes</title>
+  <title>How It Works</title>
 
   <para>
    The transformation proceeds in the following steps:
@@ -200,97 +280,89 @@ PostgreSQL documentation
   <procedure>
    <step>
     <para>
-     <application>pg_createsubscriber</application> checks if the given target data
-     directory has the same system identifier than the source data directory.
-     Since it uses the recovery process as one of the steps, it starts the
-     target server as a replica from the source server. If the system
-     identifier is not the same, <application>pg_createsubscriber</application> will
-     terminate with an error.
+     Checks the target can be converted.  In particular, things listed in
+     <link linkend="r1-app-pg_createsubscriber-1">above section</link> would be
+     checked.  If these are not met <application>pg_createsubscriber</application>
+     will terminate with an error.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> checks if the target data
-     directory is used by a physical replica. Stop the physical replica if it is
-     running. One of the next steps is to add some recovery parameters that
-     requires a server start. This step avoids an error.
+     Creates a publication and a logical replication slot for each specified
+     database on the source instance.  These publications and logical replication
+     slots have generated names:
+     <quote><literal>pg_createsubscriber_%u</literal></quote> (parameters:
+     Database <parameter>oid</parameter>) for publications,
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     Database <parameter>oid</parameter>, Pid <parameter>int</parameter>) for
+     replication slots.
     </para>
    </step>
-
    <step>
     <para>
-     <application>pg_createsubscriber</application> creates one replication slot for
-     each specified database on the source server. The replication slot name
-     contains a <literal>pg_createsubscriber</literal> prefix. These replication
-     slots will be used by the subscriptions in a future step.  A temporary
-     replication slot is used to get a consistent start location. This
-     consistent LSN will be used as a stopping point in the <xref
-     linkend="guc-recovery-target-lsn"/> parameter and by the
-     subscriptions as a replication starting point. It guarantees that no
-     transaction will be lost.
+     Stops the target instance.  This is needed to add some recovery parameters
+     during the conversion.
     </para>
    </step>
-
    <step>
     <para>
-     <application>pg_createsubscriber</application> writes recovery parameters into
-     the target data directory and start the target server. It specifies a LSN
-     (consistent LSN that was obtained in the previous step) of write-ahead
-     log location up to which recovery will proceed. It also specifies
-     <literal>promote</literal> as the action that the server should take once
-     the recovery target is reached. This step finishes once the server ends
-     standby mode and is accepting read-write operations.
+     Creates a temporary replication slot to get a consistent start location.
+     The slot has generated names:
+     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
+     (parameters: Pid <parameter>int</parameter>).  Got consistent LSN will be
+     used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
+     parameter and by the subscriptions as a replication starting point. It
+     guarantees that no transaction will be lost.
+    </para>
+   </step>
+   <step>
+    <para>
+     Writes recovery parameters into the target data directory and starts the
+     target instance.  It specifies a LSN (consistent LSN that was obtained in
+     the previous step) of write-ahead log location up to which recovery will
+     proceed. It also specifies <literal>promote</literal> as the action that
+     the server should take once the recovery target is reached. This step
+     finishes once the server ends standby mode and is accepting read-write
+     operations.
     </para>
    </step>
 
    <step>
     <para>
-     Next, <application>pg_createsubscriber</application> creates one publication
-     for each specified database on the source server. Each publication
-     replicates changes for all tables in the database. The publication name
-     contains a <literal>pg_createsubscriber</literal> prefix. These publication
-     will be used by a corresponding subscription in a next step.
+     Creates a subscription for each specified database on the target instance.
+     These subscriptions have generated name:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     Database <parameter>oid</parameter>, Pid <parameter>int</parameter>).
+     These subscription have same subscription options:
+     <quote><literal>create_slot = false, copy_data = false, enabled = false</literal></quote>.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> creates one subscription for
-     each specified database on the target server. Each subscription name
-     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
-     name is identical to the subscription name. It does not copy existing data
-     from the source server. It does not create a replication slot. Instead, it
-     uses the replication slot that was created in a previous step. The
-     subscription is created but it is not enabled yet. The reason is the
-     replication progress must be set to the consistent LSN but replication
-     origin name contains the subscription oid in its name. Hence, the
-     subscription will be enabled in a separate step.
+     Sets replication progress to the consistent LSN that was obtained in a
+     previous step.  This is the exact LSN to be used as a initial location for
+     each subscription.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> sets the replication progress to
-     the consistent LSN that was obtained in a previous step. When the target
-     server started the recovery process, it caught up to the consistent LSN.
-     This is the exact LSN to be used as a initial location for each
-     subscription.
+     Enables the subscription for each specified database on the target server.
+     The subscription starts streaming from the consistent LSN.
     </para>
    </step>
 
    <step>
     <para>
-     Finally, <application>pg_createsubscriber</application> enables the subscription
-     for each specified database on the target server. The subscription starts
-     streaming from the consistent LSN.
+     Stops the standby server.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> stops the target server to change
-     its system identifier.
+     Updates a system identifier on the target server.
     </para>
    </step>
   </procedure>
@@ -300,8 +372,15 @@ PostgreSQL documentation
   <title>Examples</title>
 
   <para>
-   To create a logical replica for databases <literal>hr</literal> and
-   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+   Here is an example of using <application>pg_createsubscriber</application>.
+   Before running the command, please make sure target server is stopped.
+<screen>
+<prompt>$</prompt> <userinput>pg_ctl -D /usr/local/pgsql/data stop</userinput>
+</screen>
+
+   Then run <application>pg_createsubscriber</application>. Below tries to
+   create subscriptions for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical standby:
 <screen>
 <prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
 </screen>
-- 
2.41.0.windows.3

v23-0004-Remove-S-option-to-force-unix-domain-connection.patchapplication/octet-stream; name=v23-0004-Remove-S-option-to-force-unix-domain-connection.patchDownload
From 27203fdfb0dbae37768428d7dbe01ebb738c3bbb Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 6 Feb 2024 14:45:03 +0530
Subject: [PATCH v23 04/13] Remove -S option to force unix domain connection

With this patch removed -S option and added option for username(-U), port(-p)
and socket directory(-s) for standby. This helps to force standby to use
unix domain connection.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     | 36 ++++++--
 src/bin/pg_basebackup/pg_createsubscriber.c   | 91 ++++++++++++++-----
 .../t/041_pg_createsubscriber_standby.pl      | 33 ++++---
 3 files changed, 115 insertions(+), 45 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 9d0c6c764c..579e50a0a0 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -34,11 +34,6 @@ PostgreSQL documentation
      <arg choice="plain"><option>--publisher-server</option></arg>
     </group>
     <replaceable>connstr</replaceable>
-    <group choice="req">
-     <arg choice="plain"><option>-S</option></arg>
-     <arg choice="plain"><option>--subscriber-server</option></arg>
-    </group>
-    <replaceable>connstr</replaceable>
     <group choice="req">
      <arg choice="plain"><option>-d</option></arg>
      <arg choice="plain"><option>--database</option></arg>
@@ -179,11 +174,36 @@ PostgreSQL documentation
      </varlistentry>
 
      <varlistentry>
-      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
-      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        A port number on which the target server is listening for connections.
+        Defaults to the <envar>PGPORT</envar> environment variable, if set, or
+        a compiled-in default.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable>username</replaceable></option></term>
+      <term><option>--username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        Target's user name. Defaults to the <envar>PGUSER</envar> environment
+        variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s</option> <replaceable>dir</replaceable></term>
+      <term><option>--socketdir=</option><replaceable>dir</replaceable></term>
       <listitem>
        <para>
-        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+        A directory which locales a temporary Unix socket files. If not
+        specified, <application>pg_createsubscriber</application> tries to
+        connect via TCP/IP to <literal>localhost</literal>.
        </para>
       </listitem>
      </varlistentry>
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index b15769c75b..1ad7de9190 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -35,7 +35,9 @@ typedef struct CreateSubscriberOptions
 {
 	char	   *subscriber_dir; /* standby/subscriber data directory */
 	char	   *pub_conninfo_str;	/* publisher connection string */
-	char	   *sub_conninfo_str;	/* subscriber connection string */
+	unsigned short subport;			/* port number listen()'d by the standby */
+	char	   *subuser;			/* database user of the standby */
+	char	   *socketdir;			/* socket directory */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;			/* retain log file? */
 	int			recovery_timeout;	/* stop recovery after this time */
@@ -57,7 +59,9 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_pub_base_conninfo(char *conninfo, char **dbname);
+static char *construct_sub_conninfo(char *username, unsigned short subport,
+									char *socketdir);
 static char *get_exec_path(const char *argv0, const char *progname);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
@@ -180,7 +184,10 @@ usage(void)
 	printf(_("\nOptions:\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
 	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
-	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -p, --port=PORT                     subscriber port number\n"));
+	printf(_(" -U, --username=NAME                 subscriber user\n"));
+	printf(_(" -s, --socketdir=DIR                 socket directory to use\n"));
+	printf(_("                                     If not specified, localhost would be used\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
 	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
@@ -193,8 +200,8 @@ usage(void)
 }
 
 /*
- * Validate a connection string. Returns a base connection string that is a
- * connection string without a database name.
+ * Validate a connection string for the publisher. Returns a base connection
+ * string that is a connection string without a database name.
  *
  * Since we might process multiple databases, each database name will be
  * appended to this base connection string to provide a final connection
@@ -206,7 +213,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char **dbname)
+get_pub_base_conninfo(char *conninfo, char **dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -1593,6 +1600,40 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	destroyPQExpBuffer(str);
 }
 
+/*
+ * Construct a connection string toward a target server, from argument options.
+ *
+ * If inputs are the zero, default value would be used.
+ * - username: PGUSER environment value (it would not be parsed)
+ * - port: PGPORT environment value (it would not be parsed)
+ * - socketdir: localhost connection (unix-domain would not be used)
+ */
+static char *
+construct_sub_conninfo(char *username, unsigned short subport, char *sockdir)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	if (username)
+		appendPQExpBuffer(buf, "user=%s ", username);
+
+	if (subport != 0)
+		appendPQExpBuffer(buf, "port=%u ", subport);
+
+	if (sockdir)
+		appendPQExpBuffer(buf, "host=%s ", sockdir);
+	else
+		appendPQExpBuffer(buf, "host=localhost ");
+
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
 int
 main(int argc, char **argv)
 {
@@ -1602,7 +1643,9 @@ main(int argc, char **argv)
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
 		{"publisher-server", required_argument, NULL, 'P'},
-		{"subscriber-server", required_argument, NULL, 'S'},
+		{"port", required_argument, NULL, 'p'},
+		{"username", required_argument, NULL, 'U'},
+		{"socketdir", required_argument, NULL, 's'},
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
 		{"recovery-timeout", required_argument, NULL, 't'},
@@ -1659,7 +1702,9 @@ main(int argc, char **argv)
 	/* Default settings */
 	opt.subscriber_dir = NULL;
 	opt.pub_conninfo_str = NULL;
-	opt.sub_conninfo_str = NULL;
+	opt.subport = 0;
+	opt.subuser = NULL;
+	opt.socketdir = NULL;
 	opt.database_names = (SimpleStringList)
 	{
 		NULL, NULL
@@ -1683,7 +1728,7 @@ main(int argc, char **argv)
 
 	get_restricted_token();
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+	while ((c = getopt_long(argc, argv, "D:P:p:U:s:S:d:nrt:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1695,8 +1740,17 @@ main(int argc, char **argv)
 			case 'P':
 				opt.pub_conninfo_str = pg_strdup(optarg);
 				break;
-			case 'S':
-				opt.sub_conninfo_str = pg_strdup(optarg);
+			case 'p':
+				if ((opt.subport = atoi(optarg)) <= 0)
+					pg_fatal("invalid old port number");
+				break;
+			case 'U':
+				pfree(opt.subuser);
+				opt.subuser = pg_strdup(optarg);
+				break;
+			case 's':
+				pfree(opt.socketdir);
+				opt.socketdir = pg_strdup(optarg);
 				break;
 			case 'd':
 				/* Ignore duplicated database names */
@@ -1763,21 +1817,12 @@ main(int argc, char **argv)
 		exit(1);
 	}
 	pg_log_info("validating connection string on publisher");
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
-										  &dbname_conninfo);
+	pub_base_conninfo = get_pub_base_conninfo(opt.pub_conninfo_str,
+											  &dbname_conninfo);
 	if (pub_base_conninfo == NULL)
 		exit(1);
 
-	if (opt.sub_conninfo_str == NULL)
-	{
-		pg_log_error("no subscriber connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	pg_log_info("validating connection string on subscriber");
-	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
-	if (sub_base_conninfo == NULL)
-		exit(1);
+	sub_base_conninfo = construct_sub_conninfo(opt.subuser, opt.subport, opt.socketdir);
 
 	if (opt.database_names.head == NULL)
 	{
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index e2807d3fac..93148417db 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -66,7 +66,8 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
 		'--publisher-server', $node_p->connstr('pg1'),
-		'--subscriber-server', $node_f->connstr('pg1'),
+		'--port', $node_f->port,
+		'--host', $node_f->host,
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
@@ -78,8 +79,9 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1'), '--database',
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--host',
+		$node_s->host, '--database',
 		'pg1', '--database',
 		'pg2'
 	],
@@ -104,10 +106,11 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_c->data_dir, '--publisher-server',
-		$node_s->connstr('pg1'), '--subscriber-server',
-		$node_c->connstr('pg1'), '--database',
-		'pg1', '--database',
-		'pg2'
+		$node_s->connstr('pg1'),
+		'--port', $node_c->port,
+		'--socketdir', $node_c->host,
+		'--database', 'pg1',
+		'--database', 'pg2'
 	],
 	'primary server is in recovery');
 
@@ -124,8 +127,9 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1'), '--database',
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--socketdir',
+		$node_s->host, '--database',
 		'pg1', '--database',
 		'pg2'
 	],
@@ -141,8 +145,9 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1')
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--socketdir',
+		$node_s->host,
 	],
 	'run pg_createsubscriber without --databases');
 
@@ -152,9 +157,9 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--verbose', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1'), '--database',
-		'pg1', '--database',
+		$node_p->connstr('pg1'), '--port', $node_s->port,
+		'--socketdir', $node_s->host,
+		'--database', 'pg1', '--database',
 		'pg2'
 	],
 	'run pg_createsubscriber on node S');
-- 
2.41.0.windows.3

v23-0005-Fix-some-trivial-issues.patchapplication/octet-stream; name=v23-0005-Fix-some-trivial-issues.patchDownload
From 7084bef82fa7ac5fe205352675f1acfe1d980367 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 03:59:19 +0000
Subject: [PATCH v23 05/13] Fix some trivial issues

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 44 ++++++++++-----------
 1 file changed, 20 insertions(+), 24 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1ad7de9190..968d0ae6bd 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -387,12 +387,11 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
 				   const char *sub_base_conninfo)
 {
 	LogicalRepInfo *dbinfo;
-	SimpleStringListCell *cell;
 	int			i = 0;
 
 	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
 
-	for (cell = dbnames.head; cell; cell = cell->next)
+	for (SimpleStringListCell *cell = dbnames.head; cell; cell = cell->next)
 	{
 		char	   *conninfo;
 
@@ -469,7 +468,6 @@ get_primary_sysid(const char *conninfo)
 	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		PQclear(res);
 		disconnect_database(conn);
 		pg_fatal("could not get system identifier: %s",
 				 PQresultErrorMessage(res));
@@ -516,7 +514,7 @@ get_standby_sysid(const char *datadir)
 	pg_log_info("system identifier is %llu on subscriber",
 				(unsigned long long) sysid);
 
-	pfree(cf);
+	pg_free(cf);
 
 	return sysid;
 }
@@ -534,7 +532,6 @@ modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *o
 	struct timeval tv;
 
 	char	   *cmd_str;
-	int			rc;
 
 	pg_log_info("modifying system identifier from subscriber");
 
@@ -567,14 +564,15 @@ modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *o
 
 	if (!dry_run)
 	{
-		rc = system(cmd_str);
+		int rc = system(cmd_str);
+
 		if (rc == 0)
 			pg_log_info("subscriber successfully changed the system identifier");
 		else
 			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
 	}
 
-	pfree(cf);
+	pg_free(cf);
 }
 
 /*
@@ -584,11 +582,11 @@ modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *o
 static bool
 setup_publisher(LogicalRepInfo *dbinfo)
 {
-	PGconn	   *conn;
-	PGresult   *res;
 
 	for (int i = 0; i < num_dbs; i++)
 	{
+		PGconn	   *conn;
+		PGresult   *res;
 		char		pubname[NAMEDATALEN];
 		char		replslotname[NAMEDATALEN];
 
@@ -901,7 +899,7 @@ check_subscriber(LogicalRepInfo *dbinfo)
 		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
 		return false;
 	}
-	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
 	{
 		pg_log_error("permission denied for function \"%s\"",
 					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
@@ -990,10 +988,10 @@ check_subscriber(LogicalRepInfo *dbinfo)
 static bool
 setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
 {
-	PGconn	   *conn;
-
 	for (int i = 0; i < num_dbs; i++)
 	{
+		PGconn	   *conn;
+
 		/* Connect to subscriber. */
 		conn = connect_database(dbinfo[i].subconninfo);
 		if (conn == NULL)
@@ -1103,7 +1101,7 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
-						 slot_name, dbinfo->dbname, PQerrorMessage(conn));
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
 
 		PQclear(res);
 	}
@@ -1294,7 +1292,6 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		PQclear(res);
 		PQfinish(conn);
 		pg_fatal("could not obtain publication information: %s",
 				 PQresultErrorMessage(res));
@@ -1348,7 +1345,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		{
 			PQfinish(conn);
 			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
-					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+					 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
 		}
 	}
 
@@ -1384,7 +1381,7 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
-						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
 
 		PQclear(res);
 	}
@@ -1429,7 +1426,7 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		{
 			PQfinish(conn);
 			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
-					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+					 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
 		}
 	}
 
@@ -1465,7 +1462,7 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
 
 		PQclear(res);
 	}
@@ -1502,7 +1499,6 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		PQclear(res);
 		PQfinish(conn);
 		pg_fatal("could not obtain subscription OID: %s",
 				 PQresultErrorMessage(res));
@@ -1591,7 +1587,7 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		{
 			PQfinish(conn);
 			pg_fatal("could not enable subscription \"%s\": %s",
-					 dbinfo->subname, PQerrorMessage(conn));
+					 dbinfo->subname, PQresultErrorMessage(res));
 		}
 
 		PQclear(res);
@@ -1745,11 +1741,11 @@ main(int argc, char **argv)
 					pg_fatal("invalid old port number");
 				break;
 			case 'U':
-				pfree(opt.subuser);
+				pg_free(opt.subuser);
 				opt.subuser = pg_strdup(optarg);
 				break;
 			case 's':
-				pfree(opt.socketdir);
+				pg_free(opt.socketdir);
 				opt.socketdir = pg_strdup(optarg);
 				break;
 			case 'd':
@@ -1854,7 +1850,7 @@ main(int argc, char **argv)
 	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
 	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
 
-	/* rudimentary check for a data directory. */
+	/* Rudimentary check for a data directory */
 	if (!check_data_directory(opt.subscriber_dir))
 		exit(1);
 
@@ -1877,7 +1873,7 @@ main(int argc, char **argv)
 	/* Create the output directory to store any data generated by this tool */
 	server_start_log = setup_server_logfile(opt.subscriber_dir);
 
-	/* subscriber PID file. */
+	/* Subscriber PID file */
 	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
 
 	/*
-- 
2.41.0.windows.3

v23-0003-Add-version-check-for-standby-server.patchapplication/octet-stream; name=v23-0003-Add-version-check-for-standby-server.patchDownload
From 84bb55feeceb82e3354488dd088b88ccae6d5b89 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 14 Feb 2024 16:27:15 +0530
Subject: [PATCH v23 03/13] Add version check for standby server

Add version check for standby server
---
 doc/src/sgml/ref/pg_createsubscriber.sgml   |  6 +++++
 src/bin/pg_basebackup/pg_createsubscriber.c | 28 +++++++++++++++++++++
 2 files changed, 34 insertions(+)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 7cdd047d67..9d0c6c764c 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -125,6 +125,12 @@ PostgreSQL documentation
      databases and walsenders.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     Both the target and source instances must have same major versions with
+     <application>pg_createsubscriber</application>.
+    </para>
+   </listitem>
   </itemizedlist>
 
   <note>
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 205a835d36..b15769c75b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -23,6 +23,7 @@
 #include "common/file_perm.h"
 #include "common/logging.h"
 #include "common/restricted_token.h"
+#include "common/string.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
@@ -294,6 +295,8 @@ check_data_directory(const char *datadir)
 {
 	struct stat statbuf;
 	char		versionfile[MAXPGPATH];
+	FILE	   *ver_fd;
+	char		rawline[64];
 
 	pg_log_info("checking if directory \"%s\" is a cluster data directory",
 				datadir);
@@ -317,6 +320,31 @@ check_data_directory(const char *datadir)
 		return false;
 	}
 
+	/* Check standby server version */
+	if ((ver_fd = fopen(versionfile, "r")) == NULL)
+		pg_fatal("could not open file \"%s\" for reading: %m", versionfile);
+
+	/* Version number has to be the first line read */
+	if (!fgets(rawline, sizeof(rawline), ver_fd))
+	{
+		if (!ferror(ver_fd))
+			pg_fatal("unexpected empty file \"%s\"", versionfile);
+		else
+			pg_fatal("could not read file \"%s\": %m", versionfile);
+	}
+
+	/* Strip trailing newline and carriage return */
+	(void) pg_strip_crlf(rawline);
+
+	if (strcmp(rawline, PG_MAJORVERSION) != 0)
+	{
+		pg_log_error("standby server is of wrong version");
+		pg_log_error_detail("File \"%s\" contains \"%s\", which is not compatible with this program's version \"%s\".",
+							versionfile, rawline, PG_MAJORVERSION);
+		exit(1);
+	}
+
+	fclose(ver_fd);
 	return true;
 }
 
-- 
2.41.0.windows.3

v23-0006-Fix-cleanup-functions.patchapplication/octet-stream; name=v23-0006-Fix-cleanup-functions.patchDownload
From fdfa74d754cb0f91f047699e77480738387f2a42 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 16 Feb 2024 07:34:41 +0000
Subject: [PATCH v23 06/13] Fix cleanup functions

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 60 +++------------------
 1 file changed, 8 insertions(+), 52 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 968d0ae6bd..252d541472 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -54,7 +54,6 @@ typedef struct LogicalRepInfo
 
 	bool		made_replslot;	/* replication slot was created */
 	bool		made_publication;	/* publication was created */
-	bool		made_subscription;	/* subscription was created */
 } LogicalRepInfo;
 
 static void cleanup_objects_atexit(void);
@@ -95,7 +94,6 @@ static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo,
 									 const char *lsn);
 static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
@@ -141,22 +139,11 @@ cleanup_objects_atexit(void)
 
 	for (i = 0; i < num_dbs; i++)
 	{
-		if (dbinfo[i].made_subscription || recovery_ended)
+		if (recovery_ended)
 		{
-			conn = connect_database(dbinfo[i].subconninfo);
-			if (conn != NULL)
-			{
-				if (dbinfo[i].made_subscription)
-					drop_subscription(conn, &dbinfo[i]);
-
-				/*
-				 * Publications are created on publisher before promotion so
-				 * it might exist on subscriber after recovery ends.
-				 */
-				if (recovery_ended)
-					drop_publication(conn, &dbinfo[i]);
-				disconnect_database(conn);
-			}
+			pg_log_warning("pg_createsubscriber failed after the end of recovery");
+			pg_log_warning("Target server could not be usable as physical standby anymore.");
+			pg_log_warning_hint("You must re-create the physical standby again.");
 		}
 
 		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
@@ -404,7 +391,6 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
 		/* Fill subscriber attributes */
 		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
 		dbinfo[i].subconninfo = conninfo;
-		dbinfo[i].made_subscription = false;
 		/* Other fields will be filled later */
 
 		i++;
@@ -1430,46 +1416,12 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		}
 	}
 
-	/* for cleanup purposes */
-	dbinfo->made_subscription = true;
-
 	if (!dry_run)
 		PQclear(res);
 
 	destroyPQExpBuffer(str);
 }
 
-/*
- * Remove subscription if it couldn't finish all steps.
- */
-static void
-drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
-{
-	PQExpBuffer str = createPQExpBuffer();
-	PGresult   *res;
-
-	Assert(conn != NULL);
-
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
-				dbinfo->subname, dbinfo->dbname);
-
-	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
-
-	pg_log_debug("command is: %s", str->data);
-
-	if (!dry_run)
-	{
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
-
-		PQclear(res);
-	}
-
-	destroyPQExpBuffer(str);
-}
-
 /*
  * Sets the replication progress to the consistent LSN.
  *
@@ -1986,6 +1938,10 @@ main(int argc, char **argv)
 	/* Waiting the subscriber to be promoted */
 	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
 
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must re-create the new physical standby before continuing.");
+
 	/*
 	 * Create the subscription for each database on subscriber. It does not
 	 * enable it immediately because it needs to adjust the logical
-- 
2.41.0.windows.3

v23-0007-Fix-server_is_in_recovery.patchapplication/octet-stream; name=v23-0007-Fix-server_is_in_recovery.patchDownload
From 266c4db08b2b0192290c9484801e98508a207c8b Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:12:32 +0000
Subject: [PATCH v23 07/13] Fix server_is_in_recovery

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 25 +++++++--------------
 1 file changed, 8 insertions(+), 17 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 252d541472..ea4eb7e621 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -73,7 +73,7 @@ static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
 static void modify_subscriber_sysid(const char *pg_resetwal_path,
 									CreateSubscriberOptions *opt);
-static int	server_is_in_recovery(PGconn *conn);
+static bool	server_is_in_recovery(PGconn *conn);
 static bool check_publisher(LogicalRepInfo *dbinfo);
 static bool setup_publisher(LogicalRepInfo *dbinfo);
 static bool check_subscriber(LogicalRepInfo *dbinfo);
@@ -651,7 +651,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
  * If the answer is yes, it returns 1, otherwise, returns 0. If an error occurs
  * while executing the query, it returns -1.
  */
-static int
+static bool
 server_is_in_recovery(PGconn *conn)
 {
 	PGresult   *res;
@@ -660,22 +660,13 @@ server_is_in_recovery(PGconn *conn)
 	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		PQclear(res);
-		pg_log_error("could not obtain recovery progress");
-		return -1;
-	}
+		pg_fatal("could not obtain recovery progress");
 
 	ret = strcmp("t", PQgetvalue(res, 0, 0));
 
 	PQclear(res);
 
-	if (ret == 0)
-		return 1;
-	else if (ret > 0)
-		return 0;
-	else
-		return -1;				/* should not happen */
+	return ret == 0;
 }
 
 /*
@@ -704,7 +695,7 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * If the primary server is in recovery (i.e. cascading replication),
 	 * objects (publication) cannot be created because it is read only.
 	 */
-	if (server_is_in_recovery(conn) == 1)
+	if (server_is_in_recovery(conn))
 		pg_fatal("primary server cannot be in recovery");
 
 	/*------------------------------------------------------------------------
@@ -845,7 +836,7 @@ check_subscriber(LogicalRepInfo *dbinfo)
 		exit(1);
 
 	/* The target server must be a standby */
-	if (server_is_in_recovery(conn) == 0)
+	if (!server_is_in_recovery(conn))
 	{
 		pg_log_error("The target server is not a standby");
 		return false;
@@ -1223,7 +1214,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 
 	for (;;)
 	{
-		int			in_recovery;
+		bool			in_recovery;
 
 		in_recovery = server_is_in_recovery(conn);
 
@@ -1231,7 +1222,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 		 * Does the recovery process finish? In dry run mode, there is no
 		 * recovery mode. Bail out as the recovery process has ended.
 		 */
-		if (in_recovery == 0 || dry_run)
+		if (!in_recovery || dry_run)
 		{
 			status = POSTMASTER_READY;
 			recovery_ended = true;
-- 
2.41.0.windows.3

v23-0008-Avoid-possible-null-report.patchapplication/octet-stream; name=v23-0008-Avoid-possible-null-report.patchDownload
From 5ea134e6e74bed7004539bbd87e96c6cfa1d0a8b Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:20:00 +0000
Subject: [PATCH v23 08/13] Avoid possible null report

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index ea4eb7e621..f10e8002c6 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -921,7 +921,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 				 max_lrworkers);
 	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
 	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
-	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
 
 	PQclear(res);
 
-- 
2.41.0.windows.3

v23-0009-prohibit-to-reuse-publications.patchapplication/octet-stream; name=v23-0009-prohibit-to-reuse-publications.patchDownload
From 878e99da843da4911e71a293ac34ccd228f19e20 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:32:35 +0000
Subject: [PATCH v23 09/13] prohibit to reuse publications

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 38 +++++++--------------
 1 file changed, 12 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index f10e8002c6..e88b29ea3e 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1264,7 +1264,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	/* Check if the publication needs to be created */
 	appendPQExpBuffer(str,
-					  "SELECT puballtables FROM pg_catalog.pg_publication "
+					  "SELECT count(1) FROM pg_catalog.pg_publication "
 					  "WHERE pubname = '%s'",
 					  dbinfo->pubname);
 	res = PQexec(conn, str->data);
@@ -1275,34 +1275,20 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 				 PQresultErrorMessage(res));
 	}
 
-	if (PQntuples(res) == 1)
+	if (atoi(PQgetvalue(res, 0, 0)) == 1)
 	{
 		/*
-		 * If publication name already exists and puballtables is true, let's
-		 * use it. A previous run of pg_createsubscriber must have created
-		 * this publication. Bail out.
+		 * Unfortunately, if it reaches this code path, it will always
+		 * fail (unless you decide to change the existing publication
+		 * name). That's bad but it is very unlikely that the user will
+		 * choose a name with pg_createsubscriber_ prefix followed by the
+		 * exact database oid in which puballtables is false.
 		 */
-		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
-		{
-			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
-			return;
-		}
-		else
-		{
-			/*
-			 * Unfortunately, if it reaches this code path, it will always
-			 * fail (unless you decide to change the existing publication
-			 * name). That's bad but it is very unlikely that the user will
-			 * choose a name with pg_createsubscriber_ prefix followed by the
-			 * exact database oid in which puballtables is false.
-			 */
-			pg_log_error("publication \"%s\" does not replicate changes for all tables",
-						 dbinfo->pubname);
-			pg_log_error_hint("Consider renaming this publication.");
-			PQclear(res);
-			PQfinish(conn);
-			exit(1);
-		}
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication.");
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
 	}
 
 	PQclear(res);
-- 
2.41.0.windows.3

v23-0010-Make-the-ERROR-handling-more-consistent.patchapplication/octet-stream; name=v23-0010-Make-the-ERROR-handling-more-consistent.patchDownload
From f6de6da085a6d197916c1b2ca0674cc6257d8386 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:42:17 +0000
Subject: [PATCH v23 10/13] Make the ERROR handling more consistent

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 38 +++------------------
 1 file changed, 5 insertions(+), 33 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index e88b29ea3e..f5ccd479b6 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -453,18 +453,12 @@ get_primary_sysid(const char *conninfo)
 
 	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		disconnect_database(conn);
 		pg_fatal("could not get system identifier: %s",
 				 PQresultErrorMessage(res));
-	}
+
 	if (PQntuples(res) != 1)
-	{
-		PQclear(res);
-		disconnect_database(conn);
 		pg_fatal("could not get system identifier: got %d rows, expected %d row",
 				 PQntuples(res), 1);
-	}
 
 	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
 
@@ -775,8 +769,6 @@ check_publisher(LogicalRepInfo *dbinfo)
 		{
 			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
 						 PQntuples(res), 1);
-			pg_free(primary_slot_name); /* it is not being used. */
-			primary_slot_name = NULL;
 			return false;
 		}
 		else
@@ -1269,11 +1261,8 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 					  dbinfo->pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		PQfinish(conn);
 		pg_fatal("could not obtain publication information: %s",
 				 PQresultErrorMessage(res));
-	}
 
 	if (atoi(PQgetvalue(res, 0, 0)) == 1)
 	{
@@ -1286,8 +1275,6 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		 */
 		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
 		pg_log_error_hint("Consider renaming this publication.");
-		PQclear(res);
-		PQfinish(conn);
 		exit(1);
 	}
 
@@ -1305,12 +1292,10 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
 					 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
-		}
 	}
 
 	/* for cleanup purposes */
@@ -1386,12 +1371,10 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
 					 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
-		}
 	}
 
 	if (!dry_run)
@@ -1428,19 +1411,12 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		PQfinish(conn);
 		pg_fatal("could not obtain subscription OID: %s",
 				 PQresultErrorMessage(res));
-	}
 
 	if (PQntuples(res) != 1 && !dry_run)
-	{
-		PQclear(res);
-		PQfinish(conn);
 		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
 				 PQntuples(res), 1);
-	}
 
 	if (dry_run)
 	{
@@ -1475,12 +1451,10 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
 					 dbinfo->subname, PQresultErrorMessage(res));
-		}
 
 		PQclear(res);
 	}
@@ -1513,12 +1487,10 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not enable subscription \"%s\": %s",
 					 dbinfo->subname, PQresultErrorMessage(res));
-		}
 
 		PQclear(res);
 	}
-- 
2.41.0.windows.3

v23-0012-Avoid-running-pg_createsubscriber-on-standby-whi.patchapplication/octet-stream; name=v23-0012-Avoid-running-pg_createsubscriber-on-standby-whi.patchDownload
From 304d5b9e9fd6f95d8fe88ad74aab96f6d80c4336 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 20 Feb 2024 14:49:56 +0530
Subject: [PATCH v23 12/13] Avoid running pg_createsubscriber on standby which
 is primary to other node

pg_createsubscriber will throw error when run on a node which is a standby
but also primary to other node.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index f5ccd479b6..26ce91f58b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -834,6 +834,26 @@ check_subscriber(LogicalRepInfo *dbinfo)
 		return false;
 	}
 
+	/*
+	 * The target server must not be primary for other server. Because the
+	 * pg_createsubscriber would modify the system_identifier at the end of
+	 * run, but walreceiver of another standby would not accept the difference.
+	 */
+	res = PQexec(conn, 
+				 "SELECT count(1) from pg_catalog.pg_stat_activity where backend_type = 'walsender'");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+			pg_log_error("could not obtain walsender information");
+			return false;
+	}
+
+	if (atoi(PQgetvalue(res, 0, 0)) != 0)
+	{
+			pg_log_error("the target server is primary to other server");
+			return false;
+	}
+
 	/*
 	 * Subscriptions can only be created by roles that have the privileges of
 	 * pg_create_subscription role and CREATE privileges on the specified
-- 
2.41.0.windows.3

v23-0011-Update-test-codes.patchapplication/octet-stream; name=v23-0011-Update-test-codes.patchDownload
From 9af968e92d8989e0a1aee3072cf316c559755c27 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 16 Feb 2024 09:04:47 +0000
Subject: [PATCH v23 11/13] Update test codes

---
 .../t/040_pg_createsubscriber.pl              |   2 +-
 .../t/041_pg_createsubscriber_standby.pl      | 197 +++++++++---------
 2 files changed, 105 insertions(+), 94 deletions(-)

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 95eb4e70ac..65eba6f623 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -5,7 +5,7 @@
 #
 
 use strict;
-use warnings;
+use warnings  FATAL => 'all';
 use PostgreSQL::Test::Utils;
 use Test::More;
 
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 93148417db..06ef05d5e8 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -4,26 +4,23 @@
 # Test using a standby server as the subscriber.
 
 use strict;
-use warnings;
+use warnings FATAL => 'all';
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
 
-my $node_p;
-my $node_f;
-my $node_s;
-my $node_c;
-my $result;
-my $slotname;
-
 # Set up node P as primary
-$node_p = PostgreSQL::Test::Cluster->new('node_p');
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
 $node_p->init(allows_streaming => 'logical');
 $node_p->start;
 
-# Set up node F as about-to-fail node
-# Force it to initialize a new cluster instead of copying a
-# previously initdb'd cluster.
+# ------------------------------
+# Check pg_createsubscriber fails when the target server is not a
+# standby of the source.
+#
+# Set up node F as about-to-fail node. Force it to initialize a new cluster
+# instead of copying a previously initdb'd cluster.
+my $node_f;
 {
 	local $ENV{'INITDB_TEMPLATE'} = undef;
 
@@ -32,112 +29,91 @@ $node_p->start;
 	$node_f->start;
 }
 
-# On node P
-# - create databases
-# - create test tables
-# - insert a row
-# - create a physical replication slot
-$node_p->safe_psql(
-	'postgres', q(
-	CREATE DATABASE pg1;
-	CREATE DATABASE pg2;
-));
-$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
-$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
-$slotname = 'physical_slot';
-$node_p->safe_psql('pg2',
-	"SELECT pg_create_physical_replication_slot('$slotname')");
+# Run pg_createsubscriber on about-to-fail node F
+command_checks_all(
+	[
+		'pg_createsubscriber', '--verbose', '--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('postgres'),
+		'--port', $node_f->port, '--socketdir', $node_f->host,
+		'--database', 'postgres'
+	],
+	1,
+	[qr//],
+	[
+		qr/subscriber data directory is not a copy of the source database cluster/
+	],
+	'subscriber data directory is not a copy of the source database cluster');
 
+# ------------------------------
+# Check pg_createsubscriber fails when the target server is not running
+#
 # Set up node S as standby linking to node P
 $node_p->backup('backup_1');
-$node_s = PostgreSQL::Test::Cluster->new('node_s');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
 $node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
-$node_s->append_conf(
-	'postgresql.conf', qq[
-log_min_messages = debug2
-primary_slot_name = '$slotname'
-]);
 $node_s->set_standby_mode();
 
-# Run pg_createsubscriber on about-to-fail node F
-command_fails(
-	[
-		'pg_createsubscriber', '--verbose',
-		'--pgdata', $node_f->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
-		'--port', $node_f->port,
-		'--host', $node_f->host,
-		'--database', 'pg1',
-		'--database', 'pg2'
-	],
-	'subscriber data directory is not a copy of the source database cluster');
-
 # Run pg_createsubscriber on the stopped node
-command_fails(
+command_checks_all(
 	[
-		'pg_createsubscriber', '--verbose',
-		'--dry-run', '--pgdata',
-		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--port',
-		$node_s->port, '--host',
-		$node_s->host, '--database',
-		'pg1', '--database',
-		'pg2'
+		'pg_createsubscriber', '--verbose', '--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('postgres'),
+		'--port', $node_s->port, '--socketdir', $node_s->host,
+		'--database', 'postgres'
 	],
+	1,
+	[qr//],
+	[qr/standby is not running/],
 	'target server must be running');
 
 $node_s->start;
 
+# ------------------------------
+# Check pg_createsubscriber fails when the target server is a member of
+# the cascading standby.
+#
 # Set up node C as standby linking to node S
 $node_s->backup('backup_2');
-$node_c = PostgreSQL::Test::Cluster->new('node_c');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
 $node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
-$node_c->append_conf(
-	'postgresql.conf', qq[
-log_min_messages = debug2
-]);
 $node_c->set_standby_mode();
 $node_c->start;
 
 # Run pg_createsubscriber on node C (P -> S -> C)
-command_fails(
+command_checks_all(
 	[
-		'pg_createsubscriber', '--verbose',
-		'--dry-run', '--pgdata',
-		$node_c->data_dir, '--publisher-server',
-		$node_s->connstr('pg1'),
-		'--port', $node_c->port,
-		'--socketdir', $node_c->host,
-		'--database', 'pg1',
-		'--database', 'pg2'
+		'pg_createsubscriber', '--verbose', '--pgdata', $node_c->data_dir,
+		'--publisher-server', $node_s->connstr('postgres'),
+		'--port', $node_c->port, '--socketdir', $node_c->host,
+		'--database', 'postgres'
 	],
-	'primary server is in recovery');
+	1,
+	[qr//],
+	[qr/primary server cannot be in recovery/],
+	'target server must be running');
 
 # Stop node C
-$node_c->teardown_node;
-
-# Insert another row on node P and wait node S to catch up
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
-$node_p->wait_for_replay_catchup($node_s);
+$node_c->stop;
 
-# dry run mode on node S
+# ------------------------------
+# Check successful dry-run
+#
+# Dry run mode on node S
 command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--port',
-		$node_s->port, '--socketdir',
-		$node_s->host, '--database',
-		'pg1', '--database',
-		'pg2'
+		$node_p->connstr('postgres'),
+		'--port', $node_s->port,
+		'--socketdir', $node_s->host,
+		'--database', 'postgres'
 	],
 	'run pg_createsubscriber --dry-run on node S');
 
 # Check if node S is still a standby
-is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
-	't', 'standby is in recovery');
+my $result = $node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()');
+is($result, 't', 'standby is in recovery');
 
 # pg_createsubscriber can run without --databases option
 command_ok(
@@ -145,12 +121,39 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--port',
+		$node_p->connstr('postgres'), '--port',
 		$node_s->port, '--socketdir',
 		$node_s->host,
 	],
 	'run pg_createsubscriber without --databases');
 
+# ------------------------------
+# Check successful conversion
+#
+# Prepare databases and a physical replication slot
+my $slotname = 'physical_slot';
+$node_p->safe_psql(
+	'postgres', qq[
+		CREATE DATABASE pg1;
+		CREATE DATABASE pg2;
+		SELECT pg_create_physical_replication_slot('$slotname');
+]);
+
+# Use the created slot for physical replication
+$node_s->append_conf('postgresql.conf', "primary_slot_name = $slotname");
+$node_s->reload;
+
+# Prepare tables and initial data on pg1 and pg2
+$node_p->safe_psql(
+	'pg1', qq[
+		CREATE TABLE tbl1 (a text);
+		INSERT INTO tbl1 VALUES('first row');
+		INSERT INTO tbl1 VALUES('second row')
+]);
+$node_p->safe_psql('pg2', "CREATE TABLE tbl2 (a text);");
+
+$node_p->wait_for_replay_catchup($node_s);
+
 # Run pg_createsubscriber on node S
 command_ok(
 	[
@@ -176,15 +179,23 @@ is($result, qq(0),
 	'the physical replication slot used as primary_slot_name has been removed'
 );
 
-# Insert rows on P
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
-$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
-
 # PID sets to undefined because subscriber was stopped behind the scenes.
 # Start subscriber
 $node_s->{_pid} = undef;
 $node_s->start;
 
+# Confirm two subscriptions has been created
+$result = $node_s->safe_psql('postgres',
+	"SELECT count(distinct subdbid) FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_';"
+);
+is($result, qq(2),
+	'Subscriptions has been created to all the specified databases'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
 # Get subscription names
 $result = $node_s->safe_psql(
 	'postgres', qq(
@@ -214,9 +225,9 @@ my $sysid_s = $node_s->safe_psql('postgres',
 	'SELECT system_identifier FROM pg_control_system()');
 ok($sysid_p != $sysid_s, 'system identifier was changed');
 
-# clean up
-$node_p->teardown_node;
-$node_s->teardown_node;
-$node_f->teardown_node;
+# Clean up
+$node_p->stop;
+$node_s->stop;
+$node_f->stop;
 
 done_testing();
-- 
2.41.0.windows.3

v23-0013-Consider-temporary-slot-to-check-max_replication.patchapplication/octet-stream; name=v23-0013-Consider-temporary-slot-to-check-max_replication.patchDownload
From 9bf0c3c4bea46f211d2a1d5683c921baf8ea91db Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 20 Feb 2024 14:53:55 +0530
Subject: [PATCH v23 13/13] Consider temporary slot to check
 max_replication_slots in primary

While checking for max_replication_slots in primary we should consider
the temporary slot as well.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 26ce91f58b..4a28dfb81c 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -698,7 +698,8 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * we should check it to make sure it won't fail.
 	 *
 	 * - wal_level = logical
-	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_replication_slots >= current + number of dbs to be converted +
+	 * 							  temporary slot to be created
 	 * - max_wal_senders >= current + number of dbs to be converted
 	 * -----------------------------------------------------------------------
 	 */
@@ -786,12 +787,12 @@ check_publisher(LogicalRepInfo *dbinfo)
 		return false;
 	}
 
-	if (max_repslots - cur_repslots < num_dbs)
+	if (max_repslots - cur_repslots < num_dbs + 1)
 	{
 		pg_log_error("publisher requires %d replication slots, but only %d remain",
-					 num_dbs, max_repslots - cur_repslots);
+					 num_dbs + 1, max_repslots - cur_repslots);
 		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
-						  cur_repslots + num_dbs);
+						  cur_repslots + num_dbs + 1);
 		return false;
 	}
 
-- 
2.41.0.windows.3

#154vignesh C
vignesh21@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#141)
Re: speed up a logical replica setup

On Mon, 19 Feb 2024 at 11:15, Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear hackers,

Since it may be useful, I will post top-up patch on Monday, if there are no
updating.

And here are top-up patches. Feel free to check and include.

v22-0001: Same as v21-0001.
=== rebased patches ===
v22-0002: Update docs per recent changes. Same as v20-0002.
v22-0003: Add check versions of the target. Extracted from v20-0003.
v22-0004: Remove -S option. Mostly same as v20-0009, but commit massage was
slightly changed.
=== Newbie ===
V22-0005: Addressed my comments which seems to be trivial[1].
Comments #1, 3, 4, 8, 10, 14, 17 were addressed here.
v22-0006: Consider the scenario when commands are failed after the recovery.
drop_subscription() is removed and some messages are added per [2].
V22-0007: Revise server_is_in_recovery() per [1]. Comments #5, 6, 7, were addressed here.
V22-0008: Fix a strange report when physical_primary_slot is null. Per comment #9 [1].
V22-0009: Prohibit reuse publications when it has already existed. Per comments #11 and 12 [1].
V22-0010: Avoid to call PQclear()/PQfinish()/pg_free() if the process exits soon. Per comment #15 [1].
V22-0011: Update testcode. Per comments #17- [1].

Few comments on the tests:
1) If the dry run was successful because of some issue then the server
will be stopped so we can check for "pg_ctl status" if the server is
running otherwise the connection will fail in this case. Another way
would be to check if it does not have "postmaster was stopped"
messages in the stdout.
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+       't', 'standby is in recovery');
2) Can we add verification of  "postmaster was stopped" messages in
the stdout for dry run without --databases testcase
+# pg_createsubscriber can run without --databases option
+command_ok(
+       [
+               'pg_createsubscriber', '--verbose',
+               '--dry-run', '--pgdata',
+               $node_s->data_dir, '--publisher-server',
+               $node_p->connstr('pg1'), '--subscriber-server',
+               $node_s->connstr('pg1')
+       ],
+       'run pg_createsubscriber without --databases');
+
3) This message "target server must be running" seems to be wrong,
should it be cannot specify cascading replicating standby as standby
node(this is for v22-0011 patch :
+               'pg_createsubscriber', '--verbose', '--pgdata',
$node_c->data_dir,
+               '--publisher-server', $node_s->connstr('postgres'),
+               '--port', $node_c->port, '--socketdir', $node_c->host,
+               '--database', 'postgres'
        ],
-       'primary server is in recovery');
+       1,
+       [qr//],
+       [qr/primary server cannot be in recovery/],
+       'target server must be running');
4) Should this be "Wait for subscriber to catch up"
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
5) Should this be 'Subscriptions has been created on all the specified
databases'
+);
+is($result, qq(2),
+       'Subscriptions has been created to all the specified databases'
+);

6) Add test to verify current_user is not a member of
ROLE_PG_CREATE_SUBSCRIPTION, has no create permissions, has no
permissions to execution replication origin advance functions

7) Add tests to verify insufficient max_logical_replication_workers,
max_replication_slots and max_worker_processes for the subscription
node

8) Add tests to verify invalid configuration of wal_level,
max_replication_slots and max_wal_senders for the publisher node

9) We can use the same node name in comment and for the variable
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
10) Similarly we can use node_f instead of F in the comments.
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster.
+{
+       local $ENV{'INITDB_TEMPLATE'} = undef;
+
+       $node_f = PostgreSQL::Test::Cluster->new('node_f');
+       $node_f->init(allows_streaming => 'logical');
+       $node_f->start;

Regards,
Vignesh

#155Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Shlok Kyal (#143)
RE: speed up a logical replica setup

Dear Shlok,

Hi,

I have reviewed the v21 patch. And found an issue.

Initially I started the standby server with a new postgresql.conf file
(not the default postgresql.conf that is present in the instance).
pg_ctl -D ../standby start -o "-c config_file=/new_path/postgresql.conf"

And I have made 'max_replication_slots = 1' in new postgresql.conf and
made 'max_replication_slots = 0' in the default postgresql.conf file.
Now when we run pg_createsubscriber on standby we get error:
pg_createsubscriber: error: could not set replication progress for the
subscription "pg_createsubscriber_5_242843": ERROR: cannot query or
manipulate replication origin when max_replication_slots = 0
NOTICE: dropped replication slot "pg_createsubscriber_5_242843" on publisher
pg_createsubscriber: error: could not drop publication
"pg_createsubscriber_5" on database "postgres": ERROR: publication
"pg_createsubscriber_5" does not exist
pg_createsubscriber: error: could not drop replication slot
"pg_createsubscriber_5_242843" on database "postgres": ERROR:
replication slot "pg_createsubscriber_5_242843" does not exist

I observed that when we run the pg_createsubscriber command, it will
stop the standby instance (the non-default postgres configuration) and
restart the standby instance which will now be started with default
postgresql.conf, where the 'max_replication_slot = 0' and
pg_createsubscriber will now fail with the error given above.
I have added the script file with which we can reproduce this issue.
Also similar issues can happen with other configurations such as port, etc.

Possible. So the issue is that GUC settings might be changed after the restart
so that the verification phase may not be enough. There are similar GUCs in [1]https://www.postgresql.org/docs/current/storage-file-layout.html
and they may have similar issues. E.g., if "hba_file" is set when the server
started, the file cannot be seen after the restart, so pg_createsubscriber may
not connect to it anymore.

The possible solution would be
1) allow to run pg_createsubscriber if standby is initially stopped .
I observed that pg_logical_createsubscriber also uses this approach.
2) read GUCs via SHOW command and restore them when server restarts

I also prefer the first solution.
Another reason why the standby should be stopped is for backup purpose.
Basically, the standby instance should be saved before running pg_createsubscriber.
An easiest way is hard-copy, and the postmaster should be stopped at that time.
I felt it is better that users can run the command immediately later the copying.
Thought?

[1]: https://www.postgresql.org/docs/current/storage-file-layout.html

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#156Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: vignesh C (#147)
18 attachment(s)
RE: speed up a logical replica setup

Dear Vignesh,

Since no one updates, here are new patch set.
Note that comments from Peter E. was not addressed because
Euler seemed to have already fixed.

3) Error conditions is verbose mode has invalid error message like
"out of memory" messages like in below:
pg_createsubscriber: waiting the postmaster to reach the consistent state
pg_createsubscriber: postmaster reached the consistent state
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: creating subscription
"pg_createsubscriber_5_278343" on database "postgres"
pg_createsubscriber: error: could not create subscription
"pg_createsubscriber_5_278343" on database "postgres": out of memory

Descriptions were added.

4) In error cases we try to drop this publication again resulting in error:
+               /*
+                * Since the publication was created before the
consistent LSN, it is
+                * available on the subscriber when the physical
replica is promoted.
+                * Remove publications from the subscriber because it
has no use.
+                */
+               drop_publication(conn, &dbinfo[i]);

Which throws these errors(because of drop publication multiple times):
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: error: could not drop publication
"pg_createsubscriber_5" on database "postgres": ERROR: publication
"pg_createsubscriber_5" does not exist
pg_createsubscriber: dropping publication "pg_createsubscriber_5" on
database "postgres"
pg_createsubscriber: dropping the replication slot
"pg_createsubscriber_5_278343" on database "postgres"

Changed to ... IF EXISTS. I thought your another proposal looked not good
because if the flag was turned off, the publication on publisher node could
not be dropped.

5) In error cases, wait_for_end_recovery waits even though it has
identified that the replication between primary and standby is
stopped:
+/*
+ * Is recovery still in progress?
+ * If the answer is yes, it returns 1, otherwise, returns 0. If an error occurs
+ * while executing the query, it returns -1.
+ */
+static int
+server_is_in_recovery(PGconn *conn)
+{
+       PGresult   *res;
+       int                     ret;
+
+       res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               PQclear(res);
+               pg_log_error("could not obtain recovery progress");
+               return -1;
+       }
+

You can simulate this by stopping the primary just before
wait_for_end_recovery and you will see these error messages, but
pg_createsubscriber will continue to wait:
pg_createsubscriber: error: could not obtain recovery progress
pg_createsubscriber: error: could not obtain recovery progress
pg_createsubscriber: error: could not obtain recovery progress
pg_createsubscriber: error: could not obtain recovery progress

Based on idea from Euler, I roughly implemented. Thought?

0001-0013 were not changed from the previous version.

V24-0014: addressed your comment in the replied e-mail.
V24-0015: Add disconnect_database() again, per [3]/messages/by-id/202402201053.6jjvdrm7kahf@alvherre.pgsql
V24-0016: addressed your comment in [4]/messages/by-id/CALDaNm2qHuZZvh6ym6OM367RfozU7RaaRDSm=F8M3SNrcQG2pQ@mail.gmail.com.
V24-0017: addressed your comment in [5]/messages/by-id/CALDaNm3Q5W=EvphDjHA1n8ii5fv2DvxVShSmQLNFgeiHsOUwPg@mail.gmail.com.
V24-0018: addressed your comment in [6]/messages/by-id/CALDaNm1M73ds0GBxX-XZX56f1D+GPojeCCwo-DLTVnfu8DMAvw@mail.gmail.com.

[1]: /messages/by-id/3ee79f2c-e8b3-4342-857c-a31b87e1afda@eisentraut.org
[2]: /messages/by-id/CALDaNm1ocVQmWhUJqxJDmR8N=CTbrH5GCdFU72ywnVRV6dND2A@mail.gmail.com
[3]: /messages/by-id/202402201053.6jjvdrm7kahf@alvherre.pgsql
[4]: /messages/by-id/CALDaNm2qHuZZvh6ym6OM367RfozU7RaaRDSm=F8M3SNrcQG2pQ@mail.gmail.com
[5]: /messages/by-id/CALDaNm3Q5W=EvphDjHA1n8ii5fv2DvxVShSmQLNFgeiHsOUwPg@mail.gmail.com
[6]: /messages/by-id/CALDaNm1M73ds0GBxX-XZX56f1D+GPojeCCwo-DLTVnfu8DMAvw@mail.gmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

v24-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchapplication/octet-stream; name=v24-0001-Creates-a-new-logical-replica-from-a-standby-ser.patchDownload
From 8c4c5f6702d8fb312eb62d863f37fb4dad0bfe01 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v24 01/18] Creates a new logical replica from a standby server

A new tool called pg_createsubscriber can convert a physical replica
into a logical replica. It runs on the target server and should be able
to connect to the source server (publisher) and the target server
(subscriber).

The conversion requires a few steps. Check if the target data directory
has the same system identifier than the source data directory. Stop the
target server if it is running as a standby server. Create one
replication slot per specified database on the source server. One
additional replication slot is created at the end to get the consistent
LSN (This consistent LSN will be used as (a) a stopping point for the
recovery process and (b) a starting point for the subscriptions). Write
recovery parameters into the target data directory and start the target
server (Wait until the target server is promoted). Create one
publication (FOR ALL TABLES) per specified database on the source
server. Create one subscription per specified database on the target
server (Use replication slot and publication created in a previous step.
Don't enable the subscriptions yet). Sets the replication progress to
the consistent LSN that was got in a previous step. Enable the
subscription for each specified database on the target server.  Stop the
target server. Change the system identifier from the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  320 +++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |    8 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 1972 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |   39 +
 .../t/041_pg_createsubscriber_standby.pl      |  217 ++
 src/tools/pgindent/typedefs.list              |    2 +
 10 files changed, 2579 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
 create mode 100644 src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..a2b5eea0e0 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -214,6 +214,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
 <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
 <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgVerifyBackup     SYSTEM "pg_verifybackup.sgml">
 <!ENTITY pgtestfsync        SYSTEM "pgtestfsync.sgml">
 <!ENTITY pgtesttiming       SYSTEM "pgtesttiming.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f5238771b7
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,320 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-S</option></arg>
+     <arg choice="plain"><option>--subscriber-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> should be run at the target
+   server. The source server (known as publisher server) should accept logical
+   replication connections from the target server (known as subscriber server).
+   The target server should accept local logical replication connection.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The transformation proceeds in the following steps:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the given target data
+     directory has the same system identifier than the source data directory.
+     Since it uses the recovery process as one of the steps, it starts the
+     target server as a replica from the source server. If the system
+     identifier is not the same, <application>pg_createsubscriber</application> will
+     terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> checks if the target data
+     directory is used by a physical replica. Stop the physical replica if it is
+     running. One of the next steps is to add some recovery parameters that
+     requires a server start. This step avoids an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one replication slot for
+     each specified database on the source server. The replication slot name
+     contains a <literal>pg_createsubscriber</literal> prefix. These replication
+     slots will be used by the subscriptions in a future step.  A temporary
+     replication slot is used to get a consistent start location. This
+     consistent LSN will be used as a stopping point in the <xref
+     linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication starting point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> writes recovery parameters into
+     the target data directory and start the target server. It specifies a LSN
+     (consistent LSN that was obtained in the previous step) of write-ahead
+     log location up to which recovery will proceed. It also specifies
+     <literal>promote</literal> as the action that the server should take once
+     the recovery target is reached. This step finishes once the server ends
+     standby mode and is accepting read-write operations.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Next, <application>pg_createsubscriber</application> creates one publication
+     for each specified database on the source server. Each publication
+     replicates changes for all tables in the database. The publication name
+     contains a <literal>pg_createsubscriber</literal> prefix. These publication
+     will be used by a corresponding subscription in a next step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> creates one subscription for
+     each specified database on the target server. Each subscription name
+     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
+     name is identical to the subscription name. It does not copy existing data
+     from the source server. It does not create a replication slot. Instead, it
+     uses the replication slot that was created in a previous step. The
+     subscription is created but it is not enabled yet. The reason is the
+     replication progress must be set to the consistent LSN but replication
+     origin name contains the subscription oid in its name. Hence, the
+     subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> sets the replication progress to
+     the consistent LSN that was obtained in a previous step. When the target
+     server started the recovery process, it caught up to the consistent LSN.
+     This is the exact LSN to be used as a initial location for each
+     subscription.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Finally, <application>pg_createsubscriber</application> enables the subscription
+     for each specified database on the target server. The subscription starts
+     streaming from the consistent LSN.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     <application>pg_createsubscriber</application> stops the target server to change
+     its system identifier.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
    &pgCtl;
    &pgResetwal;
    &pgRewind;
+   &pgCreateSubscriber;
    &pgtestfsync;
    &pgtesttiming;
    &pgupgrade;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..ded434b683 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,7 +44,7 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_receivewal pg_recvlogical pg_createsubscriber
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
@@ -55,10 +55,14 @@ pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake
 pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_recvlogical.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
@@ -67,10 +71,12 @@ uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 
 clean distclean:
 	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
 		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+		pg_createsubscriber$(X) pg_createsubscriber.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..345a2d6fcd 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -75,6 +75,23 @@ pg_recvlogical = executable('pg_recvlogical',
 )
 bin_targets += pg_recvlogical
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
 tests += {
   'name': 'pg_basebackup',
   'sd': meson.current_source_dir(),
@@ -89,6 +106,8 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
+      't/041_pg_createsubscriber_standby.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..205a835d36
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,1972 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	PGS_OUTPUT_DIR	"pg_createsubscriber_output.d"
+
+/* Command-line options */
+typedef struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *sub_conninfo_str;	/* subscriber connection string */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+} CreateSubscriberOptions;
+
+typedef struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name / replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+	bool		made_subscription;	/* subscription was created */
+} LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_exec_path(const char *argv0, const char *progname);
+static bool check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+										  const char *pub_base_conninfo,
+										  const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo);
+static void disconnect_database(PGconn *conn);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path,
+									CreateSubscriberOptions *opt);
+static int	server_is_in_recovery(PGconn *conn);
+static bool check_publisher(LogicalRepInfo *dbinfo);
+static bool setup_publisher(LogicalRepInfo *dbinfo);
+static bool check_subscriber(LogicalRepInfo *dbinfo);
+static bool setup_subscriber(LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn,
+											 LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void start_standby_server(const char *pg_ctl_path, const char *datadir,
+								 const char *logfile);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc, int action);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+								  CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static bool recovery_ended = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	for (i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_subscription || recovery_ended)
+		{
+			conn = connect_database(dbinfo[i].subconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_subscription)
+					drop_subscription(conn, &dbinfo[i]);
+
+				/*
+				 * Publications are created on publisher before promotion so
+				 * it might exist on subscriber after recovery ends.
+				 */
+				if (recovery_ended)
+					drop_publication(conn, &dbinfo[i]);
+				disconnect_database(conn);
+			}
+		}
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn);
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static bool
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_log_error("data directory \"%s\" does not exist", datadir);
+		else
+			pg_log_error("could not access directory \"%s\": %s", datadir,
+						 strerror(errno));
+
+		return false;
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_log_error("directory \"%s\" is not a database cluster directory",
+					 datadir);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	LogicalRepInfo *dbinfo;
+	SimpleStringListCell *cell;
+	int			i = 0;
+
+	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
+
+	for (cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		dbinfo[i].made_subscription = false;
+		/* Other fields will be filled later */
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+static PGconn *
+connect_database(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+static void
+disconnect_database(PGconn *conn)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: %s",
+				 PQresultErrorMessage(res));
+	}
+	if (PQntuples(res) != 1)
+	{
+		PQclear(res);
+		disconnect_database(conn);
+		pg_fatal("could not get system identifier: got %d rows, expected %d row",
+				 PQntuples(res), 1);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pfree(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+	int			rc;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		rc = system(cmd_str);
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pfree(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static bool
+setup_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			return false;
+		}
+
+		/* Remember database OID */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
+		else
+			return false;
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Is recovery still in progress?
+ * If the answer is yes, it returns 1, otherwise, returns 0. If an error occurs
+ * while executing the query, it returns -1.
+ */
+static int
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		pg_log_error("could not obtain recovery progress");
+		return -1;
+	}
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	if (ret == 0)
+		return 1;
+	else if (ret > 0)
+		return 0;
+	else
+		return -1;				/* should not happen */
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static bool
+check_publisher(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn) == 1)
+		pg_fatal("primary server cannot be in recovery");
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			return false;
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			pg_free(primary_slot_name); /* it is not being used. */
+			primary_slot_name = NULL;
+			return false;
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		return false;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
+		return false;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static bool
+check_subscriber(LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo);
+	if (conn == NULL)
+		exit(1);
+
+	/* The target server must be a standby */
+	if (server_is_in_recovery(conn) == 0)
+	{
+		pg_log_error("The target server is not a standby");
+		return false;
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		return false;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		return false;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		return false;
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		return false;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		return false;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
+ */
+static bool
+setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	PGconn	   *conn;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo);
+		if (conn == NULL)
+			exit(1);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn);
+	}
+
+	return true;
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/* This temporary replication slot is only used for catchup purposes */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return lsn;
+		}
+	}
+
+	/* Cleanup if there is any failure */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	if (!dry_run)
+	{
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it is
+ * run.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, PGS_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* Append timestamp with ISO 8601 format */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/%s/server_start_%s.log", datadir,
+				   PGS_OUTPUT_DIR, timebuf);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+static void
+start_standby_server(const char *pg_ctl_path, const char *datadir,
+					 const char *logfile)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" start -D \"%s\" -s -l \"%s\"",
+						  pg_ctl_path, datadir, logfile);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 1);
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc, 0);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc, int action)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+
+	if (action)
+		pg_log_info("postmaster was started");
+	else
+		pg_log_info("postmaster was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+					  CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+
+	pg_log_info("waiting the postmaster to reach the consistent state");
+
+	conn = connect_database(conninfo);
+	if (conn == NULL)
+		exit(1);
+
+	for (;;)
+	{
+		int			in_recovery;
+
+		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (in_recovery == 0 || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			pg_fatal("recovery timed out");
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("postmaster reached the consistent state");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication needs to be created */
+	appendPQExpBuffer(str,
+					  "SELECT puballtables FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain publication information: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * If publication name already exists and puballtables is true, let's
+		 * use it. A previous run of pg_createsubscriber must have created
+		 * this publication. Bail out.
+		 */
+		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
+		{
+			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
+			return;
+		}
+		else
+		{
+			/*
+			 * Unfortunately, if it reaches this code path, it will always
+			 * fail (unless you decide to change the existing publication
+			 * name). That's bad but it is very unlikely that the user will
+			 * choose a name with pg_createsubscriber_ prefix followed by the
+			 * exact database oid in which puballtables is false.
+			 */
+			pg_log_error("publication \"%s\" does not replicate changes for all tables",
+						 dbinfo->pubname);
+			pg_log_error_hint("Consider renaming this publication.");
+			PQclear(res);
+			PQfinish(conn);
+			exit(1);
+		}
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
+					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_publication = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
+					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+		}
+	}
+
+	/* for cleanup purposes */
+	dbinfo->made_subscription = true;
+
+	if (!dry_run)
+		PQclear(res);
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove subscription if it couldn't finish all steps.
+ */
+static void
+drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: %s",
+				 PQresultErrorMessage(res));
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		PQclear(res);
+		PQfinish(conn);
+		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
+				 PQntuples(res), 1);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	PQclear(res);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
+					 dbinfo->subname, PQresultErrorMessage(res));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial location, enabling the subscription is the last step
+ * of this setup.
+ */
+static void
+enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQfinish(conn);
+			pg_fatal("could not enable subscription \"%s\": %s",
+					 dbinfo->subname, PQerrorMessage(conn));
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"help", no_argument, NULL, '?'},
+		{"version", no_argument, NULL, 'V'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"subscriber-server", required_argument, NULL, 'S'},
+		{"database", required_argument, NULL, 'd'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"retain", no_argument, NULL, 'r'},
+		{"verbose", no_argument, NULL, 'v'},
+		{NULL, 0, NULL, 0}
+	};
+
+	CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_ctl_path = NULL;
+	char	   *pg_resetwal_path = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.sub_conninfo_str = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(opt.subscriber_dir);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'S':
+				opt.sub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.sub_conninfo_str == NULL)
+	{
+		pg_log_error("no subscriber connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* rudimentary check for a data directory. */
+	if (!check_data_directory(opt.subscriber_dir))
+		exit(1);
+
+	/* Store database information for publisher and subscriber */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Create the output directory to store any data generated by this tool */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* subscriber PID file. */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * The standby server must be running. That's because some checks will be
+	 * done (is it ready for a logical replication setup?). After that, stop
+	 * the subscriber in preparation to modify some recovery parameters that
+	 * require a restart.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		/* Check if the standby server is ready for logical replication */
+		if (!check_subscriber(dbinfo))
+			exit(1);
+
+		/*
+		 * Check if the primary server is ready for logical replication. This
+		 * routine checks if a replication slot is in use on primary so it
+		 * relies on check_subscriber() to obtain the primary_slot_name.
+		 * That's why it is called after it.
+		 */
+		if (!check_publisher(dbinfo))
+			exit(1);
+
+		/*
+		 * Create the required objects for each database on publisher. This
+		 * step is here mainly because if we stop the standby we cannot verify
+		 * if the primary slot is in use. We could use an extra connection for
+		 * it but it doesn't seem worth.
+		 */
+		if (!setup_publisher(dbinfo))
+			exit(1);
+
+		/* Stop the standby server */
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		if (!dry_run)
+			stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+	else
+	{
+		pg_log_error("standby is not running");
+		pg_log_error_hint("Start the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo);
+	if (conn == NULL)
+		exit(1);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0], true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/* Start subscriber and wait until accepting connections */
+	pg_log_info("starting the subscriber");
+	if (!dry_run)
+		start_standby_server(pg_ctl_path, opt.subscriber_dir, server_start_log);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	if (!setup_subscriber(dbinfo, consistent_lsn))
+		exit(1);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary",
+						   primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+		disconnect_database(conn);
+	}
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	if (!dry_run)
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(pg_resetwal_path, &opt);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..95eb4e70ac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,39 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test checking options of pg_createsubscriber.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--dry-run',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres'
+	],
+	'no subscriber connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'dbname=postgres',
+		'--subscriber-server', 'dbname=postgres'
+	],
+	'no database name specified');
+
+done_testing();
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
new file mode 100644
index 0000000000..e2807d3fac
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -0,0 +1,217 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_p;
+my $node_f;
+my $node_s;
+my $node_c;
+my $result;
+my $slotname;
+
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster.
+{
+	local $ENV{'INITDB_TEMPLATE'} = undef;
+
+	$node_f = PostgreSQL::Test::Cluster->new('node_f');
+	$node_f->init(allows_streaming => 'logical');
+	$node_f->start;
+}
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+$slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+$node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+log_min_messages = debug2
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--subscriber-server', $node_f->connstr('pg1'),
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Run pg_createsubscriber on the stopped node
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'target server must be running');
+
+$node_s->start;
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+$node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->append_conf(
+	'postgresql.conf', qq[
+log_min_messages = debug2
+]);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'), '--subscriber-server',
+		$node_c->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'primary server is in recovery');
+
+# Stop node C
+$node_c->teardown_node;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1')
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--subscriber-server',
+		$node_s->connstr('pg1'), '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+ok( -d $node_s->data_dir . "/pg_createsubscriber_output.d",
+	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success"
+);
+
+# Confirm the physical replication slot has been removed
+$result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d808aad8b0..08de2bf4e6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData
-- 
2.43.0

v24-0002-Update-documentation.patchapplication/octet-stream; name=v24-0002-Update-documentation.patchDownload
From 81a3ea3597193f13cdf1675d7cc1cb36dca6df80 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Tue, 13 Feb 2024 10:59:47 +0000
Subject: [PATCH v24 02/18] Update documentation

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 205 +++++++++++++++-------
 1 file changed, 142 insertions(+), 63 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index f5238771b7..7cdd047d67 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -48,19 +48,99 @@ PostgreSQL documentation
   </cmdsynopsis>
  </refsynopsisdiv>
 
- <refsect1>
+ <refsect1 id="r1-app-pg_createsubscriber-1">
   <title>Description</title>
   <para>
-    <application>pg_createsubscriber</application> creates a new logical
-    replica from a physical standby server.
+   The <application>pg_createsubscriber</application> creates a new <link
+   linkend="logical-replication-subscription">subscriber</link> from a physical
+   standby server.
   </para>
 
   <para>
-   The <application>pg_createsubscriber</application> should be run at the target
-   server. The source server (known as publisher server) should accept logical
-   replication connections from the target server (known as subscriber server).
-   The target server should accept local logical replication connection.
+   The <application>pg_createsubscriber</application> must be run at the target
+   server. The source server (known as publisher server) must accept both
+   normal and logical replication connections from the target server (known as
+   subscriber server). The target server must accept normal local connections.
   </para>
+
+  <para>
+   There are some prerequisites for both the source and target instance. If
+   these are not met an error will be reported.
+  </para>
+
+  <itemizedlist>
+   <listitem>
+    <para>
+     The given target data directory must have the same system identifier than the
+     source data directory.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must be used as a physical standby.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The given database user for the target instance must have privileges for
+     creating subscriptions and using functions for replication origin.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     and <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+     configured to a value greater than the number of target databases.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The source instance must have
+     <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+     <literal>logical</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases and replication slots.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The target instance must have
+     <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases and walsenders.
+    </para>
+   </listitem>
+  </itemizedlist>
+
+  <note>
+   <para>
+    After the successful conversion, a physical replication slot configured as
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>
+    would be removed from a primary instance.
+   </para>
+
+   <para>
+    The <application>pg_createsubscriber</application> focuses on large-scale
+    systems that contain more data than 1GB.  For smaller systems, initial data
+    synchronization of <link linkend="logical-replication">logical
+    replication</link> is recommended.
+   </para>
+  </note>
  </refsect1>
 
  <refsect1>
@@ -191,7 +271,7 @@ PostgreSQL documentation
  </refsect1>
 
  <refsect1>
-  <title>Notes</title>
+  <title>How It Works</title>
 
   <para>
    The transformation proceeds in the following steps:
@@ -200,97 +280,89 @@ PostgreSQL documentation
   <procedure>
    <step>
     <para>
-     <application>pg_createsubscriber</application> checks if the given target data
-     directory has the same system identifier than the source data directory.
-     Since it uses the recovery process as one of the steps, it starts the
-     target server as a replica from the source server. If the system
-     identifier is not the same, <application>pg_createsubscriber</application> will
-     terminate with an error.
+     Checks the target can be converted.  In particular, things listed in
+     <link linkend="r1-app-pg_createsubscriber-1">above section</link> would be
+     checked.  If these are not met <application>pg_createsubscriber</application>
+     will terminate with an error.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> checks if the target data
-     directory is used by a physical replica. Stop the physical replica if it is
-     running. One of the next steps is to add some recovery parameters that
-     requires a server start. This step avoids an error.
+     Creates a publication and a logical replication slot for each specified
+     database on the source instance.  These publications and logical replication
+     slots have generated names:
+     <quote><literal>pg_createsubscriber_%u</literal></quote> (parameters:
+     Database <parameter>oid</parameter>) for publications,
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     Database <parameter>oid</parameter>, Pid <parameter>int</parameter>) for
+     replication slots.
     </para>
    </step>
-
    <step>
     <para>
-     <application>pg_createsubscriber</application> creates one replication slot for
-     each specified database on the source server. The replication slot name
-     contains a <literal>pg_createsubscriber</literal> prefix. These replication
-     slots will be used by the subscriptions in a future step.  A temporary
-     replication slot is used to get a consistent start location. This
-     consistent LSN will be used as a stopping point in the <xref
-     linkend="guc-recovery-target-lsn"/> parameter and by the
-     subscriptions as a replication starting point. It guarantees that no
-     transaction will be lost.
+     Stops the target instance.  This is needed to add some recovery parameters
+     during the conversion.
     </para>
    </step>
-
    <step>
     <para>
-     <application>pg_createsubscriber</application> writes recovery parameters into
-     the target data directory and start the target server. It specifies a LSN
-     (consistent LSN that was obtained in the previous step) of write-ahead
-     log location up to which recovery will proceed. It also specifies
-     <literal>promote</literal> as the action that the server should take once
-     the recovery target is reached. This step finishes once the server ends
-     standby mode and is accepting read-write operations.
+     Creates a temporary replication slot to get a consistent start location.
+     The slot has generated names:
+     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
+     (parameters: Pid <parameter>int</parameter>).  Got consistent LSN will be
+     used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
+     parameter and by the subscriptions as a replication starting point. It
+     guarantees that no transaction will be lost.
+    </para>
+   </step>
+   <step>
+    <para>
+     Writes recovery parameters into the target data directory and starts the
+     target instance.  It specifies a LSN (consistent LSN that was obtained in
+     the previous step) of write-ahead log location up to which recovery will
+     proceed. It also specifies <literal>promote</literal> as the action that
+     the server should take once the recovery target is reached. This step
+     finishes once the server ends standby mode and is accepting read-write
+     operations.
     </para>
    </step>
 
    <step>
     <para>
-     Next, <application>pg_createsubscriber</application> creates one publication
-     for each specified database on the source server. Each publication
-     replicates changes for all tables in the database. The publication name
-     contains a <literal>pg_createsubscriber</literal> prefix. These publication
-     will be used by a corresponding subscription in a next step.
+     Creates a subscription for each specified database on the target instance.
+     These subscriptions have generated name:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     Database <parameter>oid</parameter>, Pid <parameter>int</parameter>).
+     These subscription have same subscription options:
+     <quote><literal>create_slot = false, copy_data = false, enabled = false</literal></quote>.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> creates one subscription for
-     each specified database on the target server. Each subscription name
-     contains a <literal>pg_createsubscriber</literal> prefix. The replication slot
-     name is identical to the subscription name. It does not copy existing data
-     from the source server. It does not create a replication slot. Instead, it
-     uses the replication slot that was created in a previous step. The
-     subscription is created but it is not enabled yet. The reason is the
-     replication progress must be set to the consistent LSN but replication
-     origin name contains the subscription oid in its name. Hence, the
-     subscription will be enabled in a separate step.
+     Sets replication progress to the consistent LSN that was obtained in a
+     previous step.  This is the exact LSN to be used as a initial location for
+     each subscription.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> sets the replication progress to
-     the consistent LSN that was obtained in a previous step. When the target
-     server started the recovery process, it caught up to the consistent LSN.
-     This is the exact LSN to be used as a initial location for each
-     subscription.
+     Enables the subscription for each specified database on the target server.
+     The subscription starts streaming from the consistent LSN.
     </para>
    </step>
 
    <step>
     <para>
-     Finally, <application>pg_createsubscriber</application> enables the subscription
-     for each specified database on the target server. The subscription starts
-     streaming from the consistent LSN.
+     Stops the standby server.
     </para>
    </step>
 
    <step>
     <para>
-     <application>pg_createsubscriber</application> stops the target server to change
-     its system identifier.
+     Updates a system identifier on the target server.
     </para>
    </step>
   </procedure>
@@ -300,8 +372,15 @@ PostgreSQL documentation
   <title>Examples</title>
 
   <para>
-   To create a logical replica for databases <literal>hr</literal> and
-   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+   Here is an example of using <application>pg_createsubscriber</application>.
+   Before running the command, please make sure target server is stopped.
+<screen>
+<prompt>$</prompt> <userinput>pg_ctl -D /usr/local/pgsql/data stop</userinput>
+</screen>
+
+   Then run <application>pg_createsubscriber</application>. Below tries to
+   create subscriptions for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical standby:
 <screen>
 <prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -S "host=localhost" -d hr -d finance</userinput>
 </screen>
-- 
2.43.0

v24-0003-Add-version-check-for-standby-server.patchapplication/octet-stream; name=v24-0003-Add-version-check-for-standby-server.patchDownload
From d91ff97100ae31e01dccdc4fe7497abd3fe39cd8 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 14 Feb 2024 16:27:15 +0530
Subject: [PATCH v24 03/18] Add version check for standby server

Add version check for standby server
---
 doc/src/sgml/ref/pg_createsubscriber.sgml   |  6 +++++
 src/bin/pg_basebackup/pg_createsubscriber.c | 28 +++++++++++++++++++++
 2 files changed, 34 insertions(+)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 7cdd047d67..9d0c6c764c 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -125,6 +125,12 @@ PostgreSQL documentation
      databases and walsenders.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     Both the target and source instances must have same major versions with
+     <application>pg_createsubscriber</application>.
+    </para>
+   </listitem>
   </itemizedlist>
 
   <note>
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 205a835d36..b15769c75b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -23,6 +23,7 @@
 #include "common/file_perm.h"
 #include "common/logging.h"
 #include "common/restricted_token.h"
+#include "common/string.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
@@ -294,6 +295,8 @@ check_data_directory(const char *datadir)
 {
 	struct stat statbuf;
 	char		versionfile[MAXPGPATH];
+	FILE	   *ver_fd;
+	char		rawline[64];
 
 	pg_log_info("checking if directory \"%s\" is a cluster data directory",
 				datadir);
@@ -317,6 +320,31 @@ check_data_directory(const char *datadir)
 		return false;
 	}
 
+	/* Check standby server version */
+	if ((ver_fd = fopen(versionfile, "r")) == NULL)
+		pg_fatal("could not open file \"%s\" for reading: %m", versionfile);
+
+	/* Version number has to be the first line read */
+	if (!fgets(rawline, sizeof(rawline), ver_fd))
+	{
+		if (!ferror(ver_fd))
+			pg_fatal("unexpected empty file \"%s\"", versionfile);
+		else
+			pg_fatal("could not read file \"%s\": %m", versionfile);
+	}
+
+	/* Strip trailing newline and carriage return */
+	(void) pg_strip_crlf(rawline);
+
+	if (strcmp(rawline, PG_MAJORVERSION) != 0)
+	{
+		pg_log_error("standby server is of wrong version");
+		pg_log_error_detail("File \"%s\" contains \"%s\", which is not compatible with this program's version \"%s\".",
+							versionfile, rawline, PG_MAJORVERSION);
+		exit(1);
+	}
+
+	fclose(ver_fd);
 	return true;
 }
 
-- 
2.43.0

v24-0004-Remove-S-option-to-force-unix-domain-connection.patchapplication/octet-stream; name=v24-0004-Remove-S-option-to-force-unix-domain-connection.patchDownload
From a62ecb48a6abd1a005aa27107741d0409588b3cb Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 6 Feb 2024 14:45:03 +0530
Subject: [PATCH v24 04/18] Remove -S option to force unix domain connection

With this patch removed -S option and added option for username(-U), port(-p)
and socket directory(-s) for standby. This helps to force standby to use
unix domain connection.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     | 36 ++++++--
 src/bin/pg_basebackup/pg_createsubscriber.c   | 91 ++++++++++++++-----
 .../t/041_pg_createsubscriber_standby.pl      | 33 ++++---
 3 files changed, 115 insertions(+), 45 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 9d0c6c764c..579e50a0a0 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -34,11 +34,6 @@ PostgreSQL documentation
      <arg choice="plain"><option>--publisher-server</option></arg>
     </group>
     <replaceable>connstr</replaceable>
-    <group choice="req">
-     <arg choice="plain"><option>-S</option></arg>
-     <arg choice="plain"><option>--subscriber-server</option></arg>
-    </group>
-    <replaceable>connstr</replaceable>
     <group choice="req">
      <arg choice="plain"><option>-d</option></arg>
      <arg choice="plain"><option>--database</option></arg>
@@ -179,11 +174,36 @@ PostgreSQL documentation
      </varlistentry>
 
      <varlistentry>
-      <term><option>-S <replaceable class="parameter">connstr</replaceable></option></term>
-      <term><option>--subscriber-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        A port number on which the target server is listening for connections.
+        Defaults to the <envar>PGPORT</envar> environment variable, if set, or
+        a compiled-in default.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable>username</replaceable></option></term>
+      <term><option>--username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        Target's user name. Defaults to the <envar>PGUSER</envar> environment
+        variable.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s</option> <replaceable>dir</replaceable></term>
+      <term><option>--socketdir=</option><replaceable>dir</replaceable></term>
       <listitem>
        <para>
-        The connection string to the subscriber. For details see <xref linkend="libpq-connstring"/>.
+        A directory which locales a temporary Unix socket files. If not
+        specified, <application>pg_createsubscriber</application> tries to
+        connect via TCP/IP to <literal>localhost</literal>.
        </para>
       </listitem>
      </varlistentry>
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index b15769c75b..1ad7de9190 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -35,7 +35,9 @@ typedef struct CreateSubscriberOptions
 {
 	char	   *subscriber_dir; /* standby/subscriber data directory */
 	char	   *pub_conninfo_str;	/* publisher connection string */
-	char	   *sub_conninfo_str;	/* subscriber connection string */
+	unsigned short subport;			/* port number listen()'d by the standby */
+	char	   *subuser;			/* database user of the standby */
+	char	   *socketdir;			/* socket directory */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;			/* retain log file? */
 	int			recovery_timeout;	/* stop recovery after this time */
@@ -57,7 +59,9 @@ typedef struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_pub_base_conninfo(char *conninfo, char **dbname);
+static char *construct_sub_conninfo(char *username, unsigned short subport,
+									char *socketdir);
 static char *get_exec_path(const char *argv0, const char *progname);
 static bool check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
@@ -180,7 +184,10 @@ usage(void)
 	printf(_("\nOptions:\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
 	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
-	printf(_(" -S, --subscriber-server=CONNSTR     subscriber connection string\n"));
+	printf(_(" -p, --port=PORT                     subscriber port number\n"));
+	printf(_(" -U, --username=NAME                 subscriber user\n"));
+	printf(_(" -s, --socketdir=DIR                 socket directory to use\n"));
+	printf(_("                                     If not specified, localhost would be used\n"));
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
 	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
@@ -193,8 +200,8 @@ usage(void)
 }
 
 /*
- * Validate a connection string. Returns a base connection string that is a
- * connection string without a database name.
+ * Validate a connection string for the publisher. Returns a base connection
+ * string that is a connection string without a database name.
  *
  * Since we might process multiple databases, each database name will be
  * appended to this base connection string to provide a final connection
@@ -206,7 +213,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char **dbname)
+get_pub_base_conninfo(char *conninfo, char **dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -1593,6 +1600,40 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	destroyPQExpBuffer(str);
 }
 
+/*
+ * Construct a connection string toward a target server, from argument options.
+ *
+ * If inputs are the zero, default value would be used.
+ * - username: PGUSER environment value (it would not be parsed)
+ * - port: PGPORT environment value (it would not be parsed)
+ * - socketdir: localhost connection (unix-domain would not be used)
+ */
+static char *
+construct_sub_conninfo(char *username, unsigned short subport, char *sockdir)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	if (username)
+		appendPQExpBuffer(buf, "user=%s ", username);
+
+	if (subport != 0)
+		appendPQExpBuffer(buf, "port=%u ", subport);
+
+	if (sockdir)
+		appendPQExpBuffer(buf, "host=%s ", sockdir);
+	else
+		appendPQExpBuffer(buf, "host=localhost ");
+
+	appendPQExpBuffer(buf, "fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
 int
 main(int argc, char **argv)
 {
@@ -1602,7 +1643,9 @@ main(int argc, char **argv)
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
 		{"publisher-server", required_argument, NULL, 'P'},
-		{"subscriber-server", required_argument, NULL, 'S'},
+		{"port", required_argument, NULL, 'p'},
+		{"username", required_argument, NULL, 'U'},
+		{"socketdir", required_argument, NULL, 's'},
 		{"database", required_argument, NULL, 'd'},
 		{"dry-run", no_argument, NULL, 'n'},
 		{"recovery-timeout", required_argument, NULL, 't'},
@@ -1659,7 +1702,9 @@ main(int argc, char **argv)
 	/* Default settings */
 	opt.subscriber_dir = NULL;
 	opt.pub_conninfo_str = NULL;
-	opt.sub_conninfo_str = NULL;
+	opt.subport = 0;
+	opt.subuser = NULL;
+	opt.socketdir = NULL;
 	opt.database_names = (SimpleStringList)
 	{
 		NULL, NULL
@@ -1683,7 +1728,7 @@ main(int argc, char **argv)
 
 	get_restricted_token();
 
-	while ((c = getopt_long(argc, argv, "D:P:S:d:nrt:v",
+	while ((c = getopt_long(argc, argv, "D:P:p:U:s:S:d:nrt:v",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1695,8 +1740,17 @@ main(int argc, char **argv)
 			case 'P':
 				opt.pub_conninfo_str = pg_strdup(optarg);
 				break;
-			case 'S':
-				opt.sub_conninfo_str = pg_strdup(optarg);
+			case 'p':
+				if ((opt.subport = atoi(optarg)) <= 0)
+					pg_fatal("invalid old port number");
+				break;
+			case 'U':
+				pfree(opt.subuser);
+				opt.subuser = pg_strdup(optarg);
+				break;
+			case 's':
+				pfree(opt.socketdir);
+				opt.socketdir = pg_strdup(optarg);
 				break;
 			case 'd':
 				/* Ignore duplicated database names */
@@ -1763,21 +1817,12 @@ main(int argc, char **argv)
 		exit(1);
 	}
 	pg_log_info("validating connection string on publisher");
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
-										  &dbname_conninfo);
+	pub_base_conninfo = get_pub_base_conninfo(opt.pub_conninfo_str,
+											  &dbname_conninfo);
 	if (pub_base_conninfo == NULL)
 		exit(1);
 
-	if (opt.sub_conninfo_str == NULL)
-	{
-		pg_log_error("no subscriber connection string specified");
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
-	pg_log_info("validating connection string on subscriber");
-	sub_base_conninfo = get_base_conninfo(opt.sub_conninfo_str, NULL);
-	if (sub_base_conninfo == NULL)
-		exit(1);
+	sub_base_conninfo = construct_sub_conninfo(opt.subuser, opt.subport, opt.socketdir);
 
 	if (opt.database_names.head == NULL)
 	{
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index e2807d3fac..93148417db 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -66,7 +66,8 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
 		'--publisher-server', $node_p->connstr('pg1'),
-		'--subscriber-server', $node_f->connstr('pg1'),
+		'--port', $node_f->port,
+		'--host', $node_f->host,
 		'--database', 'pg1',
 		'--database', 'pg2'
 	],
@@ -78,8 +79,9 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1'), '--database',
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--host',
+		$node_s->host, '--database',
 		'pg1', '--database',
 		'pg2'
 	],
@@ -104,10 +106,11 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_c->data_dir, '--publisher-server',
-		$node_s->connstr('pg1'), '--subscriber-server',
-		$node_c->connstr('pg1'), '--database',
-		'pg1', '--database',
-		'pg2'
+		$node_s->connstr('pg1'),
+		'--port', $node_c->port,
+		'--socketdir', $node_c->host,
+		'--database', 'pg1',
+		'--database', 'pg2'
 	],
 	'primary server is in recovery');
 
@@ -124,8 +127,9 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1'), '--database',
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--socketdir',
+		$node_s->host, '--database',
 		'pg1', '--database',
 		'pg2'
 	],
@@ -141,8 +145,9 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1')
+		$node_p->connstr('pg1'), '--port',
+		$node_s->port, '--socketdir',
+		$node_s->host,
 	],
 	'run pg_createsubscriber without --databases');
 
@@ -152,9 +157,9 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--verbose', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--subscriber-server',
-		$node_s->connstr('pg1'), '--database',
-		'pg1', '--database',
+		$node_p->connstr('pg1'), '--port', $node_s->port,
+		'--socketdir', $node_s->host,
+		'--database', 'pg1', '--database',
 		'pg2'
 	],
 	'run pg_createsubscriber on node S');
-- 
2.43.0

v24-0005-Fix-some-trivial-issues.patchapplication/octet-stream; name=v24-0005-Fix-some-trivial-issues.patchDownload
From 3b93d9ddd193e1509ac02679b2dad4c3c701a56b Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 03:59:19 +0000
Subject: [PATCH v24 05/18] Fix some trivial issues

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 44 ++++++++++-----------
 1 file changed, 20 insertions(+), 24 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1ad7de9190..968d0ae6bd 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -387,12 +387,11 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
 				   const char *sub_base_conninfo)
 {
 	LogicalRepInfo *dbinfo;
-	SimpleStringListCell *cell;
 	int			i = 0;
 
 	dbinfo = (LogicalRepInfo *) pg_malloc(num_dbs * sizeof(LogicalRepInfo));
 
-	for (cell = dbnames.head; cell; cell = cell->next)
+	for (SimpleStringListCell *cell = dbnames.head; cell; cell = cell->next)
 	{
 		char	   *conninfo;
 
@@ -469,7 +468,6 @@ get_primary_sysid(const char *conninfo)
 	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		PQclear(res);
 		disconnect_database(conn);
 		pg_fatal("could not get system identifier: %s",
 				 PQresultErrorMessage(res));
@@ -516,7 +514,7 @@ get_standby_sysid(const char *datadir)
 	pg_log_info("system identifier is %llu on subscriber",
 				(unsigned long long) sysid);
 
-	pfree(cf);
+	pg_free(cf);
 
 	return sysid;
 }
@@ -534,7 +532,6 @@ modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *o
 	struct timeval tv;
 
 	char	   *cmd_str;
-	int			rc;
 
 	pg_log_info("modifying system identifier from subscriber");
 
@@ -567,14 +564,15 @@ modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *o
 
 	if (!dry_run)
 	{
-		rc = system(cmd_str);
+		int rc = system(cmd_str);
+
 		if (rc == 0)
 			pg_log_info("subscriber successfully changed the system identifier");
 		else
 			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
 	}
 
-	pfree(cf);
+	pg_free(cf);
 }
 
 /*
@@ -584,11 +582,11 @@ modify_subscriber_sysid(const char *pg_resetwal_path, CreateSubscriberOptions *o
 static bool
 setup_publisher(LogicalRepInfo *dbinfo)
 {
-	PGconn	   *conn;
-	PGresult   *res;
 
 	for (int i = 0; i < num_dbs; i++)
 	{
+		PGconn	   *conn;
+		PGresult   *res;
 		char		pubname[NAMEDATALEN];
 		char		replslotname[NAMEDATALEN];
 
@@ -901,7 +899,7 @@ check_subscriber(LogicalRepInfo *dbinfo)
 		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
 		return false;
 	}
-	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
 	{
 		pg_log_error("permission denied for function \"%s\"",
 					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
@@ -990,10 +988,10 @@ check_subscriber(LogicalRepInfo *dbinfo)
 static bool
 setup_subscriber(LogicalRepInfo *dbinfo, const char *consistent_lsn)
 {
-	PGconn	   *conn;
-
 	for (int i = 0; i < num_dbs; i++)
 	{
+		PGconn	   *conn;
+
 		/* Connect to subscriber. */
 		conn = connect_database(dbinfo[i].subconninfo);
 		if (conn == NULL)
@@ -1103,7 +1101,7 @@ drop_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
-						 slot_name, dbinfo->dbname, PQerrorMessage(conn));
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
 
 		PQclear(res);
 	}
@@ -1294,7 +1292,6 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		PQclear(res);
 		PQfinish(conn);
 		pg_fatal("could not obtain publication information: %s",
 				 PQresultErrorMessage(res));
@@ -1348,7 +1345,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		{
 			PQfinish(conn);
 			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
-					 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+					 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
 		}
 	}
 
@@ -1384,7 +1381,7 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
-						 dbinfo->pubname, dbinfo->dbname, PQerrorMessage(conn));
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
 
 		PQclear(res);
 	}
@@ -1429,7 +1426,7 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		{
 			PQfinish(conn);
 			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
-					 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+					 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
 		}
 	}
 
@@ -1465,7 +1462,7 @@ drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQerrorMessage(conn));
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
 
 		PQclear(res);
 	}
@@ -1502,7 +1499,6 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-		PQclear(res);
 		PQfinish(conn);
 		pg_fatal("could not obtain subscription OID: %s",
 				 PQresultErrorMessage(res));
@@ -1591,7 +1587,7 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		{
 			PQfinish(conn);
 			pg_fatal("could not enable subscription \"%s\": %s",
-					 dbinfo->subname, PQerrorMessage(conn));
+					 dbinfo->subname, PQresultErrorMessage(res));
 		}
 
 		PQclear(res);
@@ -1745,11 +1741,11 @@ main(int argc, char **argv)
 					pg_fatal("invalid old port number");
 				break;
 			case 'U':
-				pfree(opt.subuser);
+				pg_free(opt.subuser);
 				opt.subuser = pg_strdup(optarg);
 				break;
 			case 's':
-				pfree(opt.socketdir);
+				pg_free(opt.socketdir);
 				opt.socketdir = pg_strdup(optarg);
 				break;
 			case 'd':
@@ -1854,7 +1850,7 @@ main(int argc, char **argv)
 	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
 	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
 
-	/* rudimentary check for a data directory. */
+	/* Rudimentary check for a data directory */
 	if (!check_data_directory(opt.subscriber_dir))
 		exit(1);
 
@@ -1877,7 +1873,7 @@ main(int argc, char **argv)
 	/* Create the output directory to store any data generated by this tool */
 	server_start_log = setup_server_logfile(opt.subscriber_dir);
 
-	/* subscriber PID file. */
+	/* Subscriber PID file */
 	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
 
 	/*
-- 
2.43.0

v24-0006-Fix-cleanup-functions.patchapplication/octet-stream; name=v24-0006-Fix-cleanup-functions.patchDownload
From 87ec2d1d133fbfc2b1dcd85af5027f44d2d35542 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 16 Feb 2024 07:34:41 +0000
Subject: [PATCH v24 06/18] Fix cleanup functions

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 60 +++------------------
 1 file changed, 8 insertions(+), 52 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 968d0ae6bd..252d541472 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -54,7 +54,6 @@ typedef struct LogicalRepInfo
 
 	bool		made_replslot;	/* replication slot was created */
 	bool		made_publication;	/* publication was created */
-	bool		made_subscription;	/* subscription was created */
 } LogicalRepInfo;
 
 static void cleanup_objects_atexit(void);
@@ -95,7 +94,6 @@ static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 static void create_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, LogicalRepInfo *dbinfo);
 static void create_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
-static void drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
 static void set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo,
 									 const char *lsn);
 static void enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo);
@@ -141,22 +139,11 @@ cleanup_objects_atexit(void)
 
 	for (i = 0; i < num_dbs; i++)
 	{
-		if (dbinfo[i].made_subscription || recovery_ended)
+		if (recovery_ended)
 		{
-			conn = connect_database(dbinfo[i].subconninfo);
-			if (conn != NULL)
-			{
-				if (dbinfo[i].made_subscription)
-					drop_subscription(conn, &dbinfo[i]);
-
-				/*
-				 * Publications are created on publisher before promotion so
-				 * it might exist on subscriber after recovery ends.
-				 */
-				if (recovery_ended)
-					drop_publication(conn, &dbinfo[i]);
-				disconnect_database(conn);
-			}
+			pg_log_warning("pg_createsubscriber failed after the end of recovery");
+			pg_log_warning("Target server could not be usable as physical standby anymore.");
+			pg_log_warning_hint("You must re-create the physical standby again.");
 		}
 
 		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
@@ -404,7 +391,6 @@ store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
 		/* Fill subscriber attributes */
 		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
 		dbinfo[i].subconninfo = conninfo;
-		dbinfo[i].made_subscription = false;
 		/* Other fields will be filled later */
 
 		i++;
@@ -1430,46 +1416,12 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		}
 	}
 
-	/* for cleanup purposes */
-	dbinfo->made_subscription = true;
-
 	if (!dry_run)
 		PQclear(res);
 
 	destroyPQExpBuffer(str);
 }
 
-/*
- * Remove subscription if it couldn't finish all steps.
- */
-static void
-drop_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
-{
-	PQExpBuffer str = createPQExpBuffer();
-	PGresult   *res;
-
-	Assert(conn != NULL);
-
-	pg_log_info("dropping subscription \"%s\" on database \"%s\"",
-				dbinfo->subname, dbinfo->dbname);
-
-	appendPQExpBuffer(str, "DROP SUBSCRIPTION %s", dbinfo->subname);
-
-	pg_log_debug("command is: %s", str->data);
-
-	if (!dry_run)
-	{
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_log_error("could not drop subscription \"%s\" on database \"%s\": %s",
-						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
-
-		PQclear(res);
-	}
-
-	destroyPQExpBuffer(str);
-}
-
 /*
  * Sets the replication progress to the consistent LSN.
  *
@@ -1986,6 +1938,10 @@ main(int argc, char **argv)
 	/* Waiting the subscriber to be promoted */
 	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
 
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must re-create the new physical standby before continuing.");
+
 	/*
 	 * Create the subscription for each database on subscriber. It does not
 	 * enable it immediately because it needs to adjust the logical
-- 
2.43.0

v24-0007-Fix-server_is_in_recovery.patchapplication/octet-stream; name=v24-0007-Fix-server_is_in_recovery.patchDownload
From a558d20eb38ee31f73c1a6c0191c41e514f43d68 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:12:32 +0000
Subject: [PATCH v24 07/18] Fix server_is_in_recovery

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 25 +++++++--------------
 1 file changed, 8 insertions(+), 17 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 252d541472..ea4eb7e621 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -73,7 +73,7 @@ static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
 static void modify_subscriber_sysid(const char *pg_resetwal_path,
 									CreateSubscriberOptions *opt);
-static int	server_is_in_recovery(PGconn *conn);
+static bool	server_is_in_recovery(PGconn *conn);
 static bool check_publisher(LogicalRepInfo *dbinfo);
 static bool setup_publisher(LogicalRepInfo *dbinfo);
 static bool check_subscriber(LogicalRepInfo *dbinfo);
@@ -651,7 +651,7 @@ setup_publisher(LogicalRepInfo *dbinfo)
  * If the answer is yes, it returns 1, otherwise, returns 0. If an error occurs
  * while executing the query, it returns -1.
  */
-static int
+static bool
 server_is_in_recovery(PGconn *conn)
 {
 	PGresult   *res;
@@ -660,22 +660,13 @@ server_is_in_recovery(PGconn *conn)
 	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		PQclear(res);
-		pg_log_error("could not obtain recovery progress");
-		return -1;
-	}
+		pg_fatal("could not obtain recovery progress");
 
 	ret = strcmp("t", PQgetvalue(res, 0, 0));
 
 	PQclear(res);
 
-	if (ret == 0)
-		return 1;
-	else if (ret > 0)
-		return 0;
-	else
-		return -1;				/* should not happen */
+	return ret == 0;
 }
 
 /*
@@ -704,7 +695,7 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * If the primary server is in recovery (i.e. cascading replication),
 	 * objects (publication) cannot be created because it is read only.
 	 */
-	if (server_is_in_recovery(conn) == 1)
+	if (server_is_in_recovery(conn))
 		pg_fatal("primary server cannot be in recovery");
 
 	/*------------------------------------------------------------------------
@@ -845,7 +836,7 @@ check_subscriber(LogicalRepInfo *dbinfo)
 		exit(1);
 
 	/* The target server must be a standby */
-	if (server_is_in_recovery(conn) == 0)
+	if (!server_is_in_recovery(conn))
 	{
 		pg_log_error("The target server is not a standby");
 		return false;
@@ -1223,7 +1214,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 
 	for (;;)
 	{
-		int			in_recovery;
+		bool			in_recovery;
 
 		in_recovery = server_is_in_recovery(conn);
 
@@ -1231,7 +1222,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 		 * Does the recovery process finish? In dry run mode, there is no
 		 * recovery mode. Bail out as the recovery process has ended.
 		 */
-		if (in_recovery == 0 || dry_run)
+		if (!in_recovery || dry_run)
 		{
 			status = POSTMASTER_READY;
 			recovery_ended = true;
-- 
2.43.0

v24-0008-Avoid-possible-null-report.patchapplication/octet-stream; name=v24-0008-Avoid-possible-null-report.patchDownload
From 9bb02ca87957234880db72023895d05b10cf45dd Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:20:00 +0000
Subject: [PATCH v24 08/18] Avoid possible null report

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index ea4eb7e621..f10e8002c6 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -921,7 +921,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 				 max_lrworkers);
 	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
 	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
-	pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
 
 	PQclear(res);
 
-- 
2.43.0

v24-0009-prohibit-to-reuse-publications.patchapplication/octet-stream; name=v24-0009-prohibit-to-reuse-publications.patchDownload
From 34540c6c40b6367f78aa710fe3a9c56ee5489738 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:32:35 +0000
Subject: [PATCH v24 09/18] prohibit to reuse publications

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 38 +++++++--------------
 1 file changed, 12 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index f10e8002c6..e88b29ea3e 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1264,7 +1264,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 
 	/* Check if the publication needs to be created */
 	appendPQExpBuffer(str,
-					  "SELECT puballtables FROM pg_catalog.pg_publication "
+					  "SELECT count(1) FROM pg_catalog.pg_publication "
 					  "WHERE pubname = '%s'",
 					  dbinfo->pubname);
 	res = PQexec(conn, str->data);
@@ -1275,34 +1275,20 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 				 PQresultErrorMessage(res));
 	}
 
-	if (PQntuples(res) == 1)
+	if (atoi(PQgetvalue(res, 0, 0)) == 1)
 	{
 		/*
-		 * If publication name already exists and puballtables is true, let's
-		 * use it. A previous run of pg_createsubscriber must have created
-		 * this publication. Bail out.
+		 * Unfortunately, if it reaches this code path, it will always
+		 * fail (unless you decide to change the existing publication
+		 * name). That's bad but it is very unlikely that the user will
+		 * choose a name with pg_createsubscriber_ prefix followed by the
+		 * exact database oid in which puballtables is false.
 		 */
-		if (strcmp(PQgetvalue(res, 0, 0), "t") == 0)
-		{
-			pg_log_info("publication \"%s\" already exists", dbinfo->pubname);
-			return;
-		}
-		else
-		{
-			/*
-			 * Unfortunately, if it reaches this code path, it will always
-			 * fail (unless you decide to change the existing publication
-			 * name). That's bad but it is very unlikely that the user will
-			 * choose a name with pg_createsubscriber_ prefix followed by the
-			 * exact database oid in which puballtables is false.
-			 */
-			pg_log_error("publication \"%s\" does not replicate changes for all tables",
-						 dbinfo->pubname);
-			pg_log_error_hint("Consider renaming this publication.");
-			PQclear(res);
-			PQfinish(conn);
-			exit(1);
-		}
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication.");
+		PQclear(res);
+		PQfinish(conn);
+		exit(1);
 	}
 
 	PQclear(res);
-- 
2.43.0

v24-0010-Make-the-ERROR-handling-more-consistent.patchapplication/octet-stream; name=v24-0010-Make-the-ERROR-handling-more-consistent.patchDownload
From 5f9de1a125989ee376eb8e80b055b334745d2587 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 19 Feb 2024 04:42:17 +0000
Subject: [PATCH v24 10/18] Make the ERROR handling more consistent

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 38 +++------------------
 1 file changed, 5 insertions(+), 33 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index e88b29ea3e..f5ccd479b6 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -453,18 +453,12 @@ get_primary_sysid(const char *conninfo)
 
 	res = PQexec(conn, "SELECT system_identifier FROM pg_control_system()");
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		disconnect_database(conn);
 		pg_fatal("could not get system identifier: %s",
 				 PQresultErrorMessage(res));
-	}
+
 	if (PQntuples(res) != 1)
-	{
-		PQclear(res);
-		disconnect_database(conn);
 		pg_fatal("could not get system identifier: got %d rows, expected %d row",
 				 PQntuples(res), 1);
-	}
 
 	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
 
@@ -775,8 +769,6 @@ check_publisher(LogicalRepInfo *dbinfo)
 		{
 			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
 						 PQntuples(res), 1);
-			pg_free(primary_slot_name); /* it is not being used. */
-			primary_slot_name = NULL;
 			return false;
 		}
 		else
@@ -1269,11 +1261,8 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 					  dbinfo->pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		PQfinish(conn);
 		pg_fatal("could not obtain publication information: %s",
 				 PQresultErrorMessage(res));
-	}
 
 	if (atoi(PQgetvalue(res, 0, 0)) == 1)
 	{
@@ -1286,8 +1275,6 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		 */
 		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
 		pg_log_error_hint("Consider renaming this publication.");
-		PQclear(res);
-		PQfinish(conn);
 		exit(1);
 	}
 
@@ -1305,12 +1292,10 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
 					 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
-		}
 	}
 
 	/* for cleanup purposes */
@@ -1386,12 +1371,10 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
 					 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
-		}
 	}
 
 	if (!dry_run)
@@ -1428,19 +1411,12 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-	{
-		PQfinish(conn);
 		pg_fatal("could not obtain subscription OID: %s",
 				 PQresultErrorMessage(res));
-	}
 
 	if (PQntuples(res) != 1 && !dry_run)
-	{
-		PQclear(res);
-		PQfinish(conn);
 		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
 				 PQntuples(res), 1);
-	}
 
 	if (dry_run)
 	{
@@ -1475,12 +1451,10 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
 					 dbinfo->subname, PQresultErrorMessage(res));
-		}
 
 		PQclear(res);
 	}
@@ -1513,12 +1487,10 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 	if (!dry_run)
 	{
 		res = PQexec(conn, str->data);
+
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-		{
-			PQfinish(conn);
 			pg_fatal("could not enable subscription \"%s\": %s",
 					 dbinfo->subname, PQresultErrorMessage(res));
-		}
 
 		PQclear(res);
 	}
-- 
2.43.0

v24-0011-Update-test-codes.patchapplication/octet-stream; name=v24-0011-Update-test-codes.patchDownload
From b5b0fbd8d4ef315f943b5aaa7f748b4230841e51 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Fri, 16 Feb 2024 09:04:47 +0000
Subject: [PATCH v24 11/18] Update test codes

---
 .../t/040_pg_createsubscriber.pl              |   2 +-
 .../t/041_pg_createsubscriber_standby.pl      | 197 +++++++++---------
 2 files changed, 105 insertions(+), 94 deletions(-)

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 95eb4e70ac..65eba6f623 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -5,7 +5,7 @@
 #
 
 use strict;
-use warnings;
+use warnings  FATAL => 'all';
 use PostgreSQL::Test::Utils;
 use Test::More;
 
diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 93148417db..06ef05d5e8 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -4,26 +4,23 @@
 # Test using a standby server as the subscriber.
 
 use strict;
-use warnings;
+use warnings FATAL => 'all';
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
 
-my $node_p;
-my $node_f;
-my $node_s;
-my $node_c;
-my $result;
-my $slotname;
-
 # Set up node P as primary
-$node_p = PostgreSQL::Test::Cluster->new('node_p');
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
 $node_p->init(allows_streaming => 'logical');
 $node_p->start;
 
-# Set up node F as about-to-fail node
-# Force it to initialize a new cluster instead of copying a
-# previously initdb'd cluster.
+# ------------------------------
+# Check pg_createsubscriber fails when the target server is not a
+# standby of the source.
+#
+# Set up node F as about-to-fail node. Force it to initialize a new cluster
+# instead of copying a previously initdb'd cluster.
+my $node_f;
 {
 	local $ENV{'INITDB_TEMPLATE'} = undef;
 
@@ -32,112 +29,91 @@ $node_p->start;
 	$node_f->start;
 }
 
-# On node P
-# - create databases
-# - create test tables
-# - insert a row
-# - create a physical replication slot
-$node_p->safe_psql(
-	'postgres', q(
-	CREATE DATABASE pg1;
-	CREATE DATABASE pg2;
-));
-$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
-$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
-$slotname = 'physical_slot';
-$node_p->safe_psql('pg2',
-	"SELECT pg_create_physical_replication_slot('$slotname')");
+# Run pg_createsubscriber on about-to-fail node F
+command_checks_all(
+	[
+		'pg_createsubscriber', '--verbose', '--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('postgres'),
+		'--port', $node_f->port, '--socketdir', $node_f->host,
+		'--database', 'postgres'
+	],
+	1,
+	[qr//],
+	[
+		qr/subscriber data directory is not a copy of the source database cluster/
+	],
+	'subscriber data directory is not a copy of the source database cluster');
 
+# ------------------------------
+# Check pg_createsubscriber fails when the target server is not running
+#
 # Set up node S as standby linking to node P
 $node_p->backup('backup_1');
-$node_s = PostgreSQL::Test::Cluster->new('node_s');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
 $node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
-$node_s->append_conf(
-	'postgresql.conf', qq[
-log_min_messages = debug2
-primary_slot_name = '$slotname'
-]);
 $node_s->set_standby_mode();
 
-# Run pg_createsubscriber on about-to-fail node F
-command_fails(
-	[
-		'pg_createsubscriber', '--verbose',
-		'--pgdata', $node_f->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
-		'--port', $node_f->port,
-		'--host', $node_f->host,
-		'--database', 'pg1',
-		'--database', 'pg2'
-	],
-	'subscriber data directory is not a copy of the source database cluster');
-
 # Run pg_createsubscriber on the stopped node
-command_fails(
+command_checks_all(
 	[
-		'pg_createsubscriber', '--verbose',
-		'--dry-run', '--pgdata',
-		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--port',
-		$node_s->port, '--host',
-		$node_s->host, '--database',
-		'pg1', '--database',
-		'pg2'
+		'pg_createsubscriber', '--verbose', '--pgdata', $node_s->data_dir,
+		'--publisher-server', $node_p->connstr('postgres'),
+		'--port', $node_s->port, '--socketdir', $node_s->host,
+		'--database', 'postgres'
 	],
+	1,
+	[qr//],
+	[qr/standby is not running/],
 	'target server must be running');
 
 $node_s->start;
 
+# ------------------------------
+# Check pg_createsubscriber fails when the target server is a member of
+# the cascading standby.
+#
 # Set up node C as standby linking to node S
 $node_s->backup('backup_2');
-$node_c = PostgreSQL::Test::Cluster->new('node_c');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
 $node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
-$node_c->append_conf(
-	'postgresql.conf', qq[
-log_min_messages = debug2
-]);
 $node_c->set_standby_mode();
 $node_c->start;
 
 # Run pg_createsubscriber on node C (P -> S -> C)
-command_fails(
+command_checks_all(
 	[
-		'pg_createsubscriber', '--verbose',
-		'--dry-run', '--pgdata',
-		$node_c->data_dir, '--publisher-server',
-		$node_s->connstr('pg1'),
-		'--port', $node_c->port,
-		'--socketdir', $node_c->host,
-		'--database', 'pg1',
-		'--database', 'pg2'
+		'pg_createsubscriber', '--verbose', '--pgdata', $node_c->data_dir,
+		'--publisher-server', $node_s->connstr('postgres'),
+		'--port', $node_c->port, '--socketdir', $node_c->host,
+		'--database', 'postgres'
 	],
-	'primary server is in recovery');
+	1,
+	[qr//],
+	[qr/primary server cannot be in recovery/],
+	'target server must be running');
 
 # Stop node C
-$node_c->teardown_node;
-
-# Insert another row on node P and wait node S to catch up
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
-$node_p->wait_for_replay_catchup($node_s);
+$node_c->stop;
 
-# dry run mode on node S
+# ------------------------------
+# Check successful dry-run
+#
+# Dry run mode on node S
 command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--port',
-		$node_s->port, '--socketdir',
-		$node_s->host, '--database',
-		'pg1', '--database',
-		'pg2'
+		$node_p->connstr('postgres'),
+		'--port', $node_s->port,
+		'--socketdir', $node_s->host,
+		'--database', 'postgres'
 	],
 	'run pg_createsubscriber --dry-run on node S');
 
 # Check if node S is still a standby
-is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
-	't', 'standby is in recovery');
+my $result = $node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()');
+is($result, 't', 'standby is in recovery');
 
 # pg_createsubscriber can run without --databases option
 command_ok(
@@ -145,12 +121,39 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--port',
+		$node_p->connstr('postgres'), '--port',
 		$node_s->port, '--socketdir',
 		$node_s->host,
 	],
 	'run pg_createsubscriber without --databases');
 
+# ------------------------------
+# Check successful conversion
+#
+# Prepare databases and a physical replication slot
+my $slotname = 'physical_slot';
+$node_p->safe_psql(
+	'postgres', qq[
+		CREATE DATABASE pg1;
+		CREATE DATABASE pg2;
+		SELECT pg_create_physical_replication_slot('$slotname');
+]);
+
+# Use the created slot for physical replication
+$node_s->append_conf('postgresql.conf', "primary_slot_name = $slotname");
+$node_s->reload;
+
+# Prepare tables and initial data on pg1 and pg2
+$node_p->safe_psql(
+	'pg1', qq[
+		CREATE TABLE tbl1 (a text);
+		INSERT INTO tbl1 VALUES('first row');
+		INSERT INTO tbl1 VALUES('second row')
+]);
+$node_p->safe_psql('pg2', "CREATE TABLE tbl2 (a text);");
+
+$node_p->wait_for_replay_catchup($node_s);
+
 # Run pg_createsubscriber on node S
 command_ok(
 	[
@@ -176,15 +179,23 @@ is($result, qq(0),
 	'the physical replication slot used as primary_slot_name has been removed'
 );
 
-# Insert rows on P
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
-$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
-
 # PID sets to undefined because subscriber was stopped behind the scenes.
 # Start subscriber
 $node_s->{_pid} = undef;
 $node_s->start;
 
+# Confirm two subscriptions has been created
+$result = $node_s->safe_psql('postgres',
+	"SELECT count(distinct subdbid) FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_';"
+);
+is($result, qq(2),
+	'Subscriptions has been created to all the specified databases'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
 # Get subscription names
 $result = $node_s->safe_psql(
 	'postgres', qq(
@@ -214,9 +225,9 @@ my $sysid_s = $node_s->safe_psql('postgres',
 	'SELECT system_identifier FROM pg_control_system()');
 ok($sysid_p != $sysid_s, 'system identifier was changed');
 
-# clean up
-$node_p->teardown_node;
-$node_s->teardown_node;
-$node_f->teardown_node;
+# Clean up
+$node_p->stop;
+$node_s->stop;
+$node_f->stop;
 
 done_testing();
-- 
2.43.0

v24-0012-Avoid-running-pg_createsubscriber-on-standby-whi.patchapplication/octet-stream; name=v24-0012-Avoid-running-pg_createsubscriber-on-standby-whi.patchDownload
From 401b8a524136004bdf682a38ebffe37ebe298e2d Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 20 Feb 2024 14:49:56 +0530
Subject: [PATCH v24 12/18] Avoid running pg_createsubscriber on standby which
 is primary to other node

pg_createsubscriber will throw error when run on a node which is a standby
but also primary to other node.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index f5ccd479b6..26ce91f58b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -834,6 +834,26 @@ check_subscriber(LogicalRepInfo *dbinfo)
 		return false;
 	}
 
+	/*
+	 * The target server must not be primary for other server. Because the
+	 * pg_createsubscriber would modify the system_identifier at the end of
+	 * run, but walreceiver of another standby would not accept the difference.
+	 */
+	res = PQexec(conn, 
+				 "SELECT count(1) from pg_catalog.pg_stat_activity where backend_type = 'walsender'");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+			pg_log_error("could not obtain walsender information");
+			return false;
+	}
+
+	if (atoi(PQgetvalue(res, 0, 0)) != 0)
+	{
+			pg_log_error("the target server is primary to other server");
+			return false;
+	}
+
 	/*
 	 * Subscriptions can only be created by roles that have the privileges of
 	 * pg_create_subscription role and CREATE privileges on the specified
-- 
2.43.0

v24-0013-Consider-temporary-slot-to-check-max_replication.patchapplication/octet-stream; name=v24-0013-Consider-temporary-slot-to-check-max_replication.patchDownload
From 2de7b6ef4f74170b9f593b9fbe1e7524f4cf76da Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 20 Feb 2024 14:53:55 +0530
Subject: [PATCH v24 13/18] Consider temporary slot to check
 max_replication_slots in primary

While checking for max_replication_slots in primary we should consider
the temporary slot as well.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 26ce91f58b..4a28dfb81c 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -698,7 +698,8 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * we should check it to make sure it won't fail.
 	 *
 	 * - wal_level = logical
-	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_replication_slots >= current + number of dbs to be converted +
+	 * 							  temporary slot to be created
 	 * - max_wal_senders >= current + number of dbs to be converted
 	 * -----------------------------------------------------------------------
 	 */
@@ -786,12 +787,12 @@ check_publisher(LogicalRepInfo *dbinfo)
 		return false;
 	}
 
-	if (max_repslots - cur_repslots < num_dbs)
+	if (max_repslots - cur_repslots < num_dbs + 1)
 	{
 		pg_log_error("publisher requires %d replication slots, but only %d remain",
-					 num_dbs, max_repslots - cur_repslots);
+					 num_dbs + 1, max_repslots - cur_repslots);
 		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
-						  cur_repslots + num_dbs);
+						  cur_repslots + num_dbs + 1);
 		return false;
 	}
 
-- 
2.43.0

v24-0014-address-comments-from-Vignesh-1.patchapplication/octet-stream; name=v24-0014-address-comments-from-Vignesh-1.patchDownload
From 6dfad1816c9aaf213b3239490802227beff0d4cc Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 22 Feb 2024 10:00:02 +0000
Subject: [PATCH v24 14/18] address comments from Vignesh #1

---
 doc/src/sgml/ref/pg_createsubscriber.sgml   |  7 +++++++
 src/bin/pg_basebackup/pg_createsubscriber.c | 17 ++++++++++++++++-
 2 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 579e50a0a0..4579495089 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -135,6 +135,13 @@ PostgreSQL documentation
     would be removed from a primary instance.
    </para>
 
+   <para>
+    Executing DDL commands while running <application>pg_createsubscriber</application>
+    is not recommended. Because if the physical standby has already been
+    converted to the subscriber, it would not be replicated, so an error
+    would occur.
+   </para>
+
    <para>
     The <application>pg_createsubscriber</application> focuses on large-scale
     systems that contain more data than 1GB.  For smaller systems, initial data
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 4a28dfb81c..a51943106a 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1226,9 +1226,13 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 	if (conn == NULL)
 		exit(1);
 
+#define NUM_ACCEPTABLE_DISCONNECTION 15
+
 	for (;;)
 	{
 		bool			in_recovery;
+		PGresult	   *res;
+		int				count = 0;
 
 		in_recovery = server_is_in_recovery(conn);
 
@@ -1243,6 +1247,16 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 			break;
 		}
 
+		res = PQexec(conn,
+					 "SELECT count(1) FROM pg_catalog.pg_stat_wal_receiver;");
+
+		if (atoi(PQgetvalue(res, 0, 0)) == 0 &&
+			count++ > NUM_ACCEPTABLE_DISCONNECTION)
+		{
+			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			pg_fatal("standby disconnected from the primary");
+		}
+
 		/* Bail out after recovery_timeout seconds if this option is set */
 		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
 		{
@@ -1251,6 +1265,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 		}
 
 		/* Keep waiting */
+		PQclear(res);
 		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
 
 		timer += WAIT_INTERVAL;
@@ -1342,7 +1357,7 @@ drop_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 	pg_log_info("dropping publication \"%s\" on database \"%s\"",
 				dbinfo->pubname, dbinfo->dbname);
 
-	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+	appendPQExpBuffer(str, "DROP PUBLICATION IF EXISTS %s", dbinfo->pubname);
 
 	pg_log_debug("command is: %s", str->data);
 
-- 
2.43.0

v24-0015-Call-disconnect_database-even-when-the-process-w.patchapplication/octet-stream; name=v24-0015-Call-disconnect_database-even-when-the-process-w.patchDownload
From 57f112c5a76b2acecf19c9c9c14e0791670b2a22 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 22 Feb 2024 10:13:09 +0000
Subject: [PATCH v24 15/18] Call disconnect_database() even when the process
 would exit soon

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 73 +++++++++++++++++++--
 1 file changed, 69 insertions(+), 4 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index a51943106a..86e277b339 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -419,6 +419,8 @@ connect_database(const char *conninfo)
 	{
 		pg_log_error("could not clear search_path: %s",
 					 PQresultErrorMessage(res));
+
+		disconnect_database(conn);
 		return NULL;
 	}
 	PQclear(res);
@@ -581,6 +583,8 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		{
 			pg_log_error("could not obtain database OID: %s",
 						 PQresultErrorMessage(res));
+
+			disconnect_database(conn);
 			return false;
 		}
 
@@ -588,6 +592,8 @@ setup_publisher(LogicalRepInfo *dbinfo)
 		{
 			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
 						 PQntuples(res), 1);
+
+			disconnect_database(conn);
 			return false;
 		}
 
@@ -654,7 +660,10 @@ server_is_in_recovery(PGconn *conn)
 	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		disconnect_database(conn);
 		pg_fatal("could not obtain recovery progress");
+	}
 
 	ret = strcmp("t", PQgetvalue(res, 0, 0));
 
@@ -690,7 +699,10 @@ check_publisher(LogicalRepInfo *dbinfo)
 	 * objects (publication) cannot be created because it is read only.
 	 */
 	if (server_is_in_recovery(conn))
+	{
+		disconnect_database(conn);
 		pg_fatal("primary server cannot be in recovery");
+	}
 
 	/*------------------------------------------------------------------------
 	 * Logical replication requires a few parameters to be set on publisher.
@@ -726,6 +738,8 @@ check_publisher(LogicalRepInfo *dbinfo)
 	{
 		pg_log_error("could not obtain publisher settings: %s",
 					 PQresultErrorMessage(res));
+
+		disconnect_database(conn);
 		return false;
 	}
 
@@ -763,6 +777,8 @@ check_publisher(LogicalRepInfo *dbinfo)
 		{
 			pg_log_error("could not obtain replication slot information: %s",
 						 PQresultErrorMessage(res));
+
+			disconnect_database(conn);
 			return false;
 		}
 
@@ -770,6 +786,8 @@ check_publisher(LogicalRepInfo *dbinfo)
 		{
 			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
 						 PQntuples(res), 1);
+
+			disconnect_database(conn);
 			return false;
 		}
 		else
@@ -832,6 +850,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	if (!server_is_in_recovery(conn))
 	{
 		pg_log_error("The target server is not a standby");
+
+		disconnect_database(conn);
 		return false;
 	}
 
@@ -845,14 +865,18 @@ check_subscriber(LogicalRepInfo *dbinfo)
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
-			pg_log_error("could not obtain walsender information");
-			return false;
+		pg_log_error("could not obtain walsender information");
+
+		disconnect_database(conn);
+		return false;
 	}
 
 	if (atoi(PQgetvalue(res, 0, 0)) != 0)
 	{
-			pg_log_error("the target server is primary to other server");
-			return false;
+		pg_log_error("the target server is primary to other server");
+
+		disconnect_database(conn);
+		return false;
 	}
 
 	/*
@@ -874,6 +898,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	{
 		pg_log_error("could not obtain access privilege information: %s",
 					 PQresultErrorMessage(res));
+
+		disconnect_database(conn);
 		return false;
 	}
 
@@ -882,6 +908,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 		pg_log_error("permission denied to create subscription");
 		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
 						  "pg_create_subscription");
+
+		disconnect_database(conn);
 		return false;
 	}
 	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
@@ -893,6 +921,8 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	{
 		pg_log_error("permission denied for function \"%s\"",
 					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+
+		disconnect_database(conn);
 		return false;
 	}
 
@@ -947,6 +977,9 @@ check_subscriber(LogicalRepInfo *dbinfo)
 					 num_dbs, max_repslots);
 		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
 						  num_dbs);
+
+		PQclear(res);
+		disconnect_database(conn);
 		return false;
 	}
 
@@ -956,6 +989,9 @@ check_subscriber(LogicalRepInfo *dbinfo)
 					 num_dbs, max_lrworkers);
 		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
 						  num_dbs);
+
+		PQclear(res);
+		disconnect_database(conn);
 		return false;
 	}
 
@@ -965,6 +1001,9 @@ check_subscriber(LogicalRepInfo *dbinfo)
 					 num_dbs + 1, max_wprocs);
 		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
 						  num_dbs + 1);
+
+		PQclear(res);
+		disconnect_database(conn);
 		return false;
 	}
 
@@ -1052,6 +1091,8 @@ create_logical_replication_slot(PGconn *conn, LogicalRepInfo *dbinfo,
 			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
 						 slot_name, dbinfo->dbname,
 						 PQresultErrorMessage(res));
+
+			disconnect_database(conn);
 			return lsn;
 		}
 	}
@@ -1253,6 +1294,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 		if (atoi(PQgetvalue(res, 0, 0)) == 0 &&
 			count++ > NUM_ACCEPTABLE_DISCONNECTION)
 		{
+			disconnect_database(conn);
 			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
 			pg_fatal("standby disconnected from the primary");
 		}
@@ -1260,6 +1302,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 		/* Bail out after recovery_timeout seconds if this option is set */
 		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
 		{
+			disconnect_database(conn);
 			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
 			pg_fatal("recovery timed out");
 		}
@@ -1297,8 +1340,11 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 					  dbinfo->pubname);
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		disconnect_database(conn);
 		pg_fatal("could not obtain publication information: %s",
 				 PQresultErrorMessage(res));
+	}
 
 	if (atoi(PQgetvalue(res, 0, 0)) == 1)
 	{
@@ -1311,6 +1357,7 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		 */
 		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
 		pg_log_error_hint("Consider renaming this publication.");
+		disconnect_database(conn);
 		exit(1);
 	}
 
@@ -1330,8 +1377,11 @@ create_publication(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			disconnect_database(conn);
 			pg_fatal("could not create publication \"%s\" on database \"%s\": %s",
 					 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+		}
 	}
 
 	/* for cleanup purposes */
@@ -1409,8 +1459,11 @@ create_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			disconnect_database(conn);
 			pg_fatal("could not create subscription \"%s\" on database \"%s\": %s",
 					 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+		}
 	}
 
 	if (!dry_run)
@@ -1447,12 +1500,18 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		disconnect_database(conn);
 		pg_fatal("could not obtain subscription OID: %s",
 				 PQresultErrorMessage(res));
+	}
 
 	if (PQntuples(res) != 1 && !dry_run)
+	{
+		disconnect_database(conn);
 		pg_fatal("could not obtain subscription OID: got %d rows, expected %d rows",
 				 PQntuples(res), 1);
+	}
 
 	if (dry_run)
 	{
@@ -1489,8 +1548,11 @@ set_replication_progress(PGconn *conn, LogicalRepInfo *dbinfo, const char *lsn)
 		res = PQexec(conn, str->data);
 
 		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			disconnect_database(conn);
 			pg_fatal("could not set replication progress for the subscription \"%s\": %s",
 					 dbinfo->subname, PQresultErrorMessage(res));
+		}
 
 		PQclear(res);
 	}
@@ -1525,8 +1587,11 @@ enable_subscription(PGconn *conn, LogicalRepInfo *dbinfo)
 		res = PQexec(conn, str->data);
 
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			disconnect_database(conn);
 			pg_fatal("could not enable subscription \"%s\": %s",
 					 dbinfo->subname, PQresultErrorMessage(res));
+		}
 
 		PQclear(res);
 	}
-- 
2.43.0

v24-0016-Address-comments-From-Vignesh-2.patchapplication/octet-stream; name=v24-0016-Address-comments-From-Vignesh-2.patchDownload
From dcdf9498a4436a14c713fa6be6bc5aa273d62aa8 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 22 Feb 2024 11:01:46 +0000
Subject: [PATCH v24 16/18] Address comments From Vignesh #2

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 29 +++++++++++++++--------
 1 file changed, 19 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 4579495089..92e77af6df 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -43,7 +43,7 @@ PostgreSQL documentation
   </cmdsynopsis>
  </refsynopsisdiv>
 
- <refsect1 id="r1-app-pg_createsubscriber-1">
+ <refsect1>
   <title>Description</title>
   <para>
    The <application>pg_createsubscriber</application> creates a new <link
@@ -63,7 +63,7 @@ PostgreSQL documentation
    these are not met an error will be reported.
   </para>
 
-  <itemizedlist>
+  <itemizedlist id="app-pg-createsubscriber-description-prerequisites">
    <listitem>
     <para>
      The given target data directory must have the same system identifier than the
@@ -106,15 +106,15 @@ PostgreSQL documentation
    </listitem>
    <listitem>
     <para>
-     The target instance must have
+     The source instance must have
      <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
-     configured to a value greater than or equal to the number of target
-     databases and replication slots.
+     configured to a value greater than the number of target databases and
+     replication slots.
     </para>
    </listitem>
    <listitem>
     <para>
-     The target instance must have
+     The source instance must have
      <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
      configured to a value greater than or equal to the number of target
      databases and walsenders.
@@ -149,6 +149,15 @@ PostgreSQL documentation
     replication</link> is recommended.
    </para>
   </note>
+
+  <caution>
+   <para>
+    If <application>pg_createsubscriber</application> fails after the
+    promotion of physical standby, you must re-create the new physical standby
+    before continuing.
+   </para>
+  </caution>
+
  </refsect1>
 
  <refsect1>
@@ -314,8 +323,8 @@ PostgreSQL documentation
    <step>
     <para>
      Checks the target can be converted.  In particular, things listed in
-     <link linkend="r1-app-pg_createsubscriber-1">above section</link> would be
-     checked.  If these are not met <application>pg_createsubscriber</application>
+     <link linkend="app-pg-createsubscriber-description-prerequisites">prerequisites</link>
+     would be checked.  If these are not met <application>pg_createsubscriber</application>
      will terminate with an error.
     </para>
    </step>
@@ -406,9 +415,9 @@ PostgreSQL documentation
 
   <para>
    Here is an example of using <application>pg_createsubscriber</application>.
-   Before running the command, please make sure target server is stopped.
+   Before running the command, please make sure target server is running.
 <screen>
-<prompt>$</prompt> <userinput>pg_ctl -D /usr/local/pgsql/data stop</userinput>
+<prompt>$</prompt> <userinput>pg_ctl -D /usr/local/pgsql/data start</userinput>
 </screen>
 
    Then run <application>pg_createsubscriber</application>. Below tries to
-- 
2.43.0

v24-0017-Address-comments-From-Vignesh-3.patchapplication/octet-stream; name=v24-0017-Address-comments-From-Vignesh-3.patchDownload
From 49ca0b2fb21252ceed7ff8bc91bbd8cf33ab7909 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 22 Feb 2024 11:07:22 +0000
Subject: [PATCH v24 17/18]  Address comments From Vignesh #3

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 86e277b339..63b76bda12 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -766,7 +766,7 @@ check_publisher(LogicalRepInfo *dbinfo)
 	if (primary_slot_name)
 	{
 		appendPQExpBuffer(str,
-						  "SELECT 1 FROM pg_replication_slots "
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
 						  "WHERE active AND slot_name = '%s'",
 						  primary_slot_name);
 
@@ -940,7 +940,7 @@ check_subscriber(LogicalRepInfo *dbinfo)
 	 *------------------------------------------------------------------------
 	 */
 	res = PQexec(conn,
-				 "SELECT setting FROM pg_settings WHERE name IN ("
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
 				 "'max_logical_replication_workers', "
 				 "'max_replication_slots', "
 				 "'max_worker_processes', "
@@ -2015,6 +2015,7 @@ main(int argc, char **argv)
 		if (conn != NULL)
 		{
 			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+			disconnect_database(conn);
 		}
 		else
 		{
@@ -2022,7 +2023,6 @@ main(int argc, char **argv)
 						   primary_slot_name);
 			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
 		}
-		disconnect_database(conn);
 	}
 
 	/* Stop the subscriber */
-- 
2.43.0

v24-0018-Address-comments-From-Vignesh-4.patchapplication/octet-stream; name=v24-0018-Address-comments-From-Vignesh-4.patchDownload
From a01ffc85ec9a023d2836463730bdd3bae17a036d Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 22 Feb 2024 11:24:49 +0000
Subject: [PATCH v24 18/18] Address comments From Vignesh #4

---
 .../t/041_pg_createsubscriber_standby.pl      | 48 ++++++++++---------
 1 file changed, 25 insertions(+), 23 deletions(-)

diff --git a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
index 06ef05d5e8..2b428a2d5b 100644
--- a/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
+++ b/src/bin/pg_basebackup/t/041_pg_createsubscriber_standby.pl
@@ -9,7 +9,7 @@ use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
 
-# Set up node P as primary
+# Set up node_p as primary
 my $node_p = PostgreSQL::Test::Cluster->new('node_p');
 $node_p->init(allows_streaming => 'logical');
 $node_p->start;
@@ -18,18 +18,16 @@ $node_p->start;
 # Check pg_createsubscriber fails when the target server is not a
 # standby of the source.
 #
-# Set up node F as about-to-fail node. Force it to initialize a new cluster
+# Set up node_f as about-to-fail node. Force it to initialize a new cluster
 # instead of copying a previously initdb'd cluster.
-my $node_f;
-{
-	local $ENV{'INITDB_TEMPLATE'} = undef;
 
-	$node_f = PostgreSQL::Test::Cluster->new('node_f');
-	$node_f->init(allows_streaming => 'logical');
-	$node_f->start;
-}
 
-# Run pg_createsubscriber on about-to-fail node F
+
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+$node_f->start;
+
+# Run pg_createsubscriber on about-to-fail node_F
 command_checks_all(
 	[
 		'pg_createsubscriber', '--verbose', '--pgdata', $node_f->data_dir,
@@ -47,7 +45,7 @@ command_checks_all(
 # ------------------------------
 # Check pg_createsubscriber fails when the target server is not running
 #
-# Set up node S as standby linking to node P
+# Set up node_s as standby linking to node_p
 $node_p->backup('backup_1');
 my $node_s = PostgreSQL::Test::Cluster->new('node_s');
 $node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
@@ -72,14 +70,14 @@ $node_s->start;
 # Check pg_createsubscriber fails when the target server is a member of
 # the cascading standby.
 #
-# Set up node C as standby linking to node S
+# Set up node_c as standby linking to node_s
 $node_s->backup('backup_2');
 my $node_c = PostgreSQL::Test::Cluster->new('node_c');
 $node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
 $node_c->set_standby_mode();
 $node_c->start;
 
-# Run pg_createsubscriber on node C (P -> S -> C)
+# Run pg_createsubscriber on node_c (P -> S -> C)
 command_checks_all(
 	[
 		'pg_createsubscriber', '--verbose', '--pgdata', $node_c->data_dir,
@@ -90,15 +88,15 @@ command_checks_all(
 	1,
 	[qr//],
 	[qr/primary server cannot be in recovery/],
-	'target server must be running');
+	'source server must not be another standby');
 
-# Stop node C
+# Stop node_c
 $node_c->stop;
 
 # ------------------------------
 # Check successful dry-run
 #
-# Dry run mode on node S
+# Dry run mode on node_s
 command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
@@ -109,9 +107,13 @@ command_ok(
 		'--socketdir', $node_s->host,
 		'--database', 'postgres'
 	],
-	'run pg_createsubscriber --dry-run on node S');
+	'run pg_createsubscriber --dry-run on node_s');
+
+# Check if node_s is still running
+command_exit_is([ 'pg_ctl', 'status', '-D', $node_s->data_dir ],
+	0, 'pg_ctl status with server running');
 
-# Check if node S is still a standby
+# Check if node_s is still a standby
 my $result = $node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()');
 is($result, 't', 'standby is in recovery');
 
@@ -154,7 +156,7 @@ $node_p->safe_psql('pg2', "CREATE TABLE tbl2 (a text);");
 
 $node_p->wait_for_replay_catchup($node_s);
 
-# Run pg_createsubscriber on node S
+# Run pg_createsubscriber on node_s
 command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
@@ -165,7 +167,7 @@ command_ok(
 		'--database', 'pg1', '--database',
 		'pg2'
 	],
-	'run pg_createsubscriber on node S');
+	'run pg_createsubscriber on node_s');
 
 ok( -d $node_s->data_dir . "/pg_createsubscriber_output.d",
 	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success"
@@ -184,12 +186,12 @@ is($result, qq(0),
 $node_s->{_pid} = undef;
 $node_s->start;
 
-# Confirm two subscriptions has been created
+# Confirm two subscriptions have been created
 $result = $node_s->safe_psql('postgres',
 	"SELECT count(distinct subdbid) FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_';"
 );
 is($result, qq(2),
-	'Subscriptions has been created to all the specified databases'
+	'Subscriptions have been created on all the specified databases'
 );
 
 # Insert rows on P
@@ -203,7 +205,7 @@ $result = $node_s->safe_psql(
 ));
 my @subnames = split("\n", $result);
 
-# Wait subscriber to catch up
+# Wait for subscriber to catch up
 $node_s->wait_for_subscription_sync($node_p, $subnames[0]);
 $node_s->wait_for_subscription_sync($node_p, $subnames[1]);
 
-- 
2.43.0

#157Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Alvaro Herrera (#150)
RE: speed up a logical replica setup

Dear Alvaro,

15.

You said in case of failure, cleanups is not needed if the process exits soon [1].
But some functions call PQfinish() then exit(1) or pg_fatal(). Should we follow?

Hmm, but doesn't this mean that the server will log an ugly message that
"client closed connection unexpectedly"? I think it's nicer to close
the connection before terminating the process (especially since the
code for that is already written).

OK. So we should disconnect properly even if the process exits. I added the function call
again. Note that PQclear() was not added because it is only related with the application.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#158Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: vignesh C (#151)
RE: speed up a logical replica setup

Dear Vignesh,

Few comments regarding the documentation:
1) max_replication_slots information seems to be present couple of times:

+    <para>
+     The target instance must have
+     <link
linkend="guc-max-replication-slots"><varname>max_replication_slots</varna
me></link>
+     and <link
linkend="guc-max-logical-replication-workers"><varname>max_logical_replica
tion_workers</varname></link>
+     configured to a value greater than or equal to the number of target
+     databases.
+    </para>
+   <listitem>
+    <para>
+     The target instance must have
+     <link
linkend="guc-max-replication-slots"><varname>max_replication_slots</varna
me></link>
+     configured to a value greater than or equal to the number of target
+     databases and replication slots.
+    </para>
+   </listitem>

Fixed.

2) Can we add an id to prerequisites and use it instead of referring
to r1-app-pg_createsubscriber-1:
-     <application>pg_createsubscriber</application> checks if the
given target data
-     directory has the same system identifier than the source data directory.
-     Since it uses the recovery process as one of the steps, it starts the
-     target server as a replica from the source server. If the system
-     identifier is not the same,
<application>pg_createsubscriber</application> will
-     terminate with an error.
+     Checks the target can be converted.  In particular, things listed in
+     <link linkend="r1-app-pg_createsubscriber-1">above section</link>
would be
+     checked.  If these are not met
<application>pg_createsubscriber</application>
+     will terminate with an error.
</para>

Changed.

3) The code also checks the following:
Verify if a PostgreSQL binary (progname) is available in the same
directory as pg_createsubscriber.

But this is not present in the pre-requisites of documentation.

I think it is quite trivial so that I did not add.

4) Here we mention that the target server should be stopped, but the
same is not mentioned in prerequisites:
+   Here is an example of using
<application>pg_createsubscriber</application>.
+   Before running the command, please make sure target server is stopped.
+<screen>
+<prompt>$</prompt> <userinput>pg_ctl -D /usr/local/pgsql/data
stop</userinput>
+</screen>
+

Oh, it is opposite, it should NOT be stopped. Fixed.

5) If there is an error during any of the pg_createsubscriber
operation like if create subscription fails, it might not be possible
to rollback to the earlier state which had physical-standby
replication. I felt we should document this and also add it to the
console message like how we do in case of pg_upgrade.

Added.

New version can be available in [1]/messages/by-id/TYCPR01MB12077CD333376B53F9CAE7AC0F5562@TYCPR01MB12077.jpnprd01.prod.outlook.com

[1]: /messages/by-id/TYCPR01MB12077CD333376B53F9CAE7AC0F5562@TYCPR01MB12077.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#159Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: vignesh C (#152)
RE: speed up a logical replica setup

Dear Vignesh,

Few comments:
1) The below code can lead to assertion failure if the publisher is
stopped while dropping the replication slot:
+       if (primary_slot_name != NULL)
+       {
+               conn = connect_database(dbinfo[0].pubconninfo);
+               if (conn != NULL)
+               {
+                       drop_replication_slot(conn, &dbinfo[0],
primary_slot_name);
+               }
+               else
+               {
+                       pg_log_warning("could not drop replication
slot \"%s\" on primary",
+                                                  primary_slot_name);
+                       pg_log_warning_hint("Drop this replication
slot soon to avoid retention of WAL files.");
+               }
+               disconnect_database(conn);
+       }

pg_createsubscriber: error: connection to database failed: connection
to server on socket "/tmp/.s.PGSQL.5432" failed: No such file or
directory
Is the server running locally and accepting connections on that socket?
pg_createsubscriber: warning: could not drop replication slot
"standby_1" on primary
pg_createsubscriber: hint: Drop this replication slot soon to avoid
retention of WAL files.
pg_createsubscriber: pg_createsubscriber.c:432: disconnect_database:
Assertion `conn != ((void *)0)' failed.
Aborted (core dumped)

This is happening because we are calling disconnect_database in case
of connection failure case too which has the following assert:
+static void
+disconnect_database(PGconn *conn)
+{
+       Assert(conn != NULL);
+
+       PQfinish(conn);
+}

Right. disconnect_database() was moved to if (conn != NULL) block.

2) There is a CheckDataVersion function which does exactly this, will
we be able to use this:
+       /* Check standby server version */
+       if ((ver_fd = fopen(versionfile, "r")) == NULL)
+               pg_fatal("could not open file \"%s\" for reading: %m",
versionfile);
+
+       /* Version number has to be the first line read */
+       if (!fgets(rawline, sizeof(rawline), ver_fd))
+       {
+               if (!ferror(ver_fd))
+                       pg_fatal("unexpected empty file \"%s\"", versionfile);
+               else
+                       pg_fatal("could not read file \"%s\": %m", versionfile);
+       }
+
+       /* Strip trailing newline and carriage return */
+       (void) pg_strip_crlf(rawline);
+
+       if (strcmp(rawline, PG_MAJORVERSION) != 0)
+       {
+               pg_log_error("standby server is of wrong version");
+               pg_log_error_detail("File \"%s\" contains \"%s\",
which is not compatible with this program's version \"%s\".",
+                                                       versionfile,
rawline, PG_MAJORVERSION);
+               exit(1);
+       }
+
+       fclose(ver_fd);
3) Should this be added to typedefs.list:
+enum WaitPMResult
+{
+       POSTMASTER_READY,
+       POSTMASTER_STILL_STARTING
+};

But the comment from Peter E. [1]/messages/by-id/3ee79f2c-e8b3-4342-857c-a31b87e1afda@eisentraut.org was opposite. I did not handle this.

4) pgCreateSubscriber should be mentioned after pg_controldata to keep
the ordering consistency:
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..c5edd244ef 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -285,6 +285,7 @@
&pgCtl;
&pgResetwal;
&pgRewind;
+   &pgCreateSubscriber;
&pgtestfsync;

This has been already pointed out by Peter E. I did not handle this.

5) Here pg_replication_slots should be pg_catalog.pg_replication_slots:
+       if (primary_slot_name)
+       {
+               appendPQExpBuffer(str,
+                                                 "SELECT 1 FROM
pg_replication_slots "
+                                                 "WHERE active AND
slot_name = '%s'",
+                                                 primary_slot_name);

Fixed.

6) Here pg_settings should be pg_catalog.pg_settings:
+        * - max_worker_processes >= 1 + number of dbs to be converted
+
*------------------------------------------------------------------------
+        */
+       res = PQexec(conn,
+                                "SELECT setting FROM pg_settings
WHERE name IN ("
+                                "'max_logical_replication_workers', "
+                                "'max_replication_slots', "
+                                "'max_worker_processes', "
+                                "'primary_slot_name') "
+                                "ORDER BY name");

Fixed.

New version can be available in [2]/messages/by-id/TYCPR01MB12077CD333376B53F9CAE7AC0F5562@TYCPR01MB12077.jpnprd01.prod.outlook.com

[1]: /messages/by-id/3ee79f2c-e8b3-4342-857c-a31b87e1afda@eisentraut.org
[2]: /messages/by-id/TYCPR01MB12077CD333376B53F9CAE7AC0F5562@TYCPR01MB12077.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#160Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: vignesh C (#154)
RE: speed up a logical replica setup

Dear Vignesh,

Few comments on the tests:
1) If the dry run was successful because of some issue then the server
will be stopped so we can check for "pg_ctl status" if the server is
running otherwise the connection will fail in this case. Another way
would be to check if it does not have "postmaster was stopped"
messages in the stdout.
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+       't', 'standby is in recovery');

Just to confirm - your suggestion is to add `pg_ctl status`, right? Added.

2) Can we add verification of  "postmaster was stopped" messages in
the stdout for dry run without --databases testcase
+# pg_createsubscriber can run without --databases option
+command_ok(
+       [
+               'pg_createsubscriber', '--verbose',
+               '--dry-run', '--pgdata',
+               $node_s->data_dir, '--publisher-server',
+               $node_p->connstr('pg1'), '--subscriber-server',
+               $node_s->connstr('pg1')
+       ],
+       'run pg_createsubscriber without --databases');
+

Hmm, in case of --dry-run, the server would be never shut down.
See below part.

```
if (!dry_run)
stop_standby_server(pg_ctl_path, opt.subscriber_dir);
```

3) This message "target server must be running" seems to be wrong,
should it be cannot specify cascading replicating standby as standby
node(this is for v22-0011 patch :
+               'pg_createsubscriber', '--verbose', '--pgdata',
$node_c->data_dir,
+               '--publisher-server', $node_s->connstr('postgres'),
+               '--port', $node_c->port, '--socketdir', $node_c->host,
+               '--database', 'postgres'
],
-       'primary server is in recovery');
+       1,
+       [qr//],
+       [qr/primary server cannot be in recovery/],
+       'target server must be running');

Fixed.

4) Should this be "Wait for subscriber to catch up"
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);

Fixed.

5) Should this be 'Subscriptions has been created on all the specified
databases'
+);
+is($result, qq(2),
+       'Subscriptions has been created to all the specified databases'
+);

Fixed, but "has" should be "have".

6) Add test to verify current_user is not a member of
ROLE_PG_CREATE_SUBSCRIPTION, has no create permissions, has no
permissions to execution replication origin advance functions

7) Add tests to verify insufficient max_logical_replication_workers,
max_replication_slots and max_worker_processes for the subscription
node

8) Add tests to verify invalid configuration of wal_level,
max_replication_slots and max_wal_senders for the publisher node

Hmm, but adding these checks may increase the test time. we should
measure the time and then decide.

9) We can use the same node name in comment and for the variable
+# Set up node P as primary
+$node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;

Fixed.

10) Similarly we can use node_f instead of F in the comments.
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster.
+{
+       local $ENV{'INITDB_TEMPLATE'} = undef;
+
+       $node_f = PostgreSQL::Test::Cluster->new('node_f');
+       $node_f->init(allows_streaming => 'logical');
+       $node_f->start;

Fixed. Also, recent commit [1]https://github.com/postgres/postgres/commit/ff9e1e764fcce9a34467d614611a34d4d2a91b50 allows to run the initdb forcibly. So followed.

New patch can be available in [2]/messages/by-id/TYCPR01MB12077CD333376B53F9CAE7AC0F5562@TYCPR01MB12077.jpnprd01.prod.outlook.com.

[1]: https://github.com/postgres/postgres/commit/ff9e1e764fcce9a34467d614611a34d4d2a91b50
[2]: /messages/by-id/TYCPR01MB12077CD333376B53F9CAE7AC0F5562@TYCPR01MB12077.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#161'Alvaro Herrera'
alvherre@alvh.no-ip.org
In reply to: Hayato Kuroda (Fujitsu) (#157)
Re: speed up a logical replica setup

Hello,

On 2024-Feb-22, Hayato Kuroda (Fujitsu) wrote:

Dear Alvaro,

Hmm, but doesn't this mean that the server will log an ugly message
that "client closed connection unexpectedly"? I think it's nicer to
close the connection before terminating the process (especially
since the code for that is already written).

OK. So we should disconnect properly even if the process exits. I
added the function call again. Note that PQclear() was not added
because it is only related with the application.

Sounds about right, but I didn't verify the patches in detail.

--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"Hay quien adquiere la mala costumbre de ser infeliz" (M. A. Evans)

#162vignesh C
vignesh21@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#160)
Re: speed up a logical replica setup

On Thu, 22 Feb 2024 at 21:17, Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear Vignesh,

Few comments on the tests:
1) If the dry run was successful because of some issue then the server
will be stopped so we can check for "pg_ctl status" if the server is
running otherwise the connection will fail in this case. Another way
would be to check if it does not have "postmaster was stopped"
messages in the stdout.
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+       't', 'standby is in recovery');

Just to confirm - your suggestion is to add `pg_ctl status`, right? Added.

Yes, that is correct.

2) Can we add verification of  "postmaster was stopped" messages in
the stdout for dry run without --databases testcase
+# pg_createsubscriber can run without --databases option
+command_ok(
+       [
+               'pg_createsubscriber', '--verbose',
+               '--dry-run', '--pgdata',
+               $node_s->data_dir, '--publisher-server',
+               $node_p->connstr('pg1'), '--subscriber-server',
+               $node_s->connstr('pg1')
+       ],
+       'run pg_createsubscriber without --databases');
+

Hmm, in case of --dry-run, the server would be never shut down.
See below part.

```
if (!dry_run)
stop_standby_server(pg_ctl_path, opt.subscriber_dir);
```

One way to differentiate whether the server is run in dry_run mode or
not is to check if the server was stopped or not. So I mean we can
check that the stdout does not have a "postmaster was stopped" message
from the stdout. Can we add validation based on this code:
+       if (action)
+               pg_log_info("postmaster was started");

Or another way is to check pg_ctl status to see that the server is not shutdown.

3) This message "target server must be running" seems to be wrong,
should it be cannot specify cascading replicating standby as standby
node(this is for v22-0011 patch :
+               'pg_createsubscriber', '--verbose', '--pgdata',
$node_c->data_dir,
+               '--publisher-server', $node_s->connstr('postgres'),
+               '--port', $node_c->port, '--socketdir', $node_c->host,
+               '--database', 'postgres'
],
-       'primary server is in recovery');
+       1,
+       [qr//],
+       [qr/primary server cannot be in recovery/],
+       'target server must be running');

Fixed.

4) Should this be "Wait for subscriber to catch up"
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);

Fixed.

5) Should this be 'Subscriptions has been created on all the specified
databases'
+);
+is($result, qq(2),
+       'Subscriptions has been created to all the specified databases'
+);

Fixed, but "has" should be "have".

6) Add test to verify current_user is not a member of
ROLE_PG_CREATE_SUBSCRIPTION, has no create permissions, has no
permissions to execution replication origin advance functions

7) Add tests to verify insufficient max_logical_replication_workers,
max_replication_slots and max_worker_processes for the subscription
node

8) Add tests to verify invalid configuration of wal_level,
max_replication_slots and max_wal_senders for the publisher node

Hmm, but adding these checks may increase the test time. we should
measure the time and then decide.

We can check and see if it does not take significantly more time, then
we can have these tests.

Regards,
Vignesh

#163Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#155)
Re: speed up a logical replica setup

On Thu, Feb 22, 2024, at 9:43 AM, Hayato Kuroda (Fujitsu) wrote:

The possible solution would be
1) allow to run pg_createsubscriber if standby is initially stopped .
I observed that pg_logical_createsubscriber also uses this approach.
2) read GUCs via SHOW command and restore them when server restarts

3. add a config-file option. That's similar to what pg_rewind does. I expect
that Debian-based installations will have this issue.

I also prefer the first solution.
Another reason why the standby should be stopped is for backup purpose.
Basically, the standby instance should be saved before running pg_createsubscriber.
An easiest way is hard-copy, and the postmaster should be stopped at that time.
I felt it is better that users can run the command immediately later the copying.
Thought?

It was not a good idea if you want to keep the postgresql.conf outside PGDATA.
I mean you need extra steps that can be error prone (different settings between
files).

Shlok, I didn't read your previous email carefully. :-/

--
Euler Taveira
EDB https://www.enterprisedb.com/

#164Euler Taveira
euler@eulerto.com
In reply to: Shlok Kyal (#153)
Re: speed up a logical replica setup

On Wed, Feb 21, 2024, at 5:00 AM, Shlok Kyal wrote:

I found some issues and fixed those issues with top up patches
v23-0012 and v23-0013
1.
Suppose there is a cascade physical replication node1->node2->node3.
Now if we run pg_createsubscriber with node1 as primary and node2 as
standby, pg_createsubscriber will be successful but the connection
between node2 and node3 will not be retained and log og node3 will
give error:
2024-02-20 12:32:12.340 IST [277664] FATAL: database system
identifier differs between the primary and standby
2024-02-20 12:32:12.340 IST [277664] DETAIL: The primary's identifier
is 7337575856950914038, the standby's identifier is
7337575783125171076.
2024-02-20 12:32:12.341 IST [277491] LOG: waiting for WAL to become
available at 0/3000F10

To fix this I am avoiding pg_createsubscriber to run if the standby
node is primary to any other server.
Made the change in v23-0012 patch

IIRC we already discussed the cascading replication scenario. Of course,
breaking a node is not good that's why you proposed v23-0012. However,
preventing pg_createsubscriber to run if there are standbys attached to it is
also annoying. If you don't access to these hosts you need to (a) kill
walsender (very fragile / unstable), (b) start with max_wal_senders = 0 or (3)
add a firewall rule to prevent that these hosts do not establish a connection
to the target server. I wouldn't like to include the patch as-is. IMO we need
at least one message explaining the situation to the user, I mean, add a hint
message. I'm resistant to a new option but probably a --force option is an
answer. There is no test coverage for it. I adjusted this patch (didn't include
the --force option) and add a test case.

2.
While checking 'max_replication_slots' in 'check_publisher' function,
we are not considering the temporary slot in the check:
+   if (max_repslots - cur_repslots < num_dbs)
+   {
+       pg_log_error("publisher requires %d replication slots, but
only %d remain",
+                    num_dbs, max_repslots - cur_repslots);
+       pg_log_error_hint("Consider increasing max_replication_slots
to at least %d.",
+                         cur_repslots + num_dbs);
+       return false;
+   }
Fixed this in v23-0013

Good catch!

Both are included in the next patch.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#165Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#164)
Re: speed up a logical replica setup

On Fri, Feb 23, 2024 at 8:16 AM Euler Taveira <euler@eulerto.com> wrote:

On Wed, Feb 21, 2024, at 5:00 AM, Shlok Kyal wrote:

I found some issues and fixed those issues with top up patches
v23-0012 and v23-0013
1.
Suppose there is a cascade physical replication node1->node2->node3.
Now if we run pg_createsubscriber with node1 as primary and node2 as
standby, pg_createsubscriber will be successful but the connection
between node2 and node3 will not be retained and log og node3 will
give error:
2024-02-20 12:32:12.340 IST [277664] FATAL: database system
identifier differs between the primary and standby
2024-02-20 12:32:12.340 IST [277664] DETAIL: The primary's identifier
is 7337575856950914038, the standby's identifier is
7337575783125171076.
2024-02-20 12:32:12.341 IST [277491] LOG: waiting for WAL to become
available at 0/3000F10

To fix this I am avoiding pg_createsubscriber to run if the standby
node is primary to any other server.
Made the change in v23-0012 patch

IIRC we already discussed the cascading replication scenario. Of course,
breaking a node is not good that's why you proposed v23-0012. However,
preventing pg_createsubscriber to run if there are standbys attached to it is
also annoying. If you don't access to these hosts you need to (a) kill
walsender (very fragile / unstable), (b) start with max_wal_senders = 0 or (3)
add a firewall rule to prevent that these hosts do not establish a connection
to the target server. I wouldn't like to include the patch as-is. IMO we need
at least one message explaining the situation to the user, I mean, add a hint
message.

Yeah, it could be a bit tricky for users to ensure that no follow-on
standby is present but I think it is still better to give an error and
prohibit running pg_createsubscriber than breaking the existing
replication. The possible solution, in this case, is to allow running
pg_basebackup via this tool or otherwise and then let the user use it
to convert to a subscriber. It would be good to keep things simple for
the first version then we can add such options like --force in
subsequent versions.

--
With Regards,
Amit Kapila.

#166Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: vignesh C (#152)
RE: speed up a logical replica setup

Dear Vignesh,

I forgot to reply one of the suggestion.

2) There is a CheckDataVersion function which does exactly this, will
we be able to use this:
+       /* Check standby server version */
+       if ((ver_fd = fopen(versionfile, "r")) == NULL)
+               pg_fatal("could not open file \"%s\" for reading: %m",
versionfile);
+
+       /* Version number has to be the first line read */
+       if (!fgets(rawline, sizeof(rawline), ver_fd))
+       {
+               if (!ferror(ver_fd))
+                       pg_fatal("unexpected empty file \"%s\"", versionfile);
+               else
+                       pg_fatal("could not read file \"%s\": %m", versionfile);
+       }
+
+       /* Strip trailing newline and carriage return */
+       (void) pg_strip_crlf(rawline);
+
+       if (strcmp(rawline, PG_MAJORVERSION) != 0)
+       {
+               pg_log_error("standby server is of wrong version");
+               pg_log_error_detail("File \"%s\" contains \"%s\",
which is not compatible with this program's version \"%s\".",
+                                                       versionfile,
rawline, PG_MAJORVERSION);
+               exit(1);
+       }
+
+       fclose(ver_fd);

This suggestion has been rejected because I was not sure the location of the
declaration and the implementation. Function which could be called from clients
must be declared in src/include/{common|fe_utils|utils} directory. I saw files
located at there but I could not appropriate location for CheckDataVersion.
Also, I did not think new file should be created only for this function.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#167Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#163)
RE: speed up a logical replica setup

Dear Euler,

The possible solution would be
1) allow to run pg_createsubscriber if standby is initially stopped .
I observed that pg_logical_createsubscriber also uses this approach.
2) read GUCs via SHOW command and restore them when server restarts

3. add a config-file option. That's similar to what pg_rewind does.

Sorry, which pg_rewind option did you mention? I cannot find.
IIUC, -l is an only option which can accept the path, but it is not related with us.

Also, I'm not sure the benefit to add as new options. Basically it should be less.
Is there benefits than read via SHOW? Even if I assume the pg_resetwal has such
an option, the reason is that the target postmaster for pg_resetwal must be stopped.

I expect
that Debian-based installations will have this issue.

I'm not familiar with the Debian-env, so can you explain the reason?

It was not a good idea if you want to keep the postgresql.conf outside PGDATA.
I mean you need extra steps that can be error prone (different settings between
files).

Yeah, if we use my approach, users who specify such GUCs may not be happy.
So...based on above discussion, we should choose either of below items. Thought?

a)
enforce the standby must be *stopped*, and options like config_file can be specified via option.
b)
enforce the standby must be *running*, options like config_file would be read via SHOW command.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#168Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#167)
RE: speed up a logical replica setup

Dear Euler,

Sorry, which pg_rewind option did you mention? I cannot find.
IIUC, -l is an only option which can accept the path, but it is not related with us.

Sorry, I found [1]https://www.postgresql.org/docs/current/app-pgrewind.html#:~:text=%2D%2Dconfig%2Dfile%3Dfilename. I was confused with pg_resetwal. But my opinion was not so changed.
The reason why --config-file exists is that pg_rewind requires that target must be stopped,
which is different from the current pg_createsubscriber. So I still do not like to add options.

[1]: https://www.postgresql.org/docs/current/app-pgrewind.html#:~:text=%2D%2Dconfig%2Dfile%3Dfilename
[2]: ``` The target server must be shut down cleanly before running pg_rewind ```
```
The target server must be shut down cleanly before running pg_rewind
```

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#169Andrew Dunstan
andrew@dunslane.net
In reply to: Hayato Kuroda (Fujitsu) (#168)
Re: speed up a logical replica setup

On 2024-02-27 Tu 05:02, Hayato Kuroda (Fujitsu) wrote:

Dear Euler,

Sorry, which pg_rewind option did you mention? I cannot find.
IIUC, -l is an only option which can accept the path, but it is not related with us.

Sorry, I found [1]. I was confused with pg_resetwal. But my opinion was not so changed.
The reason why --config-file exists is that pg_rewind requires that target must be stopped,
which is different from the current pg_createsubscriber. So I still do not like to add options.

[1]: https://www.postgresql.org/docs/current/app-pgrewind.html#:~:text=%2D%2Dconfig%2Dfile%3Dfilename
[2]:
```
The target server must be shut down cleanly before running pg_rewind
```

Even though that is a difference I'd still rather we did more or less
the same thing more or less the same way across utilities, so I agree
with Euler's suggestion.

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#170Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#156)
1 attachment(s)
Re: speed up a logical replica setup

On Thu, Feb 22, 2024, at 12:45 PM, Hayato Kuroda (Fujitsu) wrote:

Based on idea from Euler, I roughly implemented. Thought?

0001-0013 were not changed from the previous version.

V24-0014: addressed your comment in the replied e-mail.
V24-0015: Add disconnect_database() again, per [3]
V24-0016: addressed your comment in [4].
V24-0017: addressed your comment in [5].
V24-0018: addressed your comment in [6].

Thanks for your review. I'm attaching v25 that hopefully addresses all pending
points.

Regarding your comments [1]/messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com on v21, I included changes for almost all items
except 2, 20, 23, 24, and 25. It still think item 2 is not required because
pg_ctl will provide a suitable message. I decided not to rearrange the block of
SQL commands (item 20) mainly because it would avoid these objects on node_f.
Do we really need command_checks_all? Depending on the output it uses
additional cycles than command_ok.

In summary:

v24-0002: documentation is updated. I didn't apply this patch as-is. Instead, I
checked what you wrote and fix some gaps in what I've been written.
v24-0003: as I said I don't think we need to add it, however, I won't fight
against it if people want to add this check.
v24-0004: I spent some time on it. This patch is not completed. I cleaned it up
and include the start_standby_server code. It starts the server using the
specified socket directory, port and username, hence, preventing external client
connections during the execution.
v24-0005: partially applied
v24-0006: applied with cosmetic change
v24-0007: applied with cosmetic change
v24-0008: applied
v24-0009: applied with cosmetic change
v24-0010: not applied. Base on #15, I refactored this code a bit. pg_fatal is
not used when there is a database connection open. Instead, pg_log_error()
followed by disconnect_database(). In cases that it should exit immediately,
disconnect_database() has a new parameter (exit_on_error) that controls if it
needs to call exit(1). I go ahead and did the same for connect_database().
v24-0011: partially applied. I included some of the suggestions (18, 19, and 21).
v24-0012: not applied. Under reflection, after working on v24-0004, the target
server will start with new parameters (that only accepts local connections),
hence, during the execution is not possible anymore to detect if the target
server is a primary for another server. I added a sentence for it in the
documentation (Warning section).
v24-0013: good catch. Applied.
v24-0014: partially applied. After some experiments I decided to use a small
number of attempts. The current code didn't reset the counter if the connection
is reestablished. I included the documentation suggestion. I didn't include the
IF EXISTS in the DROP PUBLICATION because it doesn't solve the issue. Instead,
I refactored the drop_publication() to not try again if the DROP PUBLICATION
failed.
v24-0015: not applied. I refactored the exit code to do the right thing: print
error message, disconnect database (if applicable) and exit.
v24-0016: not applied. But checked if the information was presented in the
documentation; it is.
v24-0017: good catch. Applied.
v24-0018: not applied.

I removed almost all boolean return and include the error logic inside the
function. It reads better. I also changed the connect|disconnect_database
functions to include the error logic inside it. There is a new parameter
on_error_exit for it. I removed the action parameter from pg_ctl_status() -- I
think someone suggested it -- and the error message was moved to outside the
function. I improved the cleanup routine. It provides useful information if it
cannot remove the object (publication or replication slot) from the primary.

Since I applied v24-0004, I realized that extra start / stop service are
required. It mean pg_createsubscriber doesn't start the transformation with the
current standby settings. Instead, it stops the standby if it is running and
start it with the provided command-line options (socket, port,
listen_addresses). It has a few drawbacks:
* See v34-0012. It cannot detect if the target server is a primary for another
server. It is documented.
* I also removed the check for standby is running. If the standby was stopped a
long time ago, it will take some time to reach the start point.
* Dry run mode has to start / stop the service to work correctly. Is it an
issue?

However, I decided to include --retain option, I'm thinking about to remove it.
If the logging is enabled, the information during the pg_createsubscriber will
be available. The client log can be redirected to a file for future inspection.

Comments?

[1]: /messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v25-0001-pg_createsubscriber-creates-a-new-logical-replic.patchtext/x-patch; name="=?UTF-8?Q?v25-0001-pg=5Fcreatesubscriber-creates-a-new-logical-replic.pa?= =?UTF-8?Q?tch?="Download
From 41f222b68ab4e365be0123075d54d5da8468bcd2 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v25] pg_createsubscriber: creates a new logical replica from a
 standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber).

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
for each specified database. After that, it stops the target server. One
temporary replication slot is created to get the replication start
point. It is used as (a) a stopping point for the recovery process and
(b) a starting point for the subscriptions. Write recovery parameters
into the target data directory and start the target server. Wait until
the target server is promoted. Create one subscription per specified
database (using replication slot and publication created in a previous
step) on the target server. Set the replication progress to the
replication start point for each subscription. Enable the subscription
for each specified database on the target server. And finally, change
the system identifier on the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  523 +++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2038 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  214 ++
 8 files changed, 2805 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..44705b2c52
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,523 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. It must be run at the target server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because it speeds up the logical replication setup. For smaller
+   databases, <link linkend="logical-replication">initial data synchronization</link>
+   is recommended.
+  </para>
+
+  <para>
+   There are some prerequisites for <application>pg_createsubscriber</application>
+   to convert the target server into a logical replica. If these are not met an
+   error will be reported.
+  </para>
+
+  <itemizedlist id="app-pg-createsubscriber-description-prerequisites">
+   <listitem>
+    <para>
+     The source and target servers must have the same major version as the
+     <application>pg_createsubscriber</application>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The given target data directory must have the same system identifier than
+     the source data directory. If a standby server is running on the target
+     data directory or it is a base backup from the source data directory,
+     system identifiers are the same.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must be used as a physical standby.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The given database user for the target data directory must have privileges
+     for creating <link linkend="sql-createsubscription">subscriptions</link>
+     and using <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     and
+     <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+     configured to a value greater than or equal to the number of specified databases.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must have
+     <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+     configured to a value greater than the number of specified databases.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must accept local connections.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must accept connections from the target server.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must not be in recovery. Publications cannot be created
+     in a read-only cluster.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+     <literal>logical</literal>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     configured to a value greater than the number of specified databases plus
+     existing replication slots.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+     configured to a value greater than or equal to the number of specified
+     databases and existing <literal>walsender</literal> processes.
+    </para>
+   </listitem>
+  </itemizedlist>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    then the data directory is likely not in a state that can be recovered. It
+    is true if the target server was promoted. In such a case, creating a new
+    standby server is recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during the transformation steps.
+    Hence, connections to target server might fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Executing DDL commands on the source server while running
+    <application>pg_createsubscriber</application> is not recommended. If the
+    target server has already been converted to logical replica, the DDL
+    commands must not be replicated so an error would occur.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    should be removed. The removal might fail if the target server cannot
+    connect to the source server. In such a case, a warning message will inform
+    the objects left.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which WAL files from the source server might be used by the target server.
+    If the target server has a standby, replication will break and a fresh
+    standby should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connnections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is already running, stop it because some parameters can only
+     be set at server start.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. All
+     <link linkend="app-pg-createsubscriber-description-prerequisites">prerequisites</link>
+     are checked. If any of them are not met, <application>pg_createsubscriber</application>
+     will terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u</literal></quote> (parameter:
+     database <parameter>oid</parameter>). Each replication slot has the
+     following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a temporary replication slot to get a consistent start location.
+     This replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
+     (parameter: PID <parameter>int</parameter>). The LSN returned by
+     <link linkend="pg-create-logical-replication-slot"><function>pg_create_logical_replication_slot()</function></link>
+     is used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
+     parameter and by the subscriptions as a replication start point. It
+     guarantees that no transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies a LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are also added so it avoids unexpected behavior during the recovery
+     process such as end of the recovery as soon as a consistent state is
+     reached (WAL should be applied until the consistent start location) and
+     multiple recovery targets that can cause a failure. This step finishes
+     once the server ends standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     The subscription has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>). The
+     replication slot name is identical to the subscription name. It does not
+     copy existing data from the source server. It does not create a
+     replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the consistent LSN
+     but replication origin name contains the subscription oid in its name.
+     Hence, the subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the consistent start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the consistent start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the consistent start point. This is the exact LSN to be used
+     as a initial location for each subscription. The replication origin name
+     is obtained since the subscription was created. The replication origin
+     name and the consistent start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the consistent start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..e9a920dbcd 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..e70fc5dca0
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2038 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "common/username.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	50432
+#define	BASE_OUTPUT_DIR		"pg_createsubscriber_output.d"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	unsigned short sub_port;	/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+}			CreateSubscriberOptions;
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name / replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+}			LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path,
+									struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static void check_publisher(struct LogicalRepInfo *dbinfo);
+static void setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(struct CreateSubscriberOptions *opt,
+								 const char *pg_ctl_path, const char *logfile,
+								 bool with_options);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+								  struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static bool recovery_ended = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	PGconn	   *conn;
+	int			i;
+
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("pg_createsubscriber failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.");
+		pg_log_warning_hint("You must recreate the physical replica before continuing.");
+	}
+
+	for (i = 0; i < num_dbs; i++)
+	{
+
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].subname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %d)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	int			i = 0;
+
+	dbinfo = (struct LogicalRepInfo *) pg_malloc(num_dbs * sizeof(struct LogicalRepInfo));
+
+	for (SimpleStringListCell *cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): connection string: %s", i, dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): connection string: %s", i, dbinfo[i].subconninfo);
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static void
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+
+		/* Remember database OID */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static void
+check_publisher(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted +
+	 *                            one temporary logical replication slot
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+		pg_fatal("publisher requires wal_level >= logical");
+
+	/* One additional temporary logical replication slot */
+	if (max_repslots - cur_repslots < num_dbs + 1)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs + 1, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs + 1);
+		exit(1);
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		exit(1);
+	}
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static void
+check_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("The target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		disconnect_database(conn, true);
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		disconnect_database(conn, true);
+	}
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		disconnect_database(conn, true);
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		disconnect_database(conn, true);
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		disconnect_database(conn, true);
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		disconnect_database(conn, true);
+	}
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/* This temporary replication slot is only used for catchup purposes */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it
+ * starts the transformation.
+ * In dry run mode, doesn't create the BASE_OUTPUT_DIR directory, instead
+ * returns the full log file path.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, BASE_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (!dry_run && mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* Append timestamp with ISO 8601 format */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/server_start_%s.log", base_dir, timebuf);
+	pg_log_debug("log file is: %s", filename);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_path,
+					 const char *logfile, bool with_options)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	char		socket_string[MAXPGPATH + 200];
+	int			rc;
+
+	socket_string[0] = '\0';
+
+#if !defined(WIN32)
+
+	/*
+	 * An empty listen_addresses list means the server does not listen on any
+	 * IP interfaces; only Unix-domain sockets can be used to connect to the
+	 * server. Prevent external connections to minimize the chance of failure.
+	 */
+	strcat(socket_string,
+		   " -c listen_addresses='' -c unix_socket_permissions=0700");
+
+	if (opt->socket_dir)
+		snprintf(socket_string + strlen(socket_string),
+				 sizeof(socket_string) - strlen(socket_string),
+				 " -c unix_socket_directories='%s'",
+				 opt->socket_dir);
+#endif
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, opt->subscriber_dir);
+	if (with_options)
+	{
+		/*
+		 * Don't include the log file in dry run mode because the directory
+		 * that contains it was not created in setup_server_logfile().
+		 */
+		if (!dry_run)
+			appendPQExpBuffer(pg_ctl_cmd, " -l \"%s\"", logfile);
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %u%s\"",
+						  opt->sub_port, socket_string);
+	}
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+					  struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	5
+
+	pg_log_info("waiting the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"retain", no_argument, NULL, 'r'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_ctl_path = NULL;
+	char	   *pg_resetwal_path = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo = NULL;
+	char	   *sub_base_conninfo = NULL;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+
+	PQExpBuffer sub_conninfo_str = createPQExpBuffer();
+	PQExpBuffer recoveryconfcontents = NULL;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:nP:rS:t:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(opt.subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				if ((opt.sub_port = atoi(optarg)) <= 0)
+					pg_fatal("invalid subscriber port number");
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Any non-option arguments?
+	 */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * Required arguments
+	 */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/*
+	 * If socket directory is not provided, use the current directory.
+	 */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 *
+	 * If subscriber username is not provided, check if the environment
+	 * variable sets it. If not, obtain the operating system name of the user
+	 * running it.
+	 */
+	if (opt.sub_username == NULL)
+	{
+		char	   *errstr = NULL;
+
+		if (getenv("PGUSER"))
+		{
+			opt.sub_username = getenv("PGUSER");
+		}
+		else
+		{
+			opt.sub_username = get_user_name(&errstr);
+			if (errstr)
+				pg_fatal("%s", errstr);
+		}
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+	appendPQExpBuffer(sub_conninfo_str, "host=%s port=%u user=%s fallback_application_name=%s",
+					  opt.socket_dir, opt.sub_port, opt.sub_username, progname);
+	sub_base_conninfo = get_base_conninfo(sub_conninfo_str->data, NULL);
+	if (sub_base_conninfo == NULL)
+		exit(1);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(opt.subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Create the output directory to store any data generated by this tool */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * If the standby server is running, stop it. Some parameters (that can
+	 * only be set at server start) are informed by command-line options.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, pg_ctl_path, server_start_log, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	setup_publisher(dbinfo);
+
+	/*
+	 * Create a temporary logical replication slot to get a consistent LSN.
+	 *
+	 * This consistent LSN will be used later to advanced the recently created
+	 * replication slots. It is ok to use a temporary replication slot here
+	 * because it will have a short lifetime and it is only used as a mark to
+	 * start the logical replication.
+	 *
+	 * XXX we should probably use the last created replication slot to get a
+	 * consistent LSN but it should be changed after adding pg_basebackup
+	 * support.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+	consistent_lsn = create_logical_replication_slot(conn, &dbinfo[0], true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	/*
+	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("stopping and starting the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	start_standby_server(&opt, pg_ctl_path, server_start_log, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/*
+	 * If the primary_slot_name exists on primary, drop it.
+	 *
+	 * XXX we might not fail here. Instead, we provide a warning so the user
+	 * eventually drops this replication slot later.
+	 */
+	if (primary_slot_name != NULL)
+	{
+		conn = connect_database(dbinfo[0].pubconninfo, false);
+		if (conn != NULL)
+		{
+			drop_replication_slot(conn, &dbinfo[0], primary_slot_name);
+			disconnect_database(conn, false);
+		}
+		else
+		{
+			pg_log_warning("could not drop replication slot \"%s\" on primary",
+						   primary_slot_name);
+			pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+		}
+	}
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(pg_resetwal_path, &opt);
+
+	/*
+	 * In dry run mode, the server is restarted with the provided command-line
+	 * options so validation can be applied in the target server. In order to
+	 * preserve the initial state of the server (running), start it without
+	 * the command-line options.
+	 */
+	if (dry_run)
+		start_standby_server(&opt, pg_ctl_path, NULL, false);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!dry_run && !opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..5a2b8e9a56
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,214 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Stop node C
+$node_c->teardown_node;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+ok( -d $node_s->data_dir . "/pg_createsubscriber_output.d",
+	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success"
+);
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.30.2

#171Shubham Khanna
khannashubham1197@gmail.com
In reply to: Euler Taveira (#170)
Re: speed up a logical replica setup

On Sat, Mar 2, 2024 at 2:19 AM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Feb 22, 2024, at 12:45 PM, Hayato Kuroda (Fujitsu) wrote:

Based on idea from Euler, I roughly implemented. Thought?

0001-0013 were not changed from the previous version.

V24-0014: addressed your comment in the replied e-mail.
V24-0015: Add disconnect_database() again, per [3]
V24-0016: addressed your comment in [4].
V24-0017: addressed your comment in [5].
V24-0018: addressed your comment in [6].

Thanks for your review. I'm attaching v25 that hopefully addresses all pending
points.

Regarding your comments [1] on v21, I included changes for almost all items
except 2, 20, 23, 24, and 25. It still think item 2 is not required because
pg_ctl will provide a suitable message. I decided not to rearrange the block of
SQL commands (item 20) mainly because it would avoid these objects on node_f.
Do we really need command_checks_all? Depending on the output it uses
additional cycles than command_ok.

In summary:

v24-0002: documentation is updated. I didn't apply this patch as-is. Instead, I
checked what you wrote and fix some gaps in what I've been written.
v24-0003: as I said I don't think we need to add it, however, I won't fight
against it if people want to add this check.
v24-0004: I spent some time on it. This patch is not completed. I cleaned it up
and include the start_standby_server code. It starts the server using the
specified socket directory, port and username, hence, preventing external client
connections during the execution.
v24-0005: partially applied
v24-0006: applied with cosmetic change
v24-0007: applied with cosmetic change
v24-0008: applied
v24-0009: applied with cosmetic change
v24-0010: not applied. Base on #15, I refactored this code a bit. pg_fatal is
not used when there is a database connection open. Instead, pg_log_error()
followed by disconnect_database(). In cases that it should exit immediately,
disconnect_database() has a new parameter (exit_on_error) that controls if it
needs to call exit(1). I go ahead and did the same for connect_database().
v24-0011: partially applied. I included some of the suggestions (18, 19, and 21).
v24-0012: not applied. Under reflection, after working on v24-0004, the target
server will start with new parameters (that only accepts local connections),
hence, during the execution is not possible anymore to detect if the target
server is a primary for another server. I added a sentence for it in the
documentation (Warning section).
v24-0013: good catch. Applied.
v24-0014: partially applied. After some experiments I decided to use a small
number of attempts. The current code didn't reset the counter if the connection
is reestablished. I included the documentation suggestion. I didn't include the
IF EXISTS in the DROP PUBLICATION because it doesn't solve the issue. Instead,
I refactored the drop_publication() to not try again if the DROP PUBLICATION
failed.
v24-0015: not applied. I refactored the exit code to do the right thing: print
error message, disconnect database (if applicable) and exit.
v24-0016: not applied. But checked if the information was presented in the
documentation; it is.
v24-0017: good catch. Applied.
v24-0018: not applied.

I removed almost all boolean return and include the error logic inside the
function. It reads better. I also changed the connect|disconnect_database
functions to include the error logic inside it. There is a new parameter
on_error_exit for it. I removed the action parameter from pg_ctl_status() -- I
think someone suggested it -- and the error message was moved to outside the
function. I improved the cleanup routine. It provides useful information if it
cannot remove the object (publication or replication slot) from the primary.

Since I applied v24-0004, I realized that extra start / stop service are
required. It mean pg_createsubscriber doesn't start the transformation with the
current standby settings. Instead, it stops the standby if it is running and
start it with the provided command-line options (socket, port,
listen_addresses). It has a few drawbacks:
* See v34-0012. It cannot detect if the target server is a primary for another
server. It is documented.
* I also removed the check for standby is running. If the standby was stopped a
long time ago, it will take some time to reach the start point.
* Dry run mode has to start / stop the service to work correctly. Is it an
issue?

However, I decided to include --retain option, I'm thinking about to remove it.
If the logging is enabled, the information during the pg_createsubscriber will
be available. The client log can be redirected to a file for future inspection.

Comments?

I applied the v25 patch and did RUN the 'pg_createsubscriber' command.
I was unable to execute it and experienced the following error:

$ ./pg_createsubscriber -D node2/ -P "host=localhost port=5432
dbname=postgres" -d postgres -d db1 -p 9000 -r
./pg_createsubscriber: invalid option -- 'p'
pg_createsubscriber: hint: Try "pg_createsubscriber --help" for more
information.

Here, the --p is not accepting the desired port number. Thoughts?

Thanks and Regards,
Shubham Khanna.

#172Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Shubham Khanna (#171)
Re: speed up a logical replica setup

Hi,

I applied the v25 patch and did RUN the 'pg_createsubscriber' command.
I was unable to execute it and experienced the following error:

$ ./pg_createsubscriber -D node2/ -P "host=localhost port=5432
dbname=postgres" -d postgres -d db1 -p 9000 -r
./pg_createsubscriber: invalid option -- 'p'
pg_createsubscriber: hint: Try "pg_createsubscriber --help" for more
information.

Here, the --p is not accepting the desired port number. Thoughts?

I investigated it and found that:

+ while ((c = getopt_long(argc, argv, "d:D:nP:rS:t:v",
+             long_options, &option_index)) != -1)
+ {

Here 'p', 's' and 'U' options are missing so we are getting the error.
Also, I think the 'S' option should be removed from here.

I also see that specifying long options like --subscriber-port,
--subscriber-username, --socket-directory works fine.
Thoughts?

Thanks and regards,
Shlok Kyal

#173Euler Taveira
euler@eulerto.com
In reply to: Shubham Khanna (#171)
Re: speed up a logical replica setup

On Tue, Mar 5, 2024, at 12:48 AM, Shubham Khanna wrote:

I applied the v25 patch and did RUN the 'pg_createsubscriber' command.
I was unable to execute it and experienced the following error:

$ ./pg_createsubscriber -D node2/ -P "host=localhost port=5432
dbname=postgres" -d postgres -d db1 -p 9000 -r
./pg_createsubscriber: invalid option -- 'p'
pg_createsubscriber: hint: Try "pg_createsubscriber --help" for more
information.

Oops. Good catch! I will post an updated patch soon.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#174Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#170)
1 attachment(s)
RE: speed up a logical replica setup

Dear Euler,

Thanks for updating the patch!

v24-0003: as I said I don't think we need to add it, however, I won't fight
against it if people want to add this check.

OK, let's wait comments from senior members.

Since I applied v24-0004, I realized that extra start / stop service are
required. It mean pg_createsubscriber doesn't start the transformation with the
current standby settings. Instead, it stops the standby if it is running and
start it with the provided command-line options (socket, port,
listen_addresses). It has a few drawbacks:
* See v34-0012. It cannot detect if the target server is a primary for another
server. It is documented.

Yeah, It is a collateral damage.

* I also removed the check for standby is running. If the standby was stopped a
long time ago, it will take some time to reach the start point.
* Dry run mode has to start / stop the service to work correctly. Is it an
issue?

One concern (see below comment) is that -l option would not be passed even if
the standby has been logging before running pg_createsubscriber. Also, some settings
passed by pg_ctl start -o .... would not be restored.

However, I decided to include --retain option, I'm thinking about to remove it.
If the logging is enabled, the information during the pg_createsubscriber will
be available. The client log can be redirected to a file for future inspection.

Just to confirm - you meant to say like below, right?
* the client output would be redirected, and
* -r option would be removed.

Here are my initial comments for v25-0001. I read new doc and looks very good.
I may do reviewing more about v25-0001, but feel free to revise.

01. cleanup_objects_atexit
```
PGconn *conn;
int i;
```
The declaration *conn can be in the for-loop. Also, the declaration of the indicator can be in the bracket.

02. cleanup_objects_atexit
```

/*
* If a connection could not be established, inform the user
* that some objects were left on primary and should be
* removed before trying again.
*/
if (dbinfo[i].made_publication)
{
pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
dbinfo[i].pubname, dbinfo[i].dbname);
pg_log_warning_hint("Consider dropping this publication before trying again.");
}
if (dbinfo[i].made_replslot)
{
pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
dbinfo[i].subname, dbinfo[i].dbname);
pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
}
```

Not sure which is better, but we may able to the list to the concrete file like pg_upgrade.
(I thought it had been already discussed, but could not find from the archive. Sorry if it was a duplicated comment)

03. main
```
while ((c = getopt_long(argc, argv, "d:D:nP:rS:t:v",
long_options, &option_index)) != -1)
```

Missing update for __shortopts.

04. main
```
case 'D':
opt.subscriber_dir = pg_strdup(optarg);
canonicalize_path(opt.subscriber_dir);
break;
...
case 'P':
opt.pub_conninfo_str = pg_strdup(optarg);
break;
...
case 's':
opt.socket_dir = pg_strdup(optarg);
break;
...
case 'U':
opt.sub_username = pg_strdup(optarg);
break;
```

Should we consider the case these options would be specified twice?
I.e., should we call pg_free() before the substitution?

05. main

Missing canonicalize_path() to the socket_dir.

06. main
```
/*
* If socket directory is not provided, use the current directory.
*/
```

One-line comment can be used. Period can be also removed at that time.

07. main
```
/*
*
* If subscriber username is not provided, check if the environment
* variable sets it. If not, obtain the operating system name of the user
* running it.
*/
```
Unnecessary blank.

08. main
```
char *errstr = NULL;
```

This declaration can be at else-part.

09. main.

Also, as the first place, do we have to get username if not specified?
I felt libpq can handle the case if we skip passing the info.

10. main
```
appendPQExpBuffer(sub_conninfo_str, "host=%s port=%u user=%s fallback_application_name=%s",
opt.socket_dir, opt.sub_port, opt.sub_username, progname);
sub_base_conninfo = get_base_conninfo(sub_conninfo_str->data, NULL);
```

Is it really needed to call get_base_conninfo? I think no need to define
sub_base_conninfo.

11. main

```
/*
* In dry run mode, the server is restarted with the provided command-line
* options so validation can be applied in the target server. In order to
* preserve the initial state of the server (running), start it without
* the command-line options.
*/
if (dry_run)
start_standby_server(&opt, pg_ctl_path, NULL, false);
```

I think initial state of the server may be stopped. Now both conditions are allowed.
And I think it is not good not to specify the logfile.

12. others

As Peter E pointed out [1]/messages/by-id/b9aa614c-84ba-a869-582f-8d5e3ab57424@enterprisedb.com, the main function is still huge. It has more than 400 lines.
I think all functions should have less than 100 line to keep the readability.

I considered separation idea like below. Note that this may require to change
orderings. How do you think?

* add parse_command_options() which accepts user options and verifies them
* add verification_phase() or something which checks system identifier and calls check_XXX
* add catchup_phase() or something which creates a temporary slot, writes recovery parameters,
and wait until the end of recovery
* add cleanup_phase() or something which removes primary_slot and modifies the
system identifier
* stop/start server can be combined into one wrapper.

Attached txt file is proofs the concept.

13. others

PQresultStatus(res) is called 17 times in this source code, it may be redundant.
I think we can introduce a function like executeQueryOrDie() and gather in one place.

14. others

I found that pg_createsubscriber does not refer functions declared in other files.
Is there a possibility to use them, e.g., streamutils.h?

15. others

While reading the old discussions [2]/messages/by-id/9fd3018d-0e5f-4507-aee6-efabfb5a4440@app.fastmail.com, Amit suggested to keep the comment and avoid
creating a temporary slot. You said "Got it" but temp slot still exists.
Is there any reason? Can you clarify your opinion?

16. others

While reading [2]/messages/by-id/9fd3018d-0e5f-4507-aee6-efabfb5a4440@app.fastmail.com and [3]/messages/by-id/CAA4eK1L+E-bdKaOMSw-yWizcuprKMyeejyOwWjq_57=Uqh-f+g@mail.gmail.com, I was confused the decision. You and Amit discussed
the combination with pg_createsubscriber and slot sync and how should handle
slots on the physical standby. You seemed to agree to remove such a slot, and
Amit also suggested to raise an ERROR. However, you said in [8]/messages/by-id/be92c57b-82e1-4920-ac31-a8a04206db7b@app.fastmail.com that such
handlings is not mandatory so should raise an WARNING in dry_run. I was quite confused.
Am I missing something?

17. others

Per discussion around [4]/messages/by-id/TYCPR01MB12077B63D81B49E9DFD323661F55A2@TYCPR01MB12077.jpnprd01.prod.outlook.com, we might have to consider an if the some options like
data_directory and config_file was initially specified for standby server. Another
easy approach is to allow users to specify options like -o in pg_upgrade [5]https://www.postgresql.org/docs/devel/pgupgrade.html#:~:text=options%20to%20be%20passed%20directly%20to%20the%20old%20postgres%20command%3B%20multiple%20option%20invocations%20are%20appended,
which is similar to your idea. Thought?

18. others

How do you handle the reported failure [6]/messages/by-id/CAHv8Rj+5mzK9Jt+7ECogJzfm5czvDCCd5jO1_rCx0bTEYpBE5g@mail.gmail.com?

19. main
```
char *pub_base_conninfo = NULL;
char *sub_base_conninfo = NULL;
char *dbname_conninfo = NULL;
```

No need to initialize pub_base_conninfo and sub_base_conninfo.
These variables would not be free'd.

20. others

IIUC, slot creations would not be finished if there are prepared transactions.
Should we detect it on the verification phase and raise an ERROR?

21. others

As I said in [7]/messages/by-id/OS3PR01MB98828B15DD9502C91E0C50D7F57D2@OS3PR01MB9882.jpnprd01.prod.outlook.com, the catch up would not be finished if long recovery_min_apply_delay
is used. Should we overwrite during the catch up?

22. pg_createsubscriber.sgml
```
<para>
Check
Write recovery parameters into the target data...
```

Not sure, but "Check" seems not needed.

[1]: /messages/by-id/b9aa614c-84ba-a869-582f-8d5e3ab57424@enterprisedb.com
[2]: /messages/by-id/9fd3018d-0e5f-4507-aee6-efabfb5a4440@app.fastmail.com
[3]: /messages/by-id/CAA4eK1L+E-bdKaOMSw-yWizcuprKMyeejyOwWjq_57=Uqh-f+g@mail.gmail.com
[4]: /messages/by-id/TYCPR01MB12077B63D81B49E9DFD323661F55A2@TYCPR01MB12077.jpnprd01.prod.outlook.com
[5]: https://www.postgresql.org/docs/devel/pgupgrade.html#:~:text=options%20to%20be%20passed%20directly%20to%20the%20old%20postgres%20command%3B%20multiple%20option%20invocations%20are%20appended
[6]: /messages/by-id/CAHv8Rj+5mzK9Jt+7ECogJzfm5czvDCCd5jO1_rCx0bTEYpBE5g@mail.gmail.com
[7]: /messages/by-id/OS3PR01MB98828B15DD9502C91E0C50D7F57D2@OS3PR01MB9882.jpnprd01.prod.outlook.com
[8]: /messages/by-id/be92c57b-82e1-4920-ac31-a8a04206db7b@app.fastmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

Attachments:

0001-Shorten-main-function.txttext/plain; name=0001-Shorten-main-function.txtDownload
From 5866926dd581881af6b75c41e858125f9427b4e6 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 6 Mar 2024 06:58:48 +0000
Subject: [PATCH] Shorten main function

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 516 +++++++++++---------
 1 file changed, 281 insertions(+), 235 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index e70fc5dca0..80d76a78ce 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -70,8 +70,7 @@ static PGconn *connect_database(const char *conninfo, bool exit_on_error);
 static void disconnect_database(PGconn *conn, bool exit_on_error);
 static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
-static void modify_subscriber_sysid(const char *pg_resetwal_path,
-									struct CreateSubscriberOptions *opt);
+static void modify_subscriber_sysid(struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static void check_publisher(struct LogicalRepInfo *dbinfo);
 static void setup_publisher(struct LogicalRepInfo *dbinfo);
@@ -86,10 +85,12 @@ static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
 static char *setup_server_logfile(const char *datadir);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
 static void start_standby_server(struct CreateSubscriberOptions *opt,
-								 const char *pg_ctl_path, const char *logfile,
+								 const char *logfile,
 								 bool with_options);
-static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
-static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+static void stop_standby_server(const char *datadir);
+static void restart_server(struct CreateSubscriberOptions *options,
+						   const char *logfile)
+static void wait_for_end_recovery(const char *conninfo,
 								  struct CreateSubscriberOptions *opt);
 static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
@@ -97,11 +98,20 @@ static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
 static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
 									 const char *lsn);
 static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void parse_command_option(int argc, char **argv,
+								 struct CreateSubscriberOptions *options);
+static void verification_phase(struct CreateSubscriberOptions *options);
+static char *catchup_phase(struct CreateSubscriberOptions *options,
+						   char *server_start_log);
+static void cleanup_phase(struct CreateSubscriberOptions *options,
+						  char *server_start_log);
 
 #define	USEC_PER_SEC	1000000
 #define	WAIT_INTERVAL	1		/* 1 second */
 
 static const char *progname;
+static const char *pg_ctl_path;
+static const char *pg_resetwal_path;
 
 static char *primary_slot_name = NULL;
 static bool dry_run = false;
@@ -521,7 +531,7 @@ get_standby_sysid(const char *datadir)
  * files from one of the systems might be used in the other one.
  */
 static void
-modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOptions *opt)
+modify_subscriber_sysid(struct CreateSubscriberOptions *opt)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
@@ -1163,8 +1173,8 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc)
 }
 
 static void
-start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_path,
-					 const char *logfile, bool with_options)
+start_standby_server(struct CreateSubscriberOptions *opt, const char *logfile,
+					 bool with_options)
 {
 	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
 	char		socket_string[MAXPGPATH + 200];
@@ -1210,7 +1220,7 @@ start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_pat
 }
 
 static void
-stop_standby_server(const char *pg_ctl_path, const char *datadir)
+stop_standby_server(const char *datadir)
 {
 	char	   *pg_ctl_cmd;
 	int			rc;
@@ -1223,6 +1233,25 @@ stop_standby_server(const char *pg_ctl_path, const char *datadir)
 	pg_log_info("server was stopped");
 }
 
+/*
+ * Wrapper for stop_standby_server() and start_standby_server() 
+ */
+static void
+restart_server(struct CreateSubscriberOptions *options, const char *logfile)
+{
+	struct stat statbuf;
+	char		pidfile[MAXPGPATH];
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", options->subscriber_dir);
+
+	/* If the standby server is running, stop it */
+	if (stat(pidfile, &statbuf) == 0)
+		stop_standby_server(options->subscriber_dir);
+
+	start_standby_server(options, logfile, true);
+}
+
 /*
  * Returns after the server finishes the recovery process.
  *
@@ -1230,7 +1259,7 @@ stop_standby_server(const char *pg_ctl_path, const char *datadir)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+wait_for_end_recovery(const char *conninfo,
 					  struct CreateSubscriberOptions *opt)
 {
 	PGconn	   *conn;
@@ -1272,7 +1301,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 		{
 			if (++count > NUM_CONN_ATTEMPTS)
 			{
-				stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+				stop_standby_server(opt->subscriber_dir);
 				pg_log_error("standby server disconnected from the primary");
 				break;
 			}
@@ -1285,7 +1314,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 		/* Bail out after recovery_timeout seconds if this option is set */
 		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
 		{
-			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			stop_standby_server(opt->subscriber_dir);
 			pg_log_error("recovery timed out");
 			disconnect_database(conn, true);
 		}
@@ -1581,165 +1610,20 @@ enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
 	destroyPQExpBuffer(str);
 }
 
-int
-main(int argc, char **argv)
+/*
+ * Verify the input arguments are appropriate.
+ */
+static void
+verify_input_arguments(struct CreateSubscriberOptions *options)
 {
-	static struct option long_options[] =
-	{
-		{"database", required_argument, NULL, 'd'},
-		{"pgdata", required_argument, NULL, 'D'},
-		{"dry-run", no_argument, NULL, 'n'},
-		{"subscriber-port", required_argument, NULL, 'p'},
-		{"publisher-server", required_argument, NULL, 'P'},
-		{"retain", no_argument, NULL, 'r'},
-		{"socket-directory", required_argument, NULL, 's'},
-		{"recovery-timeout", required_argument, NULL, 't'},
-		{"subscriber-username", required_argument, NULL, 'U'},
-		{"verbose", no_argument, NULL, 'v'},
-		{"version", no_argument, NULL, 'V'},
-		{"help", no_argument, NULL, '?'},
-		{NULL, 0, NULL, 0}
-	};
-
-	struct CreateSubscriberOptions opt = {0};
-
-	int			c;
-	int			option_index;
-
-	char	   *pg_ctl_path = NULL;
-	char	   *pg_resetwal_path = NULL;
-
-	char	   *server_start_log;
-
-	char	   *pub_base_conninfo = NULL;
-	char	   *sub_base_conninfo = NULL;
 	char	   *dbname_conninfo = NULL;
-
-	uint64		pub_sysid;
-	uint64		sub_sysid;
-	struct stat statbuf;
-
-	PGconn	   *conn;
-	char	   *consistent_lsn;
-
-	PQExpBuffer sub_conninfo_str = createPQExpBuffer();
-	PQExpBuffer recoveryconfcontents = NULL;
-
-	char		pidfile[MAXPGPATH];
-
-	pg_logging_init(argv[0]);
-	pg_logging_set_level(PG_LOG_WARNING);
-	progname = get_progname(argv[0]);
-	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
-
-	if (argc > 1)
-	{
-		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
-		{
-			usage();
-			exit(0);
-		}
-		else if (strcmp(argv[1], "-V") == 0
-				 || strcmp(argv[1], "--version") == 0)
-		{
-			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
-			exit(0);
-		}
-	}
-
-	/* Default settings */
-	opt.subscriber_dir = NULL;
-	opt.pub_conninfo_str = NULL;
-	opt.socket_dir = NULL;
-	opt.sub_port = DEFAULT_SUB_PORT;
-	opt.sub_username = NULL;
-	opt.database_names = (SimpleStringList)
-	{
-		NULL, NULL
-	};
-	opt.retain = false;
-	opt.recovery_timeout = 0;
-
-	/*
-	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
-	 * it either.
-	 */
-#ifndef WIN32
-	if (geteuid() == 0)
-	{
-		pg_log_error("cannot be executed by \"root\"");
-		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
-						  progname);
-		exit(1);
-	}
-#endif
-
-	get_restricted_token();
-
-	while ((c = getopt_long(argc, argv, "d:D:nP:rS:t:v",
-							long_options, &option_index)) != -1)
-	{
-		switch (c)
-		{
-			case 'd':
-				/* Ignore duplicated database names */
-				if (!simple_string_list_member(&opt.database_names, optarg))
-				{
-					simple_string_list_append(&opt.database_names, optarg);
-					num_dbs++;
-				}
-				break;
-			case 'D':
-				opt.subscriber_dir = pg_strdup(optarg);
-				canonicalize_path(opt.subscriber_dir);
-				break;
-			case 'n':
-				dry_run = true;
-				break;
-			case 'p':
-				if ((opt.sub_port = atoi(optarg)) <= 0)
-					pg_fatal("invalid subscriber port number");
-				break;
-			case 'P':
-				opt.pub_conninfo_str = pg_strdup(optarg);
-				break;
-			case 'r':
-				opt.retain = true;
-				break;
-			case 's':
-				opt.socket_dir = pg_strdup(optarg);
-				break;
-			case 't':
-				opt.recovery_timeout = atoi(optarg);
-				break;
-			case 'U':
-				opt.sub_username = pg_strdup(optarg);
-				break;
-			case 'v':
-				pg_logging_increase_verbosity();
-				break;
-			default:
-				/* getopt_long already emitted a complaint */
-				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-				exit(1);
-		}
-	}
-
-	/*
-	 * Any non-option arguments?
-	 */
-	if (optind < argc)
-	{
-		pg_log_error("too many command-line arguments (first is \"%s\")",
-					 argv[optind]);
-		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
-		exit(1);
-	}
+	char	   *pub_base_conninfo;
+	PQExpBuffer	sub_conninfo_str = createPQExpBuffer();
 
 	/*
 	 * Required arguments
 	 */
-	if (opt.subscriber_dir == NULL)
+	if (options->subscriber_dir == NULL)
 	{
 		pg_log_error("no subscriber data directory specified");
 		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -1749,14 +1633,14 @@ main(int argc, char **argv)
 	/*
 	 * If socket directory is not provided, use the current directory.
 	 */
-	if (opt.socket_dir == NULL)
+	if (options->socket_dir == NULL)
 	{
 		char		cwd[MAXPGPATH];
 
 		if (!getcwd(cwd, MAXPGPATH))
 			pg_fatal("could not determine current directory");
-		opt.socket_dir = pg_strdup(cwd);
-		canonicalize_path(opt.socket_dir);
+		options->socket_dir = pg_strdup(cwd);
+		canonicalize_path(options->socket_dir);
 	}
 
 	/*
@@ -1765,17 +1649,17 @@ main(int argc, char **argv)
 	 * variable sets it. If not, obtain the operating system name of the user
 	 * running it.
 	 */
-	if (opt.sub_username == NULL)
+	if (options->sub_username == NULL)
 	{
 		char	   *errstr = NULL;
 
 		if (getenv("PGUSER"))
 		{
-			opt.sub_username = getenv("PGUSER");
+			options->sub_username = getenv("PGUSER");
 		}
 		else
 		{
-			opt.sub_username = get_user_name(&errstr);
+			options->sub_username = get_user_name(&errstr);
 			if (errstr)
 				pg_fatal("%s", errstr);
 		}
@@ -1785,7 +1669,7 @@ main(int argc, char **argv)
 	 * Parse connection string. Build a base connection string that might be
 	 * reused by multiple databases.
 	 */
-	if (opt.pub_conninfo_str == NULL)
+	if (options->pub_conninfo_str == NULL)
 	{
 		/*
 		 * TODO use primary_conninfo (if available) from subscriber and
@@ -1798,19 +1682,16 @@ main(int argc, char **argv)
 		exit(1);
 	}
 	pg_log_info("validating connection string on publisher");
-	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+	pub_base_conninfo = get_base_conninfo(options->pub_conninfo_str,
 										  &dbname_conninfo);
 	if (pub_base_conninfo == NULL)
 		exit(1);
 
 	pg_log_info("validating connection string on subscriber");
 	appendPQExpBuffer(sub_conninfo_str, "host=%s port=%u user=%s fallback_application_name=%s",
-					  opt.socket_dir, opt.sub_port, opt.sub_username, progname);
-	sub_base_conninfo = get_base_conninfo(sub_conninfo_str->data, NULL);
-	if (sub_base_conninfo == NULL)
-		exit(1);
+					  options->socket_dir, options->sub_port, options->sub_username, progname);
 
-	if (opt.database_names.head == NULL)
+	if (options->database_names.head == NULL)
 	{
 		pg_log_info("no database was specified");
 
@@ -1821,7 +1702,7 @@ main(int argc, char **argv)
 		 */
 		if (dbname_conninfo)
 		{
-			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			simple_string_list_append(&options->database_names, dbname_conninfo);
 			num_dbs++;
 
 			pg_log_info("database \"%s\" was extracted from the publisher connection string",
@@ -1836,58 +1717,134 @@ main(int argc, char **argv)
 		}
 	}
 
-	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
-	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
-	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
-
 	/* Rudimentary check for a data directory */
-	check_data_directory(opt.subscriber_dir);
+	check_data_directory(options->subscriber_dir);
 
 	/*
 	 * Store database information for publisher and subscriber. It should be
 	 * called before atexit() because its return is used in the
 	 * cleanup_objects_atexit().
 	 */
-	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
-								sub_base_conninfo);
+	dbinfo = store_pub_sub_info(options->database_names, pub_base_conninfo,
+								sub_conninfo_str->data);
 
-	/* Register a function to clean up objects in case of failure */
-	atexit(cleanup_objects_atexit);
+	pfree(dbname_conninfo);
+	pfree(pub_base_conninfo);
+	destroyPQExpBuffer(sub_conninfo_str);
+}
 
-	/*
-	 * Check if the subscriber data directory has the same system identifier
-	 * than the publisher data directory.
-	 */
-	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
-	sub_sysid = get_standby_sysid(opt.subscriber_dir);
-	if (pub_sysid != sub_sysid)
-		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+/*
+ * Parse command-line options and store into CreateSubscriberOptions.
+ */
+static void
+parse_command_option(int argc, char **argv, struct CreateSubscriberOptions *options)
+{
+	static struct option long_options[] =
+	{
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"retain", no_argument, NULL, 'r'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{NULL, 0, NULL, 0}
+	};
 
-	/* Create the output directory to store any data generated by this tool */
-	server_start_log = setup_server_logfile(opt.subscriber_dir);
+	int		c;
+	int		option_index;
 
-	/* Subscriber PID file */
-	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:nP:rS:t:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&options->database_names, optarg))
+				{
+					simple_string_list_append(&options->database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'D':
+				options->subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(options->subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				if ((options->sub_port = atoi(optarg)) <= 0)
+					pg_fatal("invalid subscriber port number");
+				break;
+			case 'P':
+				options->pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'r':
+				options->retain = true;
+				break;
+			case 's':
+				options->socket_dir = pg_strdup(optarg);
+				break;
+			case 't':
+				options->recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				options->sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
 
 	/*
-	 * If the standby server is running, stop it. Some parameters (that can
-	 * only be set at server start) are informed by command-line options.
+	 * Any non-option arguments?
 	 */
-	if (stat(pidfile, &statbuf) == 0)
+	if (optind < argc)
 	{
-
-		pg_log_info("standby is up and running");
-		pg_log_info("stopping the server to start the transformation steps");
-		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
 	}
 
+	verify_input_arguments(options);
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+}
+
+/*
+ * Check whether nodes can be a logical replication cluster
+ */
+static void
+verification_phase(struct CreateSubscriberOptions *options)
+{
+	uint64 pub_sysid;
+	uint64 sub_sysid;
+
 	/*
-	 * Start a short-lived standby server with temporary parameters (provided
-	 * by command-line options). The goal is to avoid connections during the
-	 * transformation steps.
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
 	 */
-	pg_log_info("starting the standby with command-line options");
-	start_standby_server(&opt, pg_ctl_path, server_start_log, true);
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(options->subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
 
 	/* Check if the standby server is ready for logical replication */
 	check_subscriber(dbinfo);
@@ -1899,14 +1856,17 @@ main(int argc, char **argv)
 	 * called after it.
 	 */
 	check_publisher(dbinfo);
+}
 
-	/*
-	 * Create the required objects for each database on publisher. This step
-	 * is here mainly because if we stop the standby we cannot verify if the
-	 * primary slot is in use. We could use an extra connection for it but it
-	 * doesn't seem worth.
-	 */
-	setup_publisher(dbinfo);
+/*
+ * Ensure the target server is caught up to the primary
+ */
+static char *
+catchup_phase(struct CreateSubscriberOptions *options, char *server_start_log)
+{
+	PGconn	   *conn;
+	char	   *consistent_lsn;
+	PQExpBuffer recoveryconfcontents = NULL;
 
 	/*
 	 * Create a temporary logical replication slot to get a consistent LSN.
@@ -1959,7 +1919,7 @@ main(int argc, char **argv)
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  consistent_lsn);
-		WriteRecoveryConfig(conn, opt.subscriber_dir, recoveryconfcontents);
+		WriteRecoveryConfig(conn, options->subscriber_dir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
 
@@ -1970,20 +1930,18 @@ main(int argc, char **argv)
 	 * until accepting connections.
 	 */
 	pg_log_info("stopping and starting the subscriber");
-	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
-	start_standby_server(&opt, pg_ctl_path, server_start_log, true);
+	restart_server(options, server_start_log);
 
 	/* Waiting the subscriber to be promoted */
-	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+	wait_for_end_recovery(dbinfo[0].subconninfo, options);
 
-	/*
-	 * Create the subscription for each database on subscriber. It does not
-	 * enable it immediately because it needs to adjust the logical
-	 * replication start point to the LSN reported by consistent_lsn (see
-	 * set_replication_progress). It also cleans up publications created by
-	 * this tool and replication to the standby.
-	 */
-	setup_subscriber(dbinfo, consistent_lsn);
+	return consistent_lsn;
+}
+
+static void
+cleanup_phase(struct CreateSubscriberOptions *options, char *server_start_log)
+{
+	PGconn     *conn;
 
 	/*
 	 * If the primary_slot_name exists on primary, drop it.
@@ -2009,10 +1967,10 @@ main(int argc, char **argv)
 
 	/* Stop the subscriber */
 	pg_log_info("stopping the subscriber");
-	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	stop_standby_server(options->subscriber_dir);
 
 	/* Change system identifier from subscriber */
-	modify_subscriber_sysid(pg_resetwal_path, &opt);
+	modify_subscriber_sysid(options);
 
 	/*
 	 * In dry run mode, the server is restarted with the provided command-line
@@ -2021,14 +1979,102 @@ main(int argc, char **argv)
 	 * the command-line options.
 	 */
 	if (dry_run)
-		start_standby_server(&opt, pg_ctl_path, NULL, false);
+		start_standby_server(options, NULL, false);
 
 	/*
 	 * The log file is kept if retain option is specified or this tool does
 	 * not run successfully. Otherwise, log file is removed.
 	 */
-	if (!dry_run && !opt.retain)
+	if (!dry_run && !options->retain)
 		unlink(server_start_log);
+}
+
+int
+main(int argc, char **argv)
+{
+	struct	CreateSubscriberOptions opt = {0};
+	char   *server_start_log;
+	char   *consistent_lsn;
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	parse_command_option(argc, argv, &opt);
+
+	/* Create the output directory to store any data generated by this tool */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	restart_server(&opt, server_start_log);
+
+	verification_phase(&opt);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	setup_publisher(dbinfo);
+
+	consistent_lsn = catchup_phase(&opt, server_start_log);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the logical
+	 * replication start point to the LSN reported by consistent_lsn (see
+	 * set_replication_progress). It also cleans up publications created by
+	 * this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	cleanup_phase(&opt, server_start_log);
 
 	success = true;
 
-- 
2.43.0

#175vignesh C
vignesh21@gmail.com
In reply to: Euler Taveira (#170)
Re: speed up a logical replica setup

On Sat, 2 Mar 2024 at 02:19, Euler Taveira <euler@eulerto.com> wrote:

On Thu, Feb 22, 2024, at 12:45 PM, Hayato Kuroda (Fujitsu) wrote:

Based on idea from Euler, I roughly implemented. Thought?

0001-0013 were not changed from the previous version.

V24-0014: addressed your comment in the replied e-mail.
V24-0015: Add disconnect_database() again, per [3]
V24-0016: addressed your comment in [4].
V24-0017: addressed your comment in [5].
V24-0018: addressed your comment in [6].

Thanks for your review. I'm attaching v25 that hopefully addresses all pending
points.

Regarding your comments [1] on v21, I included changes for almost all items
except 2, 20, 23, 24, and 25. It still think item 2 is not required because
pg_ctl will provide a suitable message. I decided not to rearrange the block of
SQL commands (item 20) mainly because it would avoid these objects on node_f.
Do we really need command_checks_all? Depending on the output it uses
additional cycles than command_ok.

In summary:

v24-0002: documentation is updated. I didn't apply this patch as-is. Instead, I
checked what you wrote and fix some gaps in what I've been written.
v24-0003: as I said I don't think we need to add it, however, I won't fight
against it if people want to add this check.
v24-0004: I spent some time on it. This patch is not completed. I cleaned it up
and include the start_standby_server code. It starts the server using the
specified socket directory, port and username, hence, preventing external client
connections during the execution.
v24-0005: partially applied
v24-0006: applied with cosmetic change
v24-0007: applied with cosmetic change
v24-0008: applied
v24-0009: applied with cosmetic change
v24-0010: not applied. Base on #15, I refactored this code a bit. pg_fatal is
not used when there is a database connection open. Instead, pg_log_error()
followed by disconnect_database(). In cases that it should exit immediately,
disconnect_database() has a new parameter (exit_on_error) that controls if it
needs to call exit(1). I go ahead and did the same for connect_database().
v24-0011: partially applied. I included some of the suggestions (18, 19, and 21).
v24-0012: not applied. Under reflection, after working on v24-0004, the target
server will start with new parameters (that only accepts local connections),
hence, during the execution is not possible anymore to detect if the target
server is a primary for another server. I added a sentence for it in the
documentation (Warning section).
v24-0013: good catch. Applied.
v24-0014: partially applied. After some experiments I decided to use a small
number of attempts. The current code didn't reset the counter if the connection
is reestablished. I included the documentation suggestion. I didn't include the
IF EXISTS in the DROP PUBLICATION because it doesn't solve the issue. Instead,
I refactored the drop_publication() to not try again if the DROP PUBLICATION
failed.
v24-0015: not applied. I refactored the exit code to do the right thing: print
error message, disconnect database (if applicable) and exit.
v24-0016: not applied. But checked if the information was presented in the
documentation; it is.
v24-0017: good catch. Applied.
v24-0018: not applied.

I removed almost all boolean return and include the error logic inside the
function. It reads better. I also changed the connect|disconnect_database
functions to include the error logic inside it. There is a new parameter
on_error_exit for it. I removed the action parameter from pg_ctl_status() -- I
think someone suggested it -- and the error message was moved to outside the
function. I improved the cleanup routine. It provides useful information if it
cannot remove the object (publication or replication slot) from the primary.

Since I applied v24-0004, I realized that extra start / stop service are
required. It mean pg_createsubscriber doesn't start the transformation with the
current standby settings. Instead, it stops the standby if it is running and
start it with the provided command-line options (socket, port,
listen_addresses). It has a few drawbacks:
* See v34-0012. It cannot detect if the target server is a primary for another
server. It is documented.
* I also removed the check for standby is running. If the standby was stopped a
long time ago, it will take some time to reach the start point.
* Dry run mode has to start / stop the service to work correctly. Is it an
issue?

However, I decided to include --retain option, I'm thinking about to remove it.
If the logging is enabled, the information during the pg_createsubscriber will
be available. The client log can be redirected to a file for future inspection.

Comments?

Few comments:
1)   Can we use strdup here instead of atoi, as we do similarly in
case of pg_dump too, else we will do double conversion, convert using
atoi and again to string while forming the connection string:
+                       case 'p':
+                               if ((opt.sub_port = atoi(optarg)) <= 0)
+                                       pg_fatal("invalid subscriber
port number");
+                               break;
2) We can have some valid range for this, else we will end up in some
unexpected values when a higher number is specified:
+                       case 't':
+                               opt.recovery_timeout = atoi(optarg);
+                               break;
3) Now that we have addressed most of the items, can we handle this TODO:
+               /*
+                * TODO use primary_conninfo (if available) from subscriber and
+                * extract publisher connection string. Assume that there are
+                * identical entries for physical and logical
replication. If there is
+                * not, we would fail anyway.
+                */
+               pg_log_error("no publisher connection string specified");
+               pg_log_error_hint("Try \"%s --help\" for more
information.", progname);
+               exit(1);
4)  By default the log level as info here, I was not sure how to set
it to debug level to get these error messages:
+               pg_log_debug("publisher(%d): connection string: %s",
i, dbinfo[i].pubconninfo);
+               pg_log_debug("subscriber(%d): connection string: %s",
i, dbinfo[i].subconninfo);

5) Currently in non verbose mode there are no messages printed on
console, we could have a few of them printed irrespective of verbose
or not like the following:
a) creating publication
b) creating replication slot
c) waiting for the target server to reach the consistent state
d) If pg_createsubscriber fails after this point, you must recreate
the physical replica before continuing.
e) creating subscription

6) The message should be "waiting for the target server to reach the
consistent state":
+#define NUM_CONN_ATTEMPTS      5
+
+       pg_log_info("waiting the target server to reach the consistent state");
+
+       conn = connect_database(conninfo, true);

Regards,
Vignesh

#176Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#174)
3 attachment(s)
Re: speed up a logical replica setup

On Wed, Mar 6, 2024, at 7:02 AM, Hayato Kuroda (Fujitsu) wrote:

Thanks for updating the patch!

Thanks for the feedback. I'm attaching v26 that addresses most of your comments
and some issues pointed by Vignesh [1]/messages/by-id/CALDaNm215LodC48p7LmfAsuCq9m33CWtcag2+9DiyNfWGuL_KQ@mail.gmail.com.

* I also removed the check for standby is running. If the standby was stopped a
long time ago, it will take some time to reach the start point.
* Dry run mode has to start / stop the service to work correctly. Is it an
issue?

One concern (see below comment) is that -l option would not be passed even if
the standby has been logging before running pg_createsubscriber. Also, some settings
passed by pg_ctl start -o .... would not be restored.

That's a good point. We should state in the documentation that GUCs specified in
the command-line options are ignored during the execution.

However, I decided to include --retain option, I'm thinking about to remove it.
If the logging is enabled, the information during the pg_createsubscriber will
be available. The client log can be redirected to a file for future inspection.

Just to confirm - you meant to say like below, right?
* the client output would be redirected, and
* -r option would be removed.

Yes. The logging_collector is usually enabled or the syslog is collecting the
log entries. Under reflection, another log directory to store entries for a
short period of time doesn't seem a good idea. It divides the information and
it also costs development time. The questions that make me think about it were:
Should I remove the pg_createsubscriber_output.d directory if it runs
successfully? What if there is an old file there? Is it another directory to
exclude while taking a backup? I also don't like the long directory name.

Here are my initial comments for v25-0001. I read new doc and looks very good.
I may do reviewing more about v25-0001, but feel free to revise.

01. cleanup_objects_atexit
```
PGconn *conn;
int i;
```
The declaration *conn can be in the for-loop. Also, the declaration of the indicator can be in the bracket.

Changed.

02. cleanup_objects_atexit
```

/*
* If a connection could not be established, inform the user
* that some objects were left on primary and should be
* removed before trying again.
*/
if (dbinfo[i].made_publication)
{
pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
dbinfo[i].pubname, dbinfo[i].dbname);
pg_log_warning_hint("Consider dropping this publication before trying again.");
}
if (dbinfo[i].made_replslot)
{
pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
dbinfo[i].subname, dbinfo[i].dbname);
pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
}
```

Not sure which is better, but we may able to the list to the concrete file like pg_upgrade.
(I thought it had been already discussed, but could not find from the archive. Sorry if it was a duplicated comment)

Do you mean the replication slot file? I think the replication slot and the
server (primary) is sufficient for checking and fixing if required.

03. main
```
while ((c = getopt_long(argc, argv, "d:D:nP:rS:t:v",
long_options, &option_index)) != -1)
```

Missing update for __shortopts.

Fixed.

04. main
```
case 'D':
opt.subscriber_dir = pg_strdup(optarg);
canonicalize_path(opt.subscriber_dir);
break;
...
case 'P':
opt.pub_conninfo_str = pg_strdup(optarg);
break;
...
case 's':
opt.socket_dir = pg_strdup(optarg);
break;
...
case 'U':
opt.sub_username = pg_strdup(optarg);
break;
```

Should we consider the case these options would be specified twice?
I.e., should we call pg_free() before the substitution?

It isn't a concern for the other client tools. I think the reason is that it
doesn't continue to leak memory during the execution. I wouldn't bother with it.

05. main

Missing canonicalize_path() to the socket_dir.

Fixed.

06. main
```
/*
* If socket directory is not provided, use the current directory.
*/
```

One-line comment can be used. Period can be also removed at that time.

Fixed.

07. main
```
/*
*
* If subscriber username is not provided, check if the environment
* variable sets it. If not, obtain the operating system name of the user
* running it.
*/
```
Unnecessary blank.

Fixed.

08. main
```
char *errstr = NULL;
```

This declaration can be at else-part.

Fixed.

09. main.

Also, as the first place, do we have to get username if not specified?
I felt libpq can handle the case if we skip passing the info.

Are you suggesting that the username should be optional?

10. main
```
appendPQExpBuffer(sub_conninfo_str, "host=%s port=%u user=%s fallback_application_name=%s",
opt.socket_dir, opt.sub_port, opt.sub_username, progname);
sub_base_conninfo = get_base_conninfo(sub_conninfo_str->data, NULL);
```

Is it really needed to call get_base_conninfo? I think no need to define
sub_base_conninfo.

No. Good catch. I removed it.

11. main

```
/*
* In dry run mode, the server is restarted with the provided command-line
* options so validation can be applied in the target server. In order to
* preserve the initial state of the server (running), start it without
* the command-line options.
*/
if (dry_run)
start_standby_server(&opt, pg_ctl_path, NULL, false);
```

I think initial state of the server may be stopped. Now both conditions are allowed.
And I think it is not good not to specify the logfile.

Indeed, it is. I don't consider the following test as the first step because it
is conditional. The stop server is a precondition to start. The first step is
the start standby server so the initial state is stopped.

/*
* If the standby server is running, stop it. Some parameters (that can
* only be set at server start) are informed by command-line options.
*/
if (stat(pidfile, &statbuf) == 0)
{

pg_log_info("standby is up and running");
pg_log_info("stopping the server to start the transformation steps");
stop_standby_server(pg_ctl_path, opt.subscriber_dir);
}

/*
* Start a short-lived standby server with temporary parameters (provided
* by command-line options). The goal is to avoid connections during the
* transformation steps.
*/
pg_log_info("starting the standby with command-line options");
start_standby_server(&opt, pg_ctl_path, server_start_log, true);

12. others

As Peter E pointed out [1], the main function is still huge. It has more than 400 lines.
I think all functions should have less than 100 line to keep the readability.

The previous versions moved a lot of code into its own function to improve
readability. Since you mentioned it again, it did some refactor to move some
code outside the main function. I created 2 new functions: setup_recovery
(groups instructions in preparation for recovery) and
drop_primary_replication_slot (remove the primary replication slot if it
exists). At this point, the main steps are in their own functions making it
easier to understand the code IMO.

/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
...
pg_log_info("Done!");

The code snippet above has ~ 120 lines and the majority of the lines are
comments.

I considered separation idea like below. Note that this may require to change
orderings. How do you think?

* add parse_command_options() which accepts user options and verifies them
* add verification_phase() or something which checks system identifier and calls check_XXX
* add catchup_phase() or something which creates a temporary slot, writes recovery parameters,
and wait until the end of recovery
* add cleanup_phase() or something which removes primary_slot and modifies the
system identifier
* stop/start server can be combined into one wrapper.

IMO generic steps are more difficult to understand. I tend to avoid it. However,
as I said above, I moved some code into its own function. We could probably
consider grouping the check for required/optional arguments into its own
function. Other than that, it wouldn't reduce the main() size and increase the
readability.

Attached txt file is proofs the concept.

13. others

PQresultStatus(res) is called 17 times in this source code, it may be redundant.
I think we can introduce a function like executeQueryOrDie() and gather in one place.

That's a good goal.

14. others

I found that pg_createsubscriber does not refer functions declared in other files.
Is there a possibility to use them, e.g., streamutils.h?

Which functions? IIRC we discussed it in the beginning of this thread but I
didn't find low-hanging fruits to use in this code.

15. others

While reading the old discussions [2], Amit suggested to keep the comment and avoid
creating a temporary slot. You said "Got it" but temp slot still exists.
Is there any reason? Can you clarify your opinion?

I decided to refactor the code and does what Amit Kapila suggested: use the last
replication slot LSN as the replication start point. I keep it in a separate
patch (v26-0002) to make it easier to review. I'm planning to incorporate it if
nobody objects.

16. others

While reading [2] and [3], I was confused the decision. You and Amit discussed
the combination with pg_createsubscriber and slot sync and how should handle
slots on the physical standby. You seemed to agree to remove such a slot, and
Amit also suggested to raise an ERROR. However, you said in [8] that such
handlings is not mandatory so should raise an WARNING in dry_run. I was quite confused.
Am I missing something?

I didn't address this item in this patch. As you pointed out, pg_resetwal does
nothing if replication slots exist. That's not an excuse for doing nothing here.
I agree that we should check this case and provide a suitable error message. If
you have a complex replication scenario, users won't be happy with this
restriction. We can always improve the UI (dropping replication slots during the
execution if an option is provided, for example).

17. others

Per discussion around [4], we might have to consider an if the some options like
data_directory and config_file was initially specified for standby server. Another
easy approach is to allow users to specify options like -o in pg_upgrade [5],
which is similar to your idea. Thought?

I didn't address this item in this patch. I have a half baked patch for it. The
proposal is exactly to allow appending config_file option into -o.

pg_ctl start -o "-c config_file=/etc/postgresql/17/postgresql.conf" ...

18. others

How do you handle the reported failure [6]?

It is a PEBCAK. I don't see an easy way to detect the scenario 1. In the current
mode, we are susceptible to this human failure. The base backup support, won't
allow it. Regarding scenario 2, the referred error is the way to capture this
wrong command line. Do you expect a different message?

19. main
```
char *pub_base_conninfo = NULL;
char *sub_base_conninfo = NULL;
char *dbname_conninfo = NULL;
```

No need to initialize pub_base_conninfo and sub_base_conninfo.
These variables would not be free'd.

Changed.

20. others

IIUC, slot creations would not be finished if there are prepared transactions.
Should we detect it on the verification phase and raise an ERROR?

Maybe. If we decide to do it, we should also check all cases not just prepared
transactions. The other option is to add a sentence into the documentation.

21. others

As I said in [7], the catch up would not be finished if long recovery_min_apply_delay
is used. Should we overwrite during the catch up?

No. If the time-delayed logical replica [2]/messages/by-id/f026292b-c9ee-472e-beaa-d32c5c3a2ced@www.fastmail.com was available, I would say that we
could use the apply delay for the logical replica. The user can expect that the
replica will continue to have the configured apply delay but that's not the case
if it silently ignore it. I'm not sure if an error is appropriate in this case
because it requires an extra step. Another option is to print a message saying
there is an apply delay. In dry run mode, user can detect this case and make a
decision. Does it seem reasonable?

22. pg_createsubscriber.sgml
```
<para>
Check
Write recovery parameters into the target data...
```

Not sure, but "Check" seems not needed.

It was a typo. Fixed.

[1]: /messages/by-id/CALDaNm215LodC48p7LmfAsuCq9m33CWtcag2+9DiyNfWGuL_KQ@mail.gmail.com
[2]: /messages/by-id/f026292b-c9ee-472e-beaa-d32c5c3a2ced@www.fastmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v26-0001-pg_createsubscriber-creates-a-new-logical-replic.patch.gzapplication/gzip; name="=?UTF-8?Q?v26-0001-pg=5Fcreatesubscriber-creates-a-new-logical-replic.pa?= =?UTF-8?Q?tch.gz?="Download
�D�ev26-0001-pg_createsubscriber-creates-a-new-logical-replic.patch�=kW�8����.'�����0��	�{��������������m{,����������!��m�T�*�K%�Y2c'#qpp4���8N����px�����`��Gc�wI�nE��Gl08����`0��`N�u����f��	������"�E�f����'�����)�����"P�{l��wr�?`�`0���??e�|y��G��{�����tr�g@�b$�,����'��X<�(��>�X&�>t�g���y������:����
���`������<��\7b���iRD6��$�O������#�"������(�S�mS�&����^�s��K3��_�P�8��L� ��}!����'�5�{�%q����:8Nm�y�"���'$�:d�TD�	P�0V�;�$c��S&S���P,�9�qD�]���0#�w����9��9�\��$��S%��bY������q��I�� B�K�����I�"���!�
��O@�I��l�T���y���@��0��3L��@���+R�@Y?O�%I
�F���U�yu��slg	����]@���Z�tG�V!qJ
#6W-������0)dG�"�n�,�&W�I�T�����C[%C�z�:V�F�E"7�K��8�Q�����x"���	�4ca���������HE ����_��d���D:;�]w������!����A�(n�	��R
�0d�q�
������l}mR���aD�!9a07����������fI(�=v�
�0j�'�S�h�v����y����\�b�$�Y�:<-�4A�b�����_d0�H��E��x��aA��e�&����q&5#!{���~����l��_��/A@���]��?���Vc_�X����B�#������'q���������m�!���u�����Q�T4:����B>����(��?����y��}[�4j �;�g������l�xx�������v���@DB}��;Z�Y6��W�rk�5��F�E�t�p<f�2�x�p��4{��l������������`$����4������n.��k����l�1x�&���#pBp7`����5�bu+`�]�����������K�����(-�����~�6���;�e���9�G�*������3����_KO��t�q�{o-{+]�d�>t^[n�<���U1K�u���>h���w��b���D[�g�RD�����(�O�7<<������~ �1����u�x(��������_�F6�m�Y���D�`o��-��=a���x�@D;�����OS/��a��3�)���
\��m��=3�/Z0;����o�:���+0�<�#qqC���mp���&�3?$Q\�.�g���lJ?����e	����s���8O9,����yiZjO�-�gy�G���n����`0+q�����J(-��,0��<If@��e�R5o�?��_�r�����@�y��@��|l�&��$K@M��$�A��M+�i�����Q��:���.����?:-��/���Y�P1���#$ou~_���E���:���=�;�����m��SQ����$-�p;��V��Gu5�%�}�:)eq%l�v�w�����Q{�FA2]'k��fm(�fmx[��&��������X+��E����M���y�&E06�������a�����2�e��Ea���/���74��qQ����P�k��&n�3f`�,�
:��fN
���G0U���zK�Ci����r
�p���a<&�"���\���I�����a�L�mi��:�^P��W��^�g��w����)�v��yS����6������(��gVa�C��Q%:�w��j�O���i�`����S��Z)ZT!G�X��"�u��
��-�D g�������w5���D�L�eHY�m��L����^��6c�e6������>�3?R��6�U�B����vR��*TW�0Z*CY��N\e�%Y8	c�<�X�q�f8
�U�{�pk��o[���+�`f�V'3)|o�?Wf�YX�g<#W�W&B����u�l�Gj1&0A�kc����u�y�����Tv�� f�y��q-��I�)q0�N�-)\�;rD�����N�������~T��/�l(j�3E{/2`uo���T�M?�k`����v��������<�m����G��$��8zb~T`����X@aG�AD�T��{z�E0!%(@�Gzmc��w������Xk,�jx�3N�e���-���^�?���(��������h��������)��Y}��������g���QtY~T��@�Blp ���<�${�b�R�h��������(T �/�[�mz�^QAi�����*��e��p��<rw��&�
�)@�����E���O��U��/	�Y�F�����fa>e�`�
5W�c8��KN�d<��L����H�����q���@)��p2�����zU�Y����3#���[z��R��KF�H���?)h���#����(D�M9&
Z0!q�SA��e�����tC����oM���s���%�b`�8�)�B�I�Hy�?� ���z�t�S������Z�1��d
��+�:�����*$��^?)b�L�"T-k�%�a
����1dbI�i>��b)�^X
%T�$u��v_�M[�m+8�����)eb+!P5��T���u�|&C<���:XS�q�:Y�|"�Z���lf���k$��
d�W1P	�I-yM���T���r:�U�#��^��k�U��R�\":6r�8LWC-�����V[�JG7R
��kp3�<��X� a6U	?HBXa^h:�I\`5�����9�P�e�`��E7m�G����+^Y>���	b!�5+i%_����c�stL\��;s��?����H��{*pR�'Q�<Z5�����I�	mR�S=&C)1���(f�w��3y�F,7�*��p)�7lY�F�f���7%[a�
�����p�}n�v�|B�$�*������"��4������0_��c�5��n-{lL��So\���t��I�]�p�j)��<F����l\s��8ir��W�v�<�Q-�(�iQ�#*����o��x��F	��/��U�:���S|��s��������]���"G�LJq��y��� �e���@�	)�Qt��bk)����r��|1S�9�
|�HVn�����]
��F�<'���G�	���}]��_���}��Sm�|��l5���n��M�[��^n rr����<
g7��z9�����V��V%u�����@�Vb����\`@O'U����r��L����D�YE��}Y'���	:��r�D�3U���S�|���n��?�RGt,��(���mG���2.��@<�[�[%�<L�$�R��h��k�?��b��c�0��G<#��y,x���b�Z���T�+U���p0�g/�����g�����1��2��9�jl3@��_�mU�v��}j5���Td*���w����
��_g5�>�J|��(���K:u�G2
�*�QXC�s�X1U6_�(+a���"/�����,a�Y2�t��LH�t���r���+��Y��M���xM��!�!:	��c���g1!�-����9�����g�}X���8����,T�0j�='�P�?����"J�@��N�G�04C�M����W��t��X��j� ��2�t��}�g����!m����g�������&g��S-k��5:����)����7�����IO�~�h;
G��n�,<��PO	�j)&�Ef�?Q7�U�R�������!<��x%���c���K3�5{.:6��i[L��2WUj��RB
+*4�� �p����[�g�x�opWm���}��P�)bU����h_���-���L"���������Zm F���/?���
8���n?j�dX�V`�N����r����$��s����yk�������'W���<���p��"���3�	-d��cQ�.��U�e����h��WI(j��������*��@����B��hw���+�=����4�;*�n�=��0wk�R���$�E^`�8���������U+S�5,���r6���%��������a[�
2Ooo���1�]-Y��Z
������������V������y!T�������2�&��E2���v��GZ��F�c���6)T�������O������4�Z�����C�Y\u)���8gd����6VmL{W��S0���3���$:km'A�!�C2����JtJ�WLN�^F��D|�����)k'�	
�/��:��HA�"�6�j��G�	O
~$���e���OM#��� P� xd�� ������@~a���f���c}��*��������%<��:�Q�Y T�qK4	���3h����B����m�r��_��-�J6��q�	��cs���a����c�����{$��;�F9G����U�%<Hw9�m	�����+7Zu��h�Od$b�LAY.�%G�������+I�U4�t/`�*���}�Ii���^�]������nj��^���e=ke��>C��e����C)+ZI/��q������A
����<��;J5./"'W~�T��x�����s?�����'%�"�I��(@[�������F��Vu9j,�	���@�Q��{�
��d��R�J2S�z6��l���l�E���U��[M�~��bF�NM�	o������2C�}jl������1V[2jtcV��	�=�.fjbW��������h*���r��[
l�Z�T���(h�%U|�@�����Lc��V�#����s��]����l2B�EkZkV�r���h�.s��R��A>����N��+1i���)��T-Zz�,��������>�����q%Jm^����H��=9]�X+�G1��v��X*j}G��\����j�QB����[�r��.Msu-6Q	2Sb
!�>��"�*���q���AR�ibN�n����<t�8\Dz�fXW�*_�X�0?�|g�����B(���5u��������`m;M��"�`��^IR��V�����,�/^���;���0N���	d��2����~:��F}r���lc
��9�����M3�m1.���}��3���B�K�jd�!2����O�8�c�l���3��E����|����uS�\	�E.�Kt�\���5����y�oL��d|���a�7p_��_��Bi�L�mA�~���_�������S:��?�(�����;��r�����Iy��wD�?5��*����A�T��&�wU".�xo���&���`�x���^o�����\
J�ti3$���O�RQ�M;;m�Q�V�F	 <�~0W��Uhc/���i`�l4����	?�#?X�.�"��FH���.�o�������������s�K��a4R�� �}�K��y�K���SV�U(�jt�i��Pl��N�){�����O���?���=���|�P�f��XyI����7o���^���A��z{e_�����o7��S/a�^����6`��z����7?��~���m��%���������N�J���W@��	<o����"}p���p�.M���i���A���:'�tl��a�fu���������?�������2�x��W[W��wW7�_m�����F�M�b���,��xs.�%�6[��z<���f��Aw��<�����O�X[�Xd3�����������W�.K������c�`N�N;���&�Zh��C������ ~��i�U�VDd��H��[=�P��o��,�W�yW2��������F���8����A����p%�����6��p��.����Z�9��	:�1��f��f�����C�9��E9k�I�*�*8��Br���^XCh(v�1��^����l���Qnb�E�Z���ND��i�-���a�}�wa�y��a���h��~�|w��m�
�MM~�y{
���f�+C��+*�����|��1N�
l�#m�-$	���s�
B��l�������<��~zT����DI|�%�4����4,}S�gU�(^�$��'(������\
�C`��?����'��>�����v�&gg�z�=���j�S��{����}{W�����)&��HF<����;�fml�bg%���4��F2�$�������{z��8;�^�i��U�U�U�zG�����q��*���Y�	��]�s��w�����B�.���%4qD��|�~_`���Q	��o�����e�����#�O���������(L���������|�N��%�g������������k�"�'
���m"�}�t������S����U�����x��q�,�����������8��d|���]�F=�__��M�����xRR���!q��lt����7x�c-F=��O��8�O���������in:��x�]F)���G@@��q��e���� [�?x�{~t�����'��gK������:H���N��R����bx�)��ot��M��r�',��3NBg�$�/���[��=rN�@�|�@��O��D
�[���pr�����O��'K�����$���s����o���|��z����5����������dP������W)ZY4�P���=��@d��Ajh#|�f��[-��M�����KN��^6��^�:���E����8�f���������`��
AJ��r]���Q��ZO��^���'H<B G,NO��!���q�/�����N��p?�7N��sk����������e������(�_���R�A�Y���X������������55��j�O������{'�
�w��n*�+)���7��w�'��LY��O�'@��	g9����_����A�}�b�a{|U������f�?������b)V*xm	�w�wx�I%�����n0�M�W�d`(�k)
Z��l��P�^R�����*O{�V�<y���`�R��SF��$�4�&.���rm�Nf�1��#�M���m��F�O���{�]�!��c������&�(S���s3yg�����*g�+�212�[�8I[X����E�0���Bw}�{v�s���}�-kl�����bP�Z��.������[�+�<�9v���s���=����J�DF���f���������%��1�%�G�e.�R9���I�&~��� ;0�F�b���w}�����H���B�������Tt���`����Y�B����
����2��#L���u1��"��[)#�U�'��-�V���9 f�Ld�y��2_�Q�E�w��y�`/=98M���mv�o�������N�=Z�&w["�E�5|���fi�	B���������d�������X��>9$�����n�D�t;le���5�5��(���:���S0��c6~N�[g�v[�T������M��������==;|��pjmq�7��m]��
$���Ns���b����C���M���YM?���Q�,�-�����e��yIi�\������<$%��P��`������(zX�Q�>E�A,��`��0f[[m���d���>��k���)��.�@���)��%��%��]��Un���{�GE�7���
������\G G�
����,�?�4�.�t�bnl<��Fn�>���������.��i�
�a=��=T����b�����J�����r��Q���1h��k������'��p8}��$�����}|��g�Nt�)�����!��_���B������n#����O�o��NDM,�&���	ji��SC�� r
*G�
���^���$u&��rC����'���tk���i��)�
6�R�s����u=���3�#���.q&���0��TP���*h���wX��-RF�J$kf�0����Ky��
��	����\`�d�%f��t����R�Z[)~�a����Gn�5O�K<��m�;��6*��{h�u��;�|NF&P���I���y^J�{EJn�/Z��n�B�#�h�0��Y>8h0�s.<n�>��3����ony�=n�4"$�e=��4�W��_ym�:�uPk��$��h�i��s��1>|�$��/�'x��ncc���j�������a@��s�dO�?vW�*�Zk/����a,��=��?<
���ETrB�#�������a�f�#B�h&���Z�047����>���,�.�5^q[Q7_+��Lx�T����*�P�;~��u&�6�������?�E��m��E	�����o��wvUJ�u�hWc�S�u5�-��y�����~�>)��Gl�g����E���hs?Is�3�9zH�e5�Xj�����	���1IU|nz��N��_r1yO{��J�LD����?v_���_�D(��C������p#�o��G��ge������,�J����`\�
t`P�h�Qc���k�8Um����w-�M�1���.�.aX79���m��Jj	a����|<S��s��c�K����WD�Z2��f9L�t:�U�1i
|E!Wb�����u#c�	��1u'����>�v���+I4�w���c�~���c��������C��.z���x�r���(O�z����}'�k�^��c�����'X���M:8��t��������cY87�����k�l4�.����7�
O�%u��$�����=�g�����H�r���{�R�9�`�-���1(1�f����=��=��]i���F���s�����&u����
�NC�zX/��Lj���Np+y�@���9�Y������N)��P�������Q>������i��4�_���w��n���%��-�Ew{@u��f����@J� ���&E�)60��Td:���*@��T���K]�o���x���N�Za����l���r�����@��n�d��M���P�T�
ST��1���(��P�HPF�J^��"�O�"X��+�=bF3E�,~'��0���]'x1�W�
�^!7�u��
``8���'�������%�������w��v���<�=�;=�Z7���G����\�nD��6+����q�=�Ms��8z����'�����[�K�J��G������n��#+z������FC�@�{��osC���W��}E����p^fY�]����p����2���*-X�Z�,�y���Z�O��p <�|��?�
�FyDj��S^D9M��(���W<N��p��� �A����������Q���?e��5��$���X
mzj����	��*=MZ������l��1�4e���#(j\)��?E�����yCnX,�]
6A?@�Z�`�������
T�Nq�@��1����xC��\�X��1h2��p[��
� G�:x}|��,d� �Q�dQcg�G
 �N}i��W���/f���1G� fCt11����U��v�	�`�v����eB
!�:F93�.&R���X�l�7��>_��X:R8>n�8��X�(p�s��%�L�,��V8b7q�N�LG��f�MI��mB�A����_�h*~Ti��vC���'�E���Vs�z*[��ZcQ�tLW�"(k����������w�T����@L}�%��6��2��?p���$��)E�y��v�Xe�N|�������J�27��&}�h%dD�����} O���,�����O�E�k�� �4���$q�-������Z�e{���`"��,�>M*����2krD���64���,��|D���bDE �����3�����/�������0��e/�w@,x���B�(\.dq��x\i���It�;fiR�"��f��IJ[���������������T3/��n{�9Pv�BJ���`�����Il&�]_g�,e�6�D&>��;���P����F�g@�\:��v/����q�O�7 ����2)=�G�;�n�����8�����Oz�;���R�����3��l���:���[Y���Q��$ve��"#�/�w�����������k�t��)*Y)��n!x[��������Vzv~rMY�b��H+\
��@�	��EK����Z�NS��{���hx<��r��t�~��Z��?����u�I���@��
���	]�\�Uns�VNu���$�S�h�1s������ t�pL=���_@F,q�KK�\��{/5���A��=j
O�D#LX�!MY-�j�up�G�KU/N�_���S7����(��F��y�b���/����6f�}y�`�#b���]�>���d������E�O��u��d{�!�"�4��8�|�f��dB+�L��?hZ�jD��LW�+���bE,u���6Y�4�G#�aX���y����4<0�D "�+��P��[L
~�u�3��N�nT��!�Vf�k}�9E~�N�a�����5���>��_�3�:�N�����P�����u�����-�#��.
�%R����N>M�c��!��%��(�j�����c�������j$��@��E������L��]:�����C���Ny5�]yqjL�2v�A�q�������;e��� �uK�m�7o��B������|�����e�O7j~���s�	�nQ=m1�b��K�<���[�N@(r�>34��y���=|{t���p��$�������?lZ�m)�������i�l�?�E�i$?��|�S�����&�������@j��A�����N��Fr"h�'��:{�:���s��$:�Xg�b���L�L��u����������lJJ�h�,�Y�������;�.w���������e�������Cw���@m�s�����[������U��#��L��kd0!'�|�0_��s_����8�&����0����M��EaL�d��D^,s������[%f����DN��QaIzt�������b�w���w#�!sc��I�T���f_x�����]h�yAB"����Y����*����=����i��c)�0�
�.��f��3��8��|��t8����2��s�B��$YK>������l&��Pz�dbA�D|�.
m�%aS�:W�u������1hm�m�n��8�
���d
���I;Z�������Mf�;����OW&�Q�_��_
�N�u�[-��z��	�����.�iCo
���S'J����(qn�a���P�/���������t���X�"�R"����4BQ���WH�b�u�Q�|����6E��;��
��b�TS�������h�,`��m�&E�R�u)��/l�(�N�N��{y��HP�����"�e��� �5#�1�z�4_�;�#�����a���=R�bb>�c�aU��P�o������Min�wdz�8-!�+�h�Q����Xi�A(I��T
Q����@+�]��d�}.�,�b�#�_[��G_WBu���"�G[�q���).R_�:k���|O�P9�u9Z���U��[�(���w�-�xR�����}��f(���V��&����S����3�4��D�#��~���OA�F>�go��]�������h��
��>��p�������T�`3H�;z��6�����N�������<�vL&i]gx�|hW�+F�L�.E���q6���8��w��i����`'/�Q���b)���*���",H�)S�/2�
���)���^����qJS��n2��u�j4��-	G�(sxhPkW�zb�h�����0�wF���VYS�1j����,5�����?��tEoc���S= B�e�[`(��=�B���(&o��������1Z�Z����W����\���k��m��^�MV�����`���@z
�3��1>s�n�D�
�b��<���e�OD�R�&2��9&zs��*b+O��k�}�������`����GX�G6����p�Jg�����MZ�&M�����{�
i&�S����M��4 ���**��l��Wj@�>���y/��6?���)O�����0���\�s=��Z{��?{���x����TZ�Z������nz+FX�����^���Q��F�Vn���#�o�_����.	�ER��j�#��Q��2nLJe�yUp(��h���o.�cE��k�A��CDe3_qU���a7  R��1s�)��4\_�+M�y�U�5�}�����r`4�"Y���n�1�Y�Hj�M�$Z�d��~���VW�U��,o���+��x34�g����LZ.��O���>���o��7�_<9���"2V��
��|�@^���iT>����y�j\s9<:����q��3��������-:?�
�D�wT�{2��P�����+v%r����8�^���)	��
F��N�B$ ����4*�q{�����@��������7wv�Y��I���S��zp��5\h��1fw6���fi@/���f,���._l�"�� T�������]�,t.���=S�KEk��<��)�0��TT:�"�kM�uAe��7�;��hq?�8]<��N{��q��T��~&�Wm��Y����0k�hI���2����j��`���{SFL}hn���iZS�����
GV�}��8b�4��I3Y}u�����2a��:�kk9�-��?yZ��2���mU�tA��q�iL2�#���=�;�8Q���'/SBC��N	����li,��e0S����}[Y�e�*Vjx�f5+����6��������{��w�AO�w�^J3J��`9F�d&D� �����:�X��}k���Z�Y#3�f�Nk�keKt�\�i�;w���
jwc�E�w$�U��"��?��_U�Z�3y�+]��x����%�(0����Vi����r���&Ow�^�i�d�D>|����uu���*�r��=|"\:���q����������g�Z�����^O��1]����=��v��;��)��;9>=>�&��N�-D�]{���S�[���4l��.����p�����e��8�����
�M�US><���xT4�jW���n���5�g�/�F�Ly����������E�j�1��VM��<���e�!.�
r�4��y~��|�
9g�|�T��i����%�b��7��{@@<rE�>��\���*������}���(6����0%`�rR��H}q����2��l�SZ�����*w�����oeu?����*�i�mPCg����� �n�����!Q����>���*�U�N|��wm:tu�����=���I+���M� C�r6�JK mU5�rxi@K��f�����j�TM���/�q���U��"���zcHK2���*�Q����N�����I��|�S�*9�K8��L^��0~��:��WmAX���]}I�����RvP�}��Amz��P��q��y�g�\�/������3�l`��1���tI�_��x��Y�l�>d#D�s/[���P�<��
QYd����C��B�_��~~���������T^�^�����'O�_��\�n�n$'7%T�dLJ�������Pr&Y�6CHk	v��T��Y�X�c�P��F��e�\�1 �L~�H0o��3m��������6���]L|���> h���x����{9e6^!gP[�/,g���>���3	�����2G|�� y��t�t'�LR���?���L0��|�u�� *I8���	/��N��=�"�mq��!�\A���`t����P���~����T�ZE`�b|mS�K��!O����aN?B���.7h�V�{|=���O��g�%L(���o=� ^��v"�p�Q������8T�,�J������Y�{��U���cx�d<j��"1�>��u�YlzU�*�A����%:~��j=b������])Y���yPb}d2��?�vG��bm�6%8�K���>��J�.���R~a�[��,*�.����"����� Io,>r)��0�����,��Q�G��q��/(�V���K E�T:1��Z���+[A����^7]epA������p�YjFF��,~������aM.���1L���oa�o8_��V�����b S���c~Y�F��3!�J��fk0�p�>�o��A0���/lJ�������"����>��.CO��	�y��K�%J
�c!/�-OC��XD
��w�ha����_v�&C��:��sfJ�)�*:Y
�*��?�����*���*���+u�M�&�P��j���0cP�T�o:��>����M���� Y����+�H!#���P��=�2T�<�~Lj�����I���E��a+Q��?8\yvRr��A�r�����~�Xff��7weX�����R�����x\�d�B#�^?���t,�X������R�D����P:�81PZ_W��F����.��R|�����*��1�����2FO�f7������<U����lO%X�Q#�'�������
�QN��f�8����M��o�|u���m,��n.zc)>�o1n���A�LC
WI��~��:H���N��R,)i��%�����y�i��~%�{��Z�z��o�T�a���.����/�;%u)D���c�;����Z?x
E�B���8\vf*������VP5	^�A���wK�D�%�
[�[{����j8b#��Kr-j�8�	�N�W�y��jq����{Th�=%C������b��X�cuI��EQ�?�wj�P>�FF3B��bO�~
H��������
�������,�Q�V�4��&z�:N����v��EL$��uv�Q������ $
�^�Gu�\E#�c,���+�+������Zi�P	]�^z0���d%�%k(��HT;A��z��������F��2^7#Fp�d3�����A�������xJW�
�}�?��6��S��{�N��O0��Dt#%������Z���rS����	��q�<BE�c���_��7�k*�>F�A{��X*����p���>|��<����� ��@�-�[i������%��rj["]�_����[�3�]�Yy��N�z�x��������O+o��������F���t�B2�d+��=�:�{�km0������"���{�����(
��qBj:;G�
��f��@���n���)4�.�g�����A�w�-����
4�9�C��8����=�J��DWo��2�z	s4�n� +3��X�f�lM,n|������>���^��5A
��\���n
3j6-q���K��*k dw���9b�a!�?��.����%��7!_,����A��C�o��nw�aN����(R��Y�?�P��-�ux��"]�;`���|������)r���J�
������Fr2"�4b	c�m]���E��{�@n|�{�1��Vr���1|�=�{+E[
ZV-Y����tu?���SyKiO��skK�2�h������G�WwTz[�7S�������j�X�������$�����O���$�w]?'�gb@��h��e�
��'��S��,��o)X�.>d�-i����\EJ�����P���KH��*Y�; di��0x1������O�v��)����'���!XD��u��2�5Rr�_]�B>b���s�K�]0]e���i��C�^%����VnPe�����5�$ �$rT@/���[3eS�n����e���2[mR�U,����t�"�o�P��� ����s�&`�����}1@��Q���
P��-���s7��XyO���$>��)��D�5cG��"�iLa�=MN�[g�v[�������#��{zv���{qdj�<�0�G����p�\�! *R�=FW���n��&���kz��gg�N�ZK);+q�����\&L�6"��Q�t��?������-C}s)�o��Q���$���+(X���
Rd|�(��w� G�mG�mW4}��U6<�'z�������������l���j�����e!l��o�5�5�-j�i��R��n��e7Yq����*jv�b��D�����,"�w��!�[p����%������.�/���"�9O������l�B���
���G�cK
�N�kk����`��]��rs�T%�,p�z���L���i���>���>(~��c�!���I'{�m��(���:sJ���|�-,��w�(n�E)���v,F�=M�Oyw�����E7B]��Y��P���e���a�-t4)��y����Yz���O�\����^z�����Yb�����{X�),����h����,��;4�x�����_�����F�,1)���"����L����P��'��T4�lI�@s�7�`���e�_t:���)�K���>RXD�2R�w�_�rSl0\P����5�U,����m�����,�����;U#`�=�W�����!p�i�ld��H����l�g�+T1'~e���i��U�L}2����l����.F0|H��W�h��3i�����>�H�B��0<�"��C�\�9����!��"����e����+��S��^�ie!S%���'s
�)nj��]��h�pR]T�H!�J�;��W���:>!���Q
-tr���poan�;������Qr��Xn:g���[�����W���
%�k�����#Va^h�;�
yq~9�yB�|�D��h�M)�"F������*�4�y��x���~���'�����P.�p��U���� z��k�w,���x���������3�����l�C��:�m!������:��z1m���@n`6Br����XC&i.���>9�b�dcH�#�0K� ���>U3���B�L��{/��|�[0��K!���"�Fk:��6�T�wt�����5����hc;{��kSm$G��YQ��s��k��a�nX���Z��M��x�������x�K�4���O����}<�9��hQB52�����4��o������*�0�����������5�y���g��z�V���T~���M�orU��
F^t��A;-2�8+��'�+��>��4#��O��]Vs�Vq��������AQ!5T"���n�C�0T1�5�%.LI+��$� �}d��i9�r>^
��;�k�p��b���;��oe�2�B�BC|8���� ��=���hmTo_��H�,��� E(����J��N���.�2�q@����$m�>b�gY��
�(���X��-_�G�>E>�i��EEppIL�4G.���#>C`%$����H�������6qQ�H��0d�m�@/�����\�T^��������������Dx�wOP:)�(�5���O�J��ja?������g%/^1���gX��x!��a<�F��%A����+�[U�`�9�ce��=���c�_)�$jc��W�I��w�t��v,���k�xD�|��n�E�����!^{����}�2�HV&F����������7U�
����3������vIa^�������;Zy��1������d�
�p���GfV6�V�/��.>���<j��<�?���Hz���-4BD�����)5��*sLT����5r��@���k`�,�N�*����Y��Cb�dR�����ZIm�����d����Y���n�]���"v(c��5��Y^� p�sl��w��C�����P�P�{`:����]��,�o53Rra�Uj�?��^��~n������������������ /?7��9{h�r��>tC1xh��0����S%��4rc���u=��B����3��ql�9K����<�oBg9m_l>�su_��O����?�x�G���e�����3����N A`����i��|b��$
r��}�G�PcJ�
�7�k�k0��F?����x��\���f�K^��oY�:|B��f��#�I�Bq[��n>,���~����br���dm�a��1�.������w��:
�_��� �-u*VGs��e�f��5�$�s���Y�pU�W�-l`��23��u�c��2=;x{��j��u=�0Rk��\�L�\(ujs�����r� �����V�&����MyP[�4�r[?I[bDE�[7�V�v8����cP[�<j$5\��N[p
W��������D�
���c�x�DJ4���&��v�-��;���=?:��:�����~���m[I��c���]�}���# k��,Z��j�������A���lD����)��u.'��8���wH�F�I����u�\���M��Yol�uoRz����2��x�I�	��X�9l!sT~&���k
Gz]e���6�fZ�N;�f��p<dm����?����\v��^�t���*�(O�z��~��E��d�������������������nKZi&��o�J������a��Bx��
�cn8��� '�L{kjd&3������NR~R�XK�3L�?(S(E���8A��/Ea�zj#O�m��^[�>�_*l�'�o&�f�M�7D�����wF'a��z$R���.MZ��/?>4�����@1��%?I���[�kYIBz�s2b����^��,�Z�RC#���.��\x�����;��F���+	9��Um�D�E�����4��������������r���U	
�w�x�]B��j�yc�6"��"��_���QhT*���'��Stwpr~�.�������5�W`��@�W�@G��kD�Sx��jmkI��7*�f�-N�#���xW)��:�R�Q��`7<=���	P��tm��
��V5�>��sbX��xK
���v����0X��Q�:w��[����B�/���B�hX@��I7�����d������K���/q���Y�PZ��R�{�|�t��|l�z�X�+^��9��������4M�-J�d�D�0�*%�	J8�bt,M�J�{b4���z���y�����4""0|�7��a�/�ZO]��0�J:@��&r�)X!'�@~3����~��G����j�F�|���%���=_
3���\x�2�fA���R���|��oc�������J��uh���h����>	�C(�O�^�GQ���w�D��XGx.�o'=�����<��1��/�����w����,JKD�7YrC�J�� �o���1� ������c�t-�������$��!k��(e(�U�;�{��������	�/�<7^F��MOW
�<��LH��`c�/�����+.��S�'f<%b��V��oF8�;���76������e*��%�{d�q$P������A%��3�����s����MQ����!���-D���2|��`�%Pb����P�^���Q�oy��X���*��"��t�j�vT�&��PZ�PP|���z����Rg�}Q���X0�r��E������D{��nQ������8���s��f5t�����C���Ys�(_7��0T����/�V�:�[�i�K�5�4�L(`UU�:�����C���\���
��]�
If���9X;�/���S��;��*��4���������!�����v%Y��<{��i\fJ���8>�������t C�����'X}/e������a��5�*Q7A}�����:��\����8��=��{6A���p���_��ib[	��+�o0����]��QGQ.V=�Q����1u*u��0Q�%#��%n&�p!��T�nK�5Rrr���E~P��|�`	�p�_�qr�H����L}�W���
�&i2p
�7�����\g���T5"�.�oS�i!]��W�������e�������%������a�y��iV�����L���� &
�������@���(GcX����a�b���B��o�n�bM�0_.������WL,6o%��[�����1����-a�IAe�@ ���<m�&�S��AU{RB_�^�\2_?�]%���XQ��N�.g�����3PU�a��Q����ZR|#�dK�KlN����-#�c�:�.����D�M2��
A�8E\]�z���M�/d�b���.���8�d�%r�;��4�r�)�t��n$o2N��sVw��r�6��mL�"���X�J�I�l��D/#�+Y����B�2#���*9������1d��L����4��{U�<�^�o��@��%�),y�,��P�;�������Ed4����	-�;J���Fo��Z)�x5	<�8������� p\���ExEY/P��m/�$qb(������.~3��nT�,��+��P)F��tX g}B�#�
������ft=Z�<�u..��Qr-#B>-������.w�T��3��D	i�;���2}V�`�$kXeQV�.��	�P%����Pr\����.^������l�r��9�\dG���2����9%�
h�����d�XrgY�G)��|�%^5gGF�y��\��<��h��[N"�2���Z���Fq�Wq���[��
c<���V�z�q�$�Hq
nI��2ZQ`Q������7=�d��pju������]8�L�e���U�����>x,���o�s����$�������b�����wQ�ws2�on=�J#aI�~rq����
/��nom}���2y$[����_�;��W�/�/���'������uymm�n��o��Vs+Y�n�l?J�����o�=0~Gt�Q�4����GM,���_I�c�[>D�Q���!�=��g�{Rd����"�:�@����oE�S�����UP3W�[7���������&���T�B��O^�MN�O9����i���Y�U��������pl
r��1�S_Z^��M�C�[1�;>�������z�QN��� �.���H	���K��w��Z��F���7
/��Dn&���
������u�S�#���N%�w��p�A�/��h|��5�����Ux����,��-U���D:�D*��{�����
��?P_��Uyi��bD]���Ud	1��IFF���*_���>���Z?��^����I�*�X�o�NP.���T�a���-q7�l�V@��w��]��F�Z5pEE�Q�f\/��%96��Y�����,g�Jw��n��_����j/e�/q]R�.�v3�s.�}8e�/��L��;�>�y3X�����D�'��`5=�/���_}d��!�&��?����:<�&l?�~�7|B�
'n#��[���/��I�
��4n�N�v��upz��>;��=:?h�W9f���U���}��I��l_�v�G�Nk�C��	8�y�\��v#c9��0�8�����.�[�������v�@1/ij�&��T���`	mGM�V����F�2/5.=��W?C���/x���[���w^�����*T��Uu
ja,0��J2.yq�g�aps6�0�;�'�&�{��&������>�'�����\3��N1�����\������7��[jS��i�3/�w�w*i�p4�3��;S��375�j�O��g���c����(�b������Y�L����-���|���B�,;�v�#:ty�"��Me��o)~ O�9�e�3���s�N�6��XB�=,&g�].����\���'G�6�B��D���z#f�-G:`I�;����R���n�/��Q3�����Tdoid�-\�5`��zE�E��F���
4x�Vq����1F��i��d`KX��0X������=�,�]�YB�Ky����_�{����`��nR�y���6c��G���0�huu���q�dl8���	c��F�Q�uw��&�E�
L��T����\PU*�?lXd��^("���2f>�/j��-�!SGo��Jx8�U�0rJ"0����N��Uo=�Jf[���m�meBa0E���<�s���I�!������I������N�W�>�[:�u?�PU����0�^i��f�.db`��x��HU�'Y��1�XH�o�,��P��^�s�exfE��1�n��B�+zLiq;�8��?L���|��������������!+����sU8)��d=�ft��5K��i�(��|���:�vf�`':����, >i4����c��_���Q<&x#G��([j�52�LM���gA���x��S).��2�KQ���@v.�<�=�
VrC2�d:��	�T�,�1���_\���n>�R�������z���������������gq
v26-0002-Use-latest-replication-slot-position-as-replicat.patch.gzapplication/gzip; name="=?UTF-8?Q?v26-0002-Use-latest-replication-slot-position-as-replicat.patc?= =?UTF-8?Q?h.gz?="Download
v26-0003-port-replace-int-with-string.patch.gzapplication/gzip; name="=?UTF-8?Q?v26-0003-port-replace-int-with-string.patch.gz?="Download
#177Euler Taveira
euler@eulerto.com
In reply to: vignesh C (#175)
Re: speed up a logical replica setup

On Wed, Mar 6, 2024, at 8:24 AM, vignesh C wrote:

Few comments:

Thanks for your review. Some changes are included in v26.

1)   Can we use strdup here instead of atoi, as we do similarly in
case of pg_dump too, else we will do double conversion, convert using
atoi and again to string while forming the connection string:
+                       case 'p':
+                               if ((opt.sub_port = atoi(optarg)) <= 0)
+                                       pg_fatal("invalid subscriber
port number");
+                               break;

I don't have a strong preference but decided to provide a patch for it. See
v26-0003.

2) We can have some valid range for this, else we will end up in some
unexpected values when a higher number is specified:
+                       case 't':
+                               opt.recovery_timeout = atoi(optarg);
+                               break;

I wouldn't like to add an arbitrary value. Suggestions?

3) Now that we have addressed most of the items, can we handle this TODO:
+               /*
+                * TODO use primary_conninfo (if available) from subscriber and
+                * extract publisher connection string. Assume that there are
+                * identical entries for physical and logical
replication. If there is
+                * not, we would fail anyway.
+                */
+               pg_log_error("no publisher connection string specified");
+               pg_log_error_hint("Try \"%s --help\" for more
information.", progname);
+               exit(1);

It is not in my top priority at the moment.

4)  By default the log level as info here, I was not sure how to set
it to debug level to get these error messages:
+               pg_log_debug("publisher(%d): connection string: %s",
i, dbinfo[i].pubconninfo);
+               pg_log_debug("subscriber(%d): connection string: %s",
i, dbinfo[i].subconninfo);

<term><option>-v</option></term>
<term><option>--verbose</option></term>
<listitem>
<para>
Enables verbose mode. This will cause
<application>pg_createsubscriber</application> to output progress messages
and detailed information about each step to standard error.
Repeating the option causes additional debug-level messages to appear on
standard error.
</para>

5) Currently in non verbose mode there are no messages printed on
console, we could have a few of them printed irrespective of verbose
or not like the following:
a) creating publication
b) creating replication slot
c) waiting for the target server to reach the consistent state
d) If pg_createsubscriber fails after this point, you must recreate
the physical replica before continuing.
e) creating subscription

That's the idea. Quiet mode by default.

6) The message should be "waiting for the target server to reach the
consistent state":
+#define NUM_CONN_ATTEMPTS      5
+
+       pg_log_info("waiting the target server to reach the consistent state");
+
+       conn = connect_database(conninfo, true);

Fixed.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#178vignesh C
vignesh21@gmail.com
In reply to: Euler Taveira (#176)
Re: speed up a logical replica setup

On Thu, 7 Mar 2024 at 10:05, Euler Taveira <euler@eulerto.com> wrote:

On Wed, Mar 6, 2024, at 7:02 AM, Hayato Kuroda (Fujitsu) wrote:

Thanks for updating the patch!

Thanks for the feedback. I'm attaching v26 that addresses most of your comments
and some issues pointed by Vignesh [1].

Few comments:
1) We are disconnecting database again in error case too, it will lead
to a double free in this scenario,
+       PQclear(res);
+
+       disconnect_database(conn, false);
+
+       if (max_repslots < num_dbs)
+       {
+               pg_log_error("subscriber requires %d replication
slots, but only %d remain",
+                                        num_dbs, max_repslots);
+               pg_log_error_hint("Consider increasing
max_replication_slots to at least %d.",
+                                                 num_dbs);
+               disconnect_database(conn, true);
+       }
+
+       if (max_lrworkers < num_dbs)
+       {
+               pg_log_error("subscriber requires %d logical
replication workers, but only %d remain",
+                                        num_dbs, max_lrworkers);
+               pg_log_error_hint("Consider increasing
max_logical_replication_workers to at least %d.",
+                                                 num_dbs);
+               disconnect_database(conn, true);
+       }

pg_createsubscriber: error: subscriber requires 5 logical replication
workers, but only 4 remain
pg_createsubscriber: hint: Consider increasing
max_logical_replication_workers to at least 5.
free(): double free detected in tcache 2
Aborted

2) We can also check that the primary is not using
synchronous_standby_names, else all the transactions in the primary
will wait infinitely once the standby server is stopped, this could be
added in the documentation:
+/*
+ * Is the primary server ready for logical replication?
+ */
+static void
+check_publisher(struct LogicalRepInfo *dbinfo)
+{
+       PGconn     *conn;
+       PGresult   *res;
+
+       char       *wal_level;
+       int                     max_repslots;
+       int                     cur_repslots;
+       int                     max_walsenders;
+       int                     cur_walsenders;
+
+       pg_log_info("checking settings on publisher");
+
+       conn = connect_database(dbinfo[0].pubconninfo, true);
+
+       /*
+        * If the primary server is in recovery (i.e. cascading replication),
+        * objects (publication) cannot be created because it is read only.
+        */
+       if (server_is_in_recovery(conn))
+       {
+               pg_log_error("primary server cannot be in recovery");
+               disconnect_database(conn, true);
+       }
3) This check is present only for publication, we do not have this in
case of creating a subscription. We can keep both of them similar,
i.e. have the check in both or don't have the check for both
publication and subscription:
+       /* Check if the publication already exists */
+       appendPQExpBuffer(str,
+                                         "SELECT 1 FROM
pg_catalog.pg_publication "
+                                         "WHERE pubname = '%s'",
+                                         dbinfo->pubname);
+       res = PQexec(conn, str->data);
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               pg_log_error("could not obtain publication information: %s",
+                                        PQresultErrorMessage(res));
+               disconnect_database(conn, true);
+       }
+
4) Few options are missing:
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>

4a) -n, --dry-run
4b) -p, --subscriber-port
4c) -r, --retain
4d) -s, --socket-directory
4e) -t, --recovery-timeout
4f) -U, --subscriber-username

5) typo connnections should be connections
+       <para>
+        The port number on which the target server is listening for
connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connnections.
6) repliation should be replication
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
7) I did not notice these changes in the latest patch:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d808aad8b0..08de2bf4e6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -517,6 +517,7 @@ CreateSeqStmt
 CreateStatsStmt
 CreateStmt
 CreateStmtContext
+CreateSubscriberOptions
 CreateSubscriptionStmt
 CreateTableAsStmt
 CreateTableSpaceStmt
@@ -1505,6 +1506,7 @@ LogicalRepBeginData
 LogicalRepCommitData
 LogicalRepCommitPreparedTxnData
 LogicalRepCtxStruct
+LogicalRepInfo
 LogicalRepMsgType
 LogicalRepPartMapEntry
 LogicalRepPreparedTxnData

Regards,
Vignesh

#179Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Euler Taveira (#176)
4 attachment(s)
Re: speed up a logical replica setup

Hi,

Thanks for the feedback. I'm attaching v26 that addresses most of your comments
and some issues pointed by Vignesh [1].

I have created a top-up patch v27-0004. It contains additional test
cases for pg_createsubscriber.

Currently, two testcases (in v27-0004 patch) are failing. These
failures are related to running pg_createsubscriber on nodes in
cascade physical replication and are already reported in [1]/messages/by-id/CAHv8Rj+5mzK9Jt+7ECogJzfm5czvDCCd5jO1_rCx0bTEYpBE5g@mail.gmail.com and [2]/messages/by-id/CAA4eK1Kq8qWiBK1-ky+jkuJRedoWLh5=VOmd+Ywh9L8PUxdq+Q@mail.gmail.com.
I think these cases should be fixed. Thoughts?

The idea of this patch is to keep track of testcases, so that any
future patch does not break any scenario which has already been worked
on. These testcases can be used to test in our development process,
but which test should actually be committed, can be discussed later.
Thought?

[1]: /messages/by-id/CAHv8Rj+5mzK9Jt+7ECogJzfm5czvDCCd5jO1_rCx0bTEYpBE5g@mail.gmail.com
[2]: /messages/by-id/CAA4eK1Kq8qWiBK1-ky+jkuJRedoWLh5=VOmd+Ywh9L8PUxdq+Q@mail.gmail.com

Thanks and regards,
Shlok Kyal

Attachments:

v27-0001-pg_createsubscriber-creates-a-new-logical-replic.patchapplication/x-patch; name=v27-0001-pg_createsubscriber-creates-a-new-logical-replic.patchDownload
From 4bcf2b80da120832d4e1d5217f7ee0196517d904 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v27 1/4] pg_createsubscriber: creates a new logical replica
 from a standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber).

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
for each specified database. After that, it stops the target server. One
temporary replication slot is created to get the replication start
point. It is used as (a) a stopping point for the recovery process and
(b) a starting point for the subscriptions. Write recovery parameters
into the target data directory and start the target server. Wait until
the target server is promoted. Create one subscription per specified
database (using replication slot and publication created in a previous
step) on the target server. Set the replication progress to the
replication start point for each subscription. Enable the subscription
for each specified database on the target server. And finally, change
the system identifier on the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  522 +++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2049 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  214 ++
 8 files changed, 2815 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..1664101075
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,522 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. It must be run at the target server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because it speeds up the logical replication setup. For smaller
+   databases, <link linkend="logical-replication">initial data synchronization</link>
+   is recommended.
+  </para>
+
+  <para>
+   There are some prerequisites for <application>pg_createsubscriber</application>
+   to convert the target server into a logical replica. If these are not met an
+   error will be reported.
+  </para>
+
+  <itemizedlist id="app-pg-createsubscriber-description-prerequisites">
+   <listitem>
+    <para>
+     The source and target servers must have the same major version as the
+     <application>pg_createsubscriber</application>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The given target data directory must have the same system identifier than
+     the source data directory. If a standby server is running on the target
+     data directory or it is a base backup from the source data directory,
+     system identifiers are the same.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must be used as a physical standby.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The given database user for the target data directory must have privileges
+     for creating <link linkend="sql-createsubscription">subscriptions</link>
+     and using <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     and
+     <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+     configured to a value greater than or equal to the number of specified databases.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must have
+     <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+     configured to a value greater than the number of specified databases.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must accept local connections.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must accept connections from the target server.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must not be in recovery. Publications cannot be created
+     in a read-only cluster.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+     <literal>logical</literal>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     configured to a value greater than the number of specified databases plus
+     existing replication slots.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+     configured to a value greater than or equal to the number of specified
+     databases and existing <literal>walsender</literal> processes.
+    </para>
+   </listitem>
+  </itemizedlist>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    then the data directory is likely not in a state that can be recovered. It
+    is true if the target server was promoted. In such a case, creating a new
+    standby server is recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during the transformation steps.
+    Hence, connections to target server might fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Executing DDL commands on the source server while running
+    <application>pg_createsubscriber</application> is not recommended. If the
+    target server has already been converted to logical replica, the DDL
+    commands must not be replicated so an error would occur.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    should be removed. The removal might fail if the target server cannot
+    connect to the source server. In such a case, a warning message will inform
+    the objects left.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which WAL files from the source server might be used by the target server.
+    If the target server has a standby, replication will break and a fresh
+    standby should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connnections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is already running, stop it because some parameters can only
+     be set at server start.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. All
+     <link linkend="app-pg-createsubscriber-description-prerequisites">prerequisites</link>
+     are checked. If any of them are not met, <application>pg_createsubscriber</application>
+     will terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u</literal></quote> (parameter:
+     database <parameter>oid</parameter>). Each replication slot has the
+     following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a temporary replication slot to get a consistent start location.
+     This replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
+     (parameter: PID <parameter>int</parameter>). The LSN returned by
+     <link linkend="pg-create-logical-replication-slot"><function>pg_create_logical_replication_slot()</function></link>
+     is used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
+     parameter and by the subscriptions as a replication start point. It
+     guarantees that no transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies a LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are also added so it avoids unexpected behavior during the recovery
+     process such as end of the recovery as soon as a consistent state is
+     reached (WAL should be applied until the consistent start location) and
+     multiple recovery targets that can cause a failure. This step finishes
+     once the server ends standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     The subscription has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>). The
+     replication slot name is identical to the subscription name. It does not
+     copy existing data from the source server. It does not create a
+     replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the consistent LSN
+     but replication origin name contains the subscription oid in its name.
+     Hence, the subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the consistent start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the consistent start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the consistent start point. This is the exact LSN to be used
+     as a initial location for each subscription. The replication origin name
+     is obtained since the subscription was created. The replication origin
+     name and the consistent start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the consistent start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..e9a920dbcd 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..3ccf3348a2
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2049 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "common/username.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	50432
+#define	BASE_OUTPUT_DIR		"pg_createsubscriber_output.d"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	unsigned short sub_port;	/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+}			CreateSubscriberOptions;
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name / replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+}			LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path,
+									struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static void check_publisher(struct LogicalRepInfo *dbinfo);
+static void setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir);
+static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
+										  char *slotname);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(struct CreateSubscriberOptions *opt,
+								 const char *pg_ctl_path, const char *logfile,
+								 bool with_options);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+								  struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static bool recovery_ended = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("pg_createsubscriber failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.");
+		pg_log_warning_hint("You must recreate the physical replica before continuing.");
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			PGconn	   *conn;
+
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].subname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %d)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	int			i = 0;
+
+	dbinfo = (struct LogicalRepInfo *) pg_malloc(num_dbs * sizeof(struct LogicalRepInfo));
+
+	for (SimpleStringListCell *cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): connection string: %s", i, dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): connection string: %s", i, dbinfo[i].subconninfo);
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static void
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+
+		/* Remember database OID */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static void
+check_publisher(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted +
+	 *                            one temporary logical replication slot
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+		pg_fatal("publisher requires wal_level >= logical");
+
+	/* One additional temporary logical replication slot */
+	if (max_repslots - cur_repslots < num_dbs + 1)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs + 1, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs + 1);
+		exit(1);
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		exit(1);
+	}
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static void
+check_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("The target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		disconnect_database(conn, true);
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		disconnect_database(conn, true);
+	}
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		disconnect_database(conn, true);
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		disconnect_database(conn, true);
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		disconnect_database(conn, true);
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		disconnect_database(conn, true);
+	}
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Get a replication start location and write the required recovery parameters
+ * into the configuration file. Returns the replication start location that is
+ * used to adjust the subscriptions (see set_replication_progress).
+ */
+static char *
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
+{
+	char	   *consistent_lsn;
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	/*
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection. The primary_conninfo is generated using the
+	 * connection settings.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	return consistent_lsn;
+}
+
+/*
+ * Drop physical replication slot on primary if the standby was using it. After
+ * the transformation, it has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, char *slotname)
+{
+	PGconn	   *conn;
+
+	/* Replication slot does not exist, do nothing */
+	if (!primary_slot_name)
+		return;
+
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn != NULL)
+	{
+		drop_replication_slot(conn, &dbinfo[0], slotname);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop replication slot \"%s\" on primary",
+					   slotname);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/* This temporary replication slot is only used for catchup purposes */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it
+ * starts the transformation.
+ * In dry run mode, doesn't create the BASE_OUTPUT_DIR directory, instead
+ * returns the full log file path.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, BASE_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (!dry_run && mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* Append timestamp with ISO 8601 format */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/server_start_%s.log", base_dir, timebuf);
+	pg_log_debug("log file is: %s", filename);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_path,
+					 const char *logfile, bool with_options)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	char		socket_string[MAXPGPATH + 200];
+	int			rc;
+
+	socket_string[0] = '\0';
+
+#if !defined(WIN32)
+
+	/*
+	 * An empty listen_addresses list means the server does not listen on any
+	 * IP interfaces; only Unix-domain sockets can be used to connect to the
+	 * server. Prevent external connections to minimize the chance of failure.
+	 */
+	strcat(socket_string,
+		   " -c listen_addresses='' -c unix_socket_permissions=0700");
+
+	if (opt->socket_dir)
+		snprintf(socket_string + strlen(socket_string),
+				 sizeof(socket_string) - strlen(socket_string),
+				 " -c unix_socket_directories='%s'",
+				 opt->socket_dir);
+#endif
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, opt->subscriber_dir);
+	if (with_options)
+	{
+		/*
+		 * Don't include the log file in dry run mode because the directory
+		 * that contains it was not created in setup_server_logfile().
+		 */
+		if (!dry_run)
+			appendPQExpBuffer(pg_ctl_cmd, " -l \"%s\"", logfile);
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %u%s\"",
+						  opt->sub_port, socket_string);
+	}
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+					  struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	5
+
+	pg_log_info("waiting the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"retain", no_argument, NULL, 'r'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_ctl_path = NULL;
+	char	   *pg_resetwal_path = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo;
+	char	   *sub_base_conninfo;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	char	   *consistent_lsn;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:np:P:rs:t:U:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(opt.subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				if ((opt.sub_port = atoi(optarg)) <= 0)
+					pg_fatal("invalid subscriber port number");
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				canonicalize_path(opt.socket_dir);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/* Any non-option arguments? */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* Required arguments */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* If socket directory is not provided, use the current directory */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 * If subscriber username is not provided, check if the environment
+	 * variable sets it. If not, obtain the operating system name of the user
+	 * running it.
+	 */
+	if (opt.sub_username == NULL)
+	{
+		if (getenv("PGUSER"))
+		{
+			opt.sub_username = getenv("PGUSER");
+		}
+		else
+		{
+			char	   *errstr = NULL;
+
+			opt.sub_username = get_user_name(&errstr);
+			if (errstr)
+				pg_fatal("%s", errstr);
+		}
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = psprintf("host=%s port=%u user=%s fallback_application_name=%s",
+								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(opt.subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Create the output directory to store any data generated by this tool */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * If the standby server is running, stop it. Some parameters (that can
+	 * only be set at server start) are informed by command-line options.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, pg_ctl_path, server_start_log, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	setup_publisher(dbinfo);
+
+	/*
+	 * Get the replication start point and write the required recovery
+	 * parameters into the configuration file.
+	 */
+	consistent_lsn = setup_recovery(dbinfo, opt.subscriber_dir);
+
+	/*
+	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("stopping and starting the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	start_standby_server(&opt, pg_ctl_path, server_start_log, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the replication start
+	 * point to the LSN reported by setup_recovery().  It also cleans up
+	 * publications created by this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/* Remove primary_slot_name if it exists on primary */
+	drop_primary_replication_slot(dbinfo, primary_slot_name);
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(pg_resetwal_path, &opt);
+
+	/*
+	 * In dry run mode, the server is restarted with the provided command-line
+	 * options so validation can be applied in the target server. In order to
+	 * preserve the initial state of the server (running), start it without
+	 * the command-line options.
+	 */
+	if (dry_run)
+		start_standby_server(&opt, pg_ctl_path, NULL, false);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!dry_run && !opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..5a2b8e9a56
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,214 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Stop node C
+$node_c->teardown_node;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+ok( -d $node_s->data_dir . "/pg_createsubscriber_output.d",
+	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success"
+);
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.34.1

v27-0002-Use-latest-replication-slot-position-as-replicat.patchapplication/x-patch; name=v27-0002-Use-latest-replication-slot-position-as-replicat.patchDownload
From 444cef5aa420e421b6ab2621709fc97e8506716d Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Wed, 6 Mar 2024 17:50:01 -0300
Subject: [PATCH v27 2/4] Use latest replication slot position as replication
 start point

Instead of using a temporary replication slot, use the latest
replication slot position (LSN).
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 60 ++++++++++-----------
 1 file changed, 29 insertions(+), 31 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 3ccf3348a2..d9797ed8f4 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -74,11 +74,12 @@ static void modify_subscriber_sysid(const char *pg_resetwal_path,
 									struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static void check_publisher(struct LogicalRepInfo *dbinfo);
-static void setup_publisher(struct LogicalRepInfo *dbinfo);
+static char *setup_publisher(struct LogicalRepInfo *dbinfo);
 static void check_subscriber(struct LogicalRepInfo *dbinfo);
 static void setup_subscriber(struct LogicalRepInfo *dbinfo,
 							 const char *consistent_lsn);
-static char *setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir);
+static void setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir,
+						   char *lsn);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  char *slotname);
 static char *create_logical_replication_slot(PGconn *conn,
@@ -574,11 +575,15 @@ modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOpt
 
 /*
  * Create the publications and replication slots in preparation for logical
- * replication.
+ * replication. Returns the LSN from latest replication slot. It will be the
+ * replication start point that is used to adjust the subscriptions (see
+ * set_replication_progress).
  */
-static void
+static char *
 setup_publisher(struct LogicalRepInfo *dbinfo)
 {
+	char	   *lsn = NULL;
+
 	for (int i = 0; i < num_dbs; i++)
 	{
 		PGconn	   *conn;
@@ -641,8 +646,10 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		dbinfo[i].subname = pg_strdup(replslotname);
 
 		/* Create replication slot on publisher */
-		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
-			dry_run)
+		if (lsn)
+			pg_free(lsn);
+		lsn = create_logical_replication_slot(conn, &dbinfo[i], false);
+		if (lsn != NULL || dry_run)
 			pg_log_info("create replication slot \"%s\" on publisher",
 						replslotname);
 		else
@@ -650,6 +657,8 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 
 		disconnect_database(conn, false);
 	}
+
+	return lsn;
 }
 
 /*
@@ -995,14 +1004,11 @@ setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
 }
 
 /*
- * Get a replication start location and write the required recovery parameters
- * into the configuration file. Returns the replication start location that is
- * used to adjust the subscriptions (see set_replication_progress).
+ * Write the required recovery parameters.
  */
-static char *
-setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
+static void
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir, char *lsn)
 {
-	char	   *consistent_lsn;
 	PGconn	   *conn;
 	PQExpBuffer recoveryconfcontents;
 
@@ -1016,15 +1022,12 @@ setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
 	/*
 	 * Write recovery parameters.
 	 *
-	 * Despite of the recovery parameters will be written to the subscriber,
-	 * use a publisher connection for the following recovery functions. The
-	 * connection is only used to check the current server version (physical
-	 * replica, same server version). The subscriber is not running yet. In
-	 * dry run mode, the recovery parameters *won't* be written. An invalid
-	 * LSN is used for printing purposes. Additional recovery parameters are
-	 * added here. It avoids unexpected behavior such as end of recovery as
-	 * soon as a consistent state is reached (recovery_target) and failure due
-	 * to multiple recovery targets (name, time, xid, LSN).
+	 * The subscriber is not running yet. In dry run mode, the recovery
+	 * parameters *won't* be written. An invalid LSN is used for printing
+	 * purposes. Additional recovery parameters are added here. It avoids
+	 * unexpected behavior such as end of recovery as soon as a consistent
+	 * state is reached (recovery_target) and failure due to multiple recovery
+	 * targets (name, time, xid, LSN).
 	 */
 	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
@@ -1048,14 +1051,12 @@ setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
 	else
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
-						  consistent_lsn);
+						  lsn);
 		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
 
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
-
-	return consistent_lsn;
 }
 
 /*
@@ -1988,13 +1989,10 @@ main(int argc, char **argv)
 	 * primary slot is in use. We could use an extra connection for it but it
 	 * doesn't seem worth.
 	 */
-	setup_publisher(dbinfo);
+	consistent_lsn = setup_publisher(dbinfo);
 
-	/*
-	 * Get the replication start point and write the required recovery
-	 * parameters into the configuration file.
-	 */
-	consistent_lsn = setup_recovery(dbinfo, opt.subscriber_dir);
+	/* Write the required recovery parameters */
+	setup_recovery(dbinfo, opt.subscriber_dir, consistent_lsn);
 
 	/*
 	 * Restart subscriber so the recovery parameters will take effect. Wait
@@ -2010,7 +2008,7 @@ main(int argc, char **argv)
 	/*
 	 * Create the subscription for each database on subscriber. It does not
 	 * enable it immediately because it needs to adjust the replication start
-	 * point to the LSN reported by setup_recovery().  It also cleans up
+	 * point to the LSN reported by setup_publisher().  It also cleans up
 	 * publications created by this tool and replication to the standby.
 	 */
 	setup_subscriber(dbinfo, consistent_lsn);
-- 
2.34.1

v27-0003-port-replace-int-with-string.patchapplication/x-patch; name=v27-0003-port-replace-int-with-string.patchDownload
From 7ed803615fdbebf2054b5cbd783fb667465e7998 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Wed, 6 Mar 2024 22:00:23 -0300
Subject: [PATCH v27 3/4] port: replace int with string

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index d9797ed8f4..85bbc3ba8f 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -28,7 +28,7 @@
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
 
-#define	DEFAULT_SUB_PORT	50432
+#define	DEFAULT_SUB_PORT	"50432"
 #define	BASE_OUTPUT_DIR		"pg_createsubscriber_output.d"
 
 /* Command-line options */
@@ -37,7 +37,7 @@ struct CreateSubscriberOptions
 	char	   *subscriber_dir; /* standby/subscriber data directory */
 	char	   *pub_conninfo_str;	/* publisher connection string */
 	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
-	unsigned short sub_port;	/* subscriber port number */
+	char	   *sub_port;		/* subscriber port number */
 	const char *sub_username;	/* subscriber username */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;			/* retain log file? */
@@ -200,7 +200,7 @@ usage(void)
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
 	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
-	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %d)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %s)\n"), DEFAULT_SUB_PORT);
 	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -r, --retain                        retain log file after success\n"));
 	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
@@ -1295,7 +1295,7 @@ start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_pat
 		 */
 		if (!dry_run)
 			appendPQExpBuffer(pg_ctl_cmd, " -l \"%s\"", logfile);
-		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %u%s\"",
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %s%s\"",
 						  opt->sub_port, socket_string);
 	}
 	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
@@ -1336,7 +1336,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 
 #define NUM_CONN_ATTEMPTS	5
 
-	pg_log_info("waiting the target server to reach the consistent state");
+	pg_log_info("waiting for the target server to reach the consistent state");
 
 	conn = connect_database(conninfo, true);
 
@@ -1743,7 +1743,8 @@ main(int argc, char **argv)
 	opt.subscriber_dir = NULL;
 	opt.pub_conninfo_str = NULL;
 	opt.socket_dir = NULL;
-	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_port = palloc(16);
+	strcpy(opt.sub_port, DEFAULT_SUB_PORT);
 	opt.sub_username = NULL;
 	opt.database_names = (SimpleStringList)
 	{
@@ -1789,8 +1790,8 @@ main(int argc, char **argv)
 				dry_run = true;
 				break;
 			case 'p':
-				if ((opt.sub_port = atoi(optarg)) <= 0)
-					pg_fatal("invalid subscriber port number");
+				pg_free(opt.sub_port);
+				opt.sub_port = pg_strdup(optarg);
 				break;
 			case 'P':
 				opt.pub_conninfo_str = pg_strdup(optarg);
@@ -1890,7 +1891,7 @@ main(int argc, char **argv)
 		exit(1);
 
 	pg_log_info("validating connection string on subscriber");
-	sub_base_conninfo = psprintf("host=%s port=%u user=%s fallback_application_name=%s",
+	sub_base_conninfo = psprintf("host=%s port=%s user=%s fallback_application_name=%s",
 								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
 
 	if (opt.database_names.head == NULL)
-- 
2.34.1

v27-0004-Add-additional-testcases.patchapplication/x-patch; name=v27-0004-Add-additional-testcases.patchDownload
From 24efeca0553c5a22f9c790e92543434ab2100d26 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 7 Mar 2024 18:00:29 +0530
Subject: [PATCH v27 4/4] Add additional testcases

Add additional testcases to test on pg_createsubscriber
---
 src/bin/pg_basebackup/t/041_tests.pl | 285 +++++++++++++++++++++++++++
 1 file changed, 285 insertions(+)
 create mode 100644 src/bin/pg_basebackup/t/041_tests.pl

diff --git a/src/bin/pg_basebackup/t/041_tests.pl b/src/bin/pg_basebackup/t/041_tests.pl
new file mode 100644
index 0000000000..2889d60d54
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_tests.pl
@@ -0,0 +1,285 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Set different parameters in postgresql.conf
+
+# set max_replication_slots
+$node_p->append_conf('postgresql.conf', 'max_replication_slots = 3');
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are less in number in publisher');
+
+$node_p->append_conf('postgresql.conf', 'max_replication_slots = 4');
+$node_p->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are accurate on publisher');
+
+$node_s->append_conf('postgresql.conf', 'max_replication_slots = 1');
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are less in number in subscriber');
+
+$node_s->append_conf('postgresql.conf', 'max_replication_slots = 2');
+$node_s->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are less in number in subscriber');
+
+# set wal_level on publisher
+$node_p->append_conf('postgresql.conf', 'wal_level = \'replica\'');
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'wal_level must be logical');
+
+$node_p->append_conf('postgresql.conf', 'wal_level = \'logical\'');
+$node_p->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'wal_level is logical');
+
+# set max_wal_senders on publisher
+$node_p->append_conf('postgresql.conf', 'max_wal_senders = 2');
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_wal_senders is not sufficient');
+
+$node_p->append_conf('postgresql.conf', 'max_wal_senders = 3');
+$node_p->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_wal_senders is sufficient');
+
+# set max_logical_replication_workers on subscriber
+$node_s->append_conf('postgresql.conf',
+	'max_logical_replication_workers = 1');
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_logical_replication_workers is not sufficient');
+
+$node_s->append_conf('postgresql.conf',
+	'max_logical_replication_workers = 2');
+$node_s->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_logical_replication_workers is sufficient');
+
+# max_worker_processes on subscriber
+$node_p->append_conf('postgresql.conf', 'max_worker_processes = 2');
+$node_p->restart;
+sleep 1;
+$node_s->append_conf('postgresql.conf', 'max_worker_processes = 2');
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_worker_processes is not sufficient');
+
+$node_p->append_conf('postgresql.conf', 'max_worker_processes = 3');
+$node_p->restart;
+sleep 1;
+$node_s->append_conf('postgresql.conf', 'max_worker_processes = 3');
+$node_s->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_worker_processes is sufficient');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Cascade Streaming Replication
+
+# Run pg_createsubscriber on node S (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'standby server is primary to other node');
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+# Specify P as the publisher and C as standby
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_c->host, '--subscriber-port',
+		$node_c->port, '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'standby server is not direct standby of publisher');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_c->teardown_node;
+
+done_testing();
-- 
2.34.1

#180Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Euler Taveira (#176)
Re: speed up a logical replica setup

Hi,

I decided to take a quick look on this patch today, to see how it works
and do some simple tests. I've only started to get familiar with it, so
I have only some comments / questions regarding usage, not on the code.
It's quite possible I didn't understand some finer points, or maybe it
was already discussed earlier in this very long thread, so please feel
free to push back or point me to the past discussion.

Also, some of this is rather opinionated, but considering I didn't see
this patch before, my opinions may easily be wrong ...

1) SGML docs

It seems the SGML docs are more about explaining how this works on the
inside, rather than how to use the tool. Maybe that's intentional, but
as someone who didn't work with pg_createsubscriber before I found it
confusing and not very helpful.

For example, the first half of the page is prerequisities+warning, and
sure those are useful details, but prerequisities are checked by the
tool (so I can't really miss this) and warnings go into a lot of details
about different places where things may go wrong. Sure, worth knowing
and including in the docs, but maybe not right at the beginning, before
I learn how to even run the tool?

Maybe that's just me, though. Also, I'm sure it's not the only part of
our docs like this. Perhaps it'd be good to reorganize the content a bit
to make the "how to use" stuff more prominent?

2) this is a bit vague

... pg_createsubscriber will check a few times if the connection has
been reestablished to stream the required WAL. After a few attempts, it
terminates with an error.

What does "a few times" mean, and how many is "a few attempts"? Seems
worth knowing when using this tool in environments where disconnections
can happen. Maybe this should be configurable?

3) publication info

For a while I was quite confused about which tables get replicated,
until I realized the publication is FOR ALL TABLES. But I learned that
from this thread, the docs say nothing about this. Surely that's an
important detail that should be mentioned?

4) Is FOR ALL TABLES a good idea?

I'm not sure FOR ALL TABLES is a good idea. Or said differently, I'm
sure it won't work for a number of use cases. I know large databases
it's common to create "work tables" (not necessarily temporary) as part
of a batch job, but there's no need to replicate those tables.

AFAIK that'd break this FOR ALL TABLES publication, because the tables
will qualify for replication, but won't be present on the subscriber. Or
did I miss something?

I do understand that FOR ALL TABLES is the simplest approach, and for v1
it may be an acceptable limitation, but maybe it'd be good to also
support restricting which tables should be replicated (e.g. blacklist or
whitelist based on table/schema name?).

BTW if I'm right and creating a table breaks the subscriber creation,
maybe it'd be good to explicitly mention that in the docs.

Note: I now realize this might fall under the warning about DDL, which
says this:

Executing DDL commands on the source server while running
pg_createsubscriber is not recommended. If the target server has
already been converted to logical replica, the DDL commands must
not be replicated so an error would occur.

But I find this confusing. Surely there are many DDL commands that have
absolutely no impact on logical replication (like creating an index or
view, various ALTER TABLE flavors, and so on). And running such DDL
certainly does not trigger error, right?

5) slot / publication / subscription name

I find it somewhat annoying it's not possible to specify names for
objects created by the tool - replication slots, publication and
subscriptions. If this is meant to be a replica running for a while,
after a while I'll have no idea what pg_createsubscriber_569853 or
pg_createsubscriber_459548_2348239 was meant for.

This is particularly annoying because renaming these objects later is
either not supported at all (e.g. for replication slots), or may be
quite difficult (e.g. publications).

I do realize there are challenges with custom names (say, if there are
multiple databases to replicate), but can't we support some simple
formatting with basic placeholders? So we could specify

--slot-name "myslot_%d_%p"

or something like that?

BTW what will happen if we convert multiple standbys? Can't they all get
the same slot name (they all have the same database OID, and I'm not
sure how much entropy the PID has)?

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#181Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Euler Taveira (#176)
5 attachment(s)
Re: speed up a logical replica setup

Hi,

Thanks for the feedback. I'm attaching v26 that addresses most of your comments
and some issues pointed by Vignesh [1].

I have tested the patch in windows. The pg_createsubscriber command is
failing in windows:

pg_createsubscriber -D ..\standby -d postgres -P "host=localhost
port=5432" --subscriber-port 9000 -r -v
pg_createsubscriber: validating connection string on publisher
pg_createsubscriber: validating connection string on subscriber
pg_createsubscriber: checking if directory "../standby" is a cluster
data directory
pg_createsubscriber: getting system identifier from publisher
pg_createsubscriber: system identifier is 7343852918334005220 on publisher
pg_createsubscriber: getting system identifier from subscriber
pg_createsubscriber: system identifier is 7343852918334005220 on subscriber
pg_createsubscriber: standby is up and running
pg_createsubscriber: stopping the server to start the transformation steps
pg_createsubscriber: server was stopped
pg_createsubscriber: starting the standby with command-line options
pg_createsubscriber: server was started
pg_createsubscriber: checking settings on subscriber
pg_createsubscriber: error: connection to database failed: connection
to server on socket "D:/project/pg_euler_v27_debug/bin/.s.PGSQL.9000"
failed: Connection refused (0x0000274D/10061)
Is the server running locally and accepting connections on that socket?

I found out that
+ sub_base_conninfo = psprintf("host=%s port=%u user=%s
fallback_application_name=%s",
+                opt.socket_dir, opt.sub_port, opt.sub_username, progname);

sub_base_conninfo has 'host' even for windows.
So when 'start_standby_server' is called it starts the server in
localhost but when we try to connect to standby inside
'check_subscriber', it tries to connect to the host defined in the
string 'sub_base_conninfo'. So, we are getting the error.

Created a top-up patch v27-0005 to resolve this.
Since there is no change in 0001, 0002...0004 patches that was
previously posted, I have reused the same version number.

Thanks and regards,
Shlok Kyal

Attachments:

v27-0004-Add-additional-testcases.patchapplication/octet-stream; name=v27-0004-Add-additional-testcases.patchDownload
From 3fbdec10a50d7b358b20f59892458f6ae49e7c93 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 7 Mar 2024 18:00:29 +0530
Subject: [PATCH v27 4/5] Add additional testcases

Add additional testcases to test on pg_createsubscriber
---
 src/bin/pg_basebackup/t/041_tests.pl | 285 +++++++++++++++++++++++++++
 1 file changed, 285 insertions(+)
 create mode 100644 src/bin/pg_basebackup/t/041_tests.pl

diff --git a/src/bin/pg_basebackup/t/041_tests.pl b/src/bin/pg_basebackup/t/041_tests.pl
new file mode 100644
index 0000000000..2889d60d54
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_tests.pl
@@ -0,0 +1,285 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Set different parameters in postgresql.conf
+
+# set max_replication_slots
+$node_p->append_conf('postgresql.conf', 'max_replication_slots = 3');
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are less in number in publisher');
+
+$node_p->append_conf('postgresql.conf', 'max_replication_slots = 4');
+$node_p->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are accurate on publisher');
+
+$node_s->append_conf('postgresql.conf', 'max_replication_slots = 1');
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are less in number in subscriber');
+
+$node_s->append_conf('postgresql.conf', 'max_replication_slots = 2');
+$node_s->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are less in number in subscriber');
+
+# set wal_level on publisher
+$node_p->append_conf('postgresql.conf', 'wal_level = \'replica\'');
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'wal_level must be logical');
+
+$node_p->append_conf('postgresql.conf', 'wal_level = \'logical\'');
+$node_p->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'wal_level is logical');
+
+# set max_wal_senders on publisher
+$node_p->append_conf('postgresql.conf', 'max_wal_senders = 2');
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_wal_senders is not sufficient');
+
+$node_p->append_conf('postgresql.conf', 'max_wal_senders = 3');
+$node_p->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_wal_senders is sufficient');
+
+# set max_logical_replication_workers on subscriber
+$node_s->append_conf('postgresql.conf',
+	'max_logical_replication_workers = 1');
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_logical_replication_workers is not sufficient');
+
+$node_s->append_conf('postgresql.conf',
+	'max_logical_replication_workers = 2');
+$node_s->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_logical_replication_workers is sufficient');
+
+# max_worker_processes on subscriber
+$node_p->append_conf('postgresql.conf', 'max_worker_processes = 2');
+$node_p->restart;
+sleep 1;
+$node_s->append_conf('postgresql.conf', 'max_worker_processes = 2');
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_worker_processes is not sufficient');
+
+$node_p->append_conf('postgresql.conf', 'max_worker_processes = 3');
+$node_p->restart;
+sleep 1;
+$node_s->append_conf('postgresql.conf', 'max_worker_processes = 3');
+$node_s->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_worker_processes is sufficient');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Cascade Streaming Replication
+
+# Run pg_createsubscriber on node S (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'standby server is primary to other node');
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+# Specify P as the publisher and C as standby
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_c->host, '--subscriber-port',
+		$node_c->port, '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'standby server is not direct standby of publisher');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_c->teardown_node;
+
+done_testing();
-- 
2.41.0.windows.3

v27-0001-pg_createsubscriber-creates-a-new-logical-replic.patchapplication/octet-stream; name=v27-0001-pg_createsubscriber-creates-a-new-logical-replic.patchDownload
From 2b60cd273854e14ddd28dd051ceca054a8eac6f2 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v27 1/5] pg_createsubscriber: creates a new logical replica
 from a standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber).

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
for each specified database. After that, it stops the target server. One
temporary replication slot is created to get the replication start
point. It is used as (a) a stopping point for the recovery process and
(b) a starting point for the subscriptions. Write recovery parameters
into the target data directory and start the target server. Wait until
the target server is promoted. Create one subscription per specified
database (using replication slot and publication created in a previous
step) on the target server. Set the replication progress to the
replication start point for each subscription. Enable the subscription
for each specified database on the target server. And finally, change
the system identifier on the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  522 +++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2049 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  214 ++
 8 files changed, 2815 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..1664101075
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,522 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. It must be run at the target server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because it speeds up the logical replication setup. For smaller
+   databases, <link linkend="logical-replication">initial data synchronization</link>
+   is recommended.
+  </para>
+
+  <para>
+   There are some prerequisites for <application>pg_createsubscriber</application>
+   to convert the target server into a logical replica. If these are not met an
+   error will be reported.
+  </para>
+
+  <itemizedlist id="app-pg-createsubscriber-description-prerequisites">
+   <listitem>
+    <para>
+     The source and target servers must have the same major version as the
+     <application>pg_createsubscriber</application>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The given target data directory must have the same system identifier than
+     the source data directory. If a standby server is running on the target
+     data directory or it is a base backup from the source data directory,
+     system identifiers are the same.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must be used as a physical standby.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The given database user for the target data directory must have privileges
+     for creating <link linkend="sql-createsubscription">subscriptions</link>
+     and using <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     and
+     <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+     configured to a value greater than or equal to the number of specified databases.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must have
+     <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+     configured to a value greater than the number of specified databases.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must accept local connections.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must accept connections from the target server.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must not be in recovery. Publications cannot be created
+     in a read-only cluster.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+     <literal>logical</literal>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     configured to a value greater than the number of specified databases plus
+     existing replication slots.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+     configured to a value greater than or equal to the number of specified
+     databases and existing <literal>walsender</literal> processes.
+    </para>
+   </listitem>
+  </itemizedlist>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    then the data directory is likely not in a state that can be recovered. It
+    is true if the target server was promoted. In such a case, creating a new
+    standby server is recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during the transformation steps.
+    Hence, connections to target server might fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Executing DDL commands on the source server while running
+    <application>pg_createsubscriber</application> is not recommended. If the
+    target server has already been converted to logical replica, the DDL
+    commands must not be replicated so an error would occur.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    should be removed. The removal might fail if the target server cannot
+    connect to the source server. In such a case, a warning message will inform
+    the objects left.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which WAL files from the source server might be used by the target server.
+    If the target server has a standby, replication will break and a fresh
+    standby should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connnections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is already running, stop it because some parameters can only
+     be set at server start.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. All
+     <link linkend="app-pg-createsubscriber-description-prerequisites">prerequisites</link>
+     are checked. If any of them are not met, <application>pg_createsubscriber</application>
+     will terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u</literal></quote> (parameter:
+     database <parameter>oid</parameter>). Each replication slot has the
+     following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a temporary replication slot to get a consistent start location.
+     This replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
+     (parameter: PID <parameter>int</parameter>). The LSN returned by
+     <link linkend="pg-create-logical-replication-slot"><function>pg_create_logical_replication_slot()</function></link>
+     is used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
+     parameter and by the subscriptions as a replication start point. It
+     guarantees that no transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies a LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are also added so it avoids unexpected behavior during the recovery
+     process such as end of the recovery as soon as a consistent state is
+     reached (WAL should be applied until the consistent start location) and
+     multiple recovery targets that can cause a failure. This step finishes
+     once the server ends standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     The subscription has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>). The
+     replication slot name is identical to the subscription name. It does not
+     copy existing data from the source server. It does not create a
+     replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the consistent LSN
+     but replication origin name contains the subscription oid in its name.
+     Hence, the subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the consistent start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the consistent start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the consistent start point. This is the exact LSN to be used
+     as a initial location for each subscription. The replication origin name
+     is obtained since the subscription was created. The replication origin
+     name and the consistent start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the consistent start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..e9a920dbcd 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..3ccf3348a2
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2049 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "common/username.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	50432
+#define	BASE_OUTPUT_DIR		"pg_createsubscriber_output.d"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	unsigned short sub_port;	/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+}			CreateSubscriberOptions;
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name / replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+}			LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path,
+									struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static void check_publisher(struct LogicalRepInfo *dbinfo);
+static void setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir);
+static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
+										  char *slotname);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(struct CreateSubscriberOptions *opt,
+								 const char *pg_ctl_path, const char *logfile,
+								 bool with_options);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+								  struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static bool recovery_ended = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("pg_createsubscriber failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.");
+		pg_log_warning_hint("You must recreate the physical replica before continuing.");
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			PGconn	   *conn;
+
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].subname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %d)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	int			i = 0;
+
+	dbinfo = (struct LogicalRepInfo *) pg_malloc(num_dbs * sizeof(struct LogicalRepInfo));
+
+	for (SimpleStringListCell *cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): connection string: %s", i, dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): connection string: %s", i, dbinfo[i].subconninfo);
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static void
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+
+		/* Remember database OID */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static void
+check_publisher(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted +
+	 *                            one temporary logical replication slot
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+		pg_fatal("publisher requires wal_level >= logical");
+
+	/* One additional temporary logical replication slot */
+	if (max_repslots - cur_repslots < num_dbs + 1)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs + 1, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs + 1);
+		exit(1);
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		exit(1);
+	}
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static void
+check_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("The target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		disconnect_database(conn, true);
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		disconnect_database(conn, true);
+	}
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		disconnect_database(conn, true);
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		disconnect_database(conn, true);
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		disconnect_database(conn, true);
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		disconnect_database(conn, true);
+	}
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Get a replication start location and write the required recovery parameters
+ * into the configuration file. Returns the replication start location that is
+ * used to adjust the subscriptions (see set_replication_progress).
+ */
+static char *
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
+{
+	char	   *consistent_lsn;
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	/*
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection. The primary_conninfo is generated using the
+	 * connection settings.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	return consistent_lsn;
+}
+
+/*
+ * Drop physical replication slot on primary if the standby was using it. After
+ * the transformation, it has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, char *slotname)
+{
+	PGconn	   *conn;
+
+	/* Replication slot does not exist, do nothing */
+	if (!primary_slot_name)
+		return;
+
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn != NULL)
+	{
+		drop_replication_slot(conn, &dbinfo[0], slotname);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop replication slot \"%s\" on primary",
+					   slotname);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/* This temporary replication slot is only used for catchup purposes */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it
+ * starts the transformation.
+ * In dry run mode, doesn't create the BASE_OUTPUT_DIR directory, instead
+ * returns the full log file path.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, BASE_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (!dry_run && mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* Append timestamp with ISO 8601 format */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/server_start_%s.log", base_dir, timebuf);
+	pg_log_debug("log file is: %s", filename);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_path,
+					 const char *logfile, bool with_options)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	char		socket_string[MAXPGPATH + 200];
+	int			rc;
+
+	socket_string[0] = '\0';
+
+#if !defined(WIN32)
+
+	/*
+	 * An empty listen_addresses list means the server does not listen on any
+	 * IP interfaces; only Unix-domain sockets can be used to connect to the
+	 * server. Prevent external connections to minimize the chance of failure.
+	 */
+	strcat(socket_string,
+		   " -c listen_addresses='' -c unix_socket_permissions=0700");
+
+	if (opt->socket_dir)
+		snprintf(socket_string + strlen(socket_string),
+				 sizeof(socket_string) - strlen(socket_string),
+				 " -c unix_socket_directories='%s'",
+				 opt->socket_dir);
+#endif
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, opt->subscriber_dir);
+	if (with_options)
+	{
+		/*
+		 * Don't include the log file in dry run mode because the directory
+		 * that contains it was not created in setup_server_logfile().
+		 */
+		if (!dry_run)
+			appendPQExpBuffer(pg_ctl_cmd, " -l \"%s\"", logfile);
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %u%s\"",
+						  opt->sub_port, socket_string);
+	}
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+					  struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	5
+
+	pg_log_info("waiting the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"retain", no_argument, NULL, 'r'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_ctl_path = NULL;
+	char	   *pg_resetwal_path = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo;
+	char	   *sub_base_conninfo;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	char	   *consistent_lsn;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:np:P:rs:t:U:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(opt.subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				if ((opt.sub_port = atoi(optarg)) <= 0)
+					pg_fatal("invalid subscriber port number");
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				canonicalize_path(opt.socket_dir);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/* Any non-option arguments? */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* Required arguments */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* If socket directory is not provided, use the current directory */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 * If subscriber username is not provided, check if the environment
+	 * variable sets it. If not, obtain the operating system name of the user
+	 * running it.
+	 */
+	if (opt.sub_username == NULL)
+	{
+		if (getenv("PGUSER"))
+		{
+			opt.sub_username = getenv("PGUSER");
+		}
+		else
+		{
+			char	   *errstr = NULL;
+
+			opt.sub_username = get_user_name(&errstr);
+			if (errstr)
+				pg_fatal("%s", errstr);
+		}
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = psprintf("host=%s port=%u user=%s fallback_application_name=%s",
+								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(opt.subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Create the output directory to store any data generated by this tool */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * If the standby server is running, stop it. Some parameters (that can
+	 * only be set at server start) are informed by command-line options.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, pg_ctl_path, server_start_log, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	setup_publisher(dbinfo);
+
+	/*
+	 * Get the replication start point and write the required recovery
+	 * parameters into the configuration file.
+	 */
+	consistent_lsn = setup_recovery(dbinfo, opt.subscriber_dir);
+
+	/*
+	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("stopping and starting the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	start_standby_server(&opt, pg_ctl_path, server_start_log, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the replication start
+	 * point to the LSN reported by setup_recovery().  It also cleans up
+	 * publications created by this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/* Remove primary_slot_name if it exists on primary */
+	drop_primary_replication_slot(dbinfo, primary_slot_name);
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(pg_resetwal_path, &opt);
+
+	/*
+	 * In dry run mode, the server is restarted with the provided command-line
+	 * options so validation can be applied in the target server. In order to
+	 * preserve the initial state of the server (running), start it without
+	 * the command-line options.
+	 */
+	if (dry_run)
+		start_standby_server(&opt, pg_ctl_path, NULL, false);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!dry_run && !opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..5a2b8e9a56
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,214 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Stop node C
+$node_c->teardown_node;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+ok( -d $node_s->data_dir . "/pg_createsubscriber_output.d",
+	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success"
+);
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.41.0.windows.3

v27-0005-Fix-error-for-windows.patchapplication/octet-stream; name=v27-0005-Fix-error-for-windows.patchDownload
From 753c7e8322a6eb61af97760550974581fb454ce2 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 8 Mar 2024 12:26:45 +0530
Subject: [PATCH v27 5/5] Fix error for windows

'sub_base_conninfo' variable should not have 'host' set to 'opt.socket_dir' in case of Windows.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 85bbc3ba8f..571e3e0a55 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1891,8 +1891,14 @@ main(int argc, char **argv)
 		exit(1);
 
 	pg_log_info("validating connection string on subscriber");
+
+#ifdef WIN32
+	sub_base_conninfo = psprintf("port=%s user=%s fallback_application_name=%s",
+								 opt.sub_port, opt.sub_username, progname);
+#else
 	sub_base_conninfo = psprintf("host=%s port=%s user=%s fallback_application_name=%s",
 								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
+#endif
 
 	if (opt.database_names.head == NULL)
 	{
-- 
2.41.0.windows.3

v27-0002-Use-latest-replication-slot-position-as-replicat.patchapplication/octet-stream; name=v27-0002-Use-latest-replication-slot-position-as-replicat.patchDownload
From 6dcabb444b56dd36c49d7add2ced19d78b520a52 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Wed, 6 Mar 2024 17:50:01 -0300
Subject: [PATCH v27 2/5] Use latest replication slot position as replication
 start point

Instead of using a temporary replication slot, use the latest
replication slot position (LSN).
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 60 ++++++++++-----------
 1 file changed, 29 insertions(+), 31 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 3ccf3348a2..d9797ed8f4 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -74,11 +74,12 @@ static void modify_subscriber_sysid(const char *pg_resetwal_path,
 									struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static void check_publisher(struct LogicalRepInfo *dbinfo);
-static void setup_publisher(struct LogicalRepInfo *dbinfo);
+static char *setup_publisher(struct LogicalRepInfo *dbinfo);
 static void check_subscriber(struct LogicalRepInfo *dbinfo);
 static void setup_subscriber(struct LogicalRepInfo *dbinfo,
 							 const char *consistent_lsn);
-static char *setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir);
+static void setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir,
+						   char *lsn);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  char *slotname);
 static char *create_logical_replication_slot(PGconn *conn,
@@ -574,11 +575,15 @@ modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOpt
 
 /*
  * Create the publications and replication slots in preparation for logical
- * replication.
+ * replication. Returns the LSN from latest replication slot. It will be the
+ * replication start point that is used to adjust the subscriptions (see
+ * set_replication_progress).
  */
-static void
+static char *
 setup_publisher(struct LogicalRepInfo *dbinfo)
 {
+	char	   *lsn = NULL;
+
 	for (int i = 0; i < num_dbs; i++)
 	{
 		PGconn	   *conn;
@@ -641,8 +646,10 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		dbinfo[i].subname = pg_strdup(replslotname);
 
 		/* Create replication slot on publisher */
-		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
-			dry_run)
+		if (lsn)
+			pg_free(lsn);
+		lsn = create_logical_replication_slot(conn, &dbinfo[i], false);
+		if (lsn != NULL || dry_run)
 			pg_log_info("create replication slot \"%s\" on publisher",
 						replslotname);
 		else
@@ -650,6 +657,8 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 
 		disconnect_database(conn, false);
 	}
+
+	return lsn;
 }
 
 /*
@@ -995,14 +1004,11 @@ setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
 }
 
 /*
- * Get a replication start location and write the required recovery parameters
- * into the configuration file. Returns the replication start location that is
- * used to adjust the subscriptions (see set_replication_progress).
+ * Write the required recovery parameters.
  */
-static char *
-setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
+static void
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir, char *lsn)
 {
-	char	   *consistent_lsn;
 	PGconn	   *conn;
 	PQExpBuffer recoveryconfcontents;
 
@@ -1016,15 +1022,12 @@ setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
 	/*
 	 * Write recovery parameters.
 	 *
-	 * Despite of the recovery parameters will be written to the subscriber,
-	 * use a publisher connection for the following recovery functions. The
-	 * connection is only used to check the current server version (physical
-	 * replica, same server version). The subscriber is not running yet. In
-	 * dry run mode, the recovery parameters *won't* be written. An invalid
-	 * LSN is used for printing purposes. Additional recovery parameters are
-	 * added here. It avoids unexpected behavior such as end of recovery as
-	 * soon as a consistent state is reached (recovery_target) and failure due
-	 * to multiple recovery targets (name, time, xid, LSN).
+	 * The subscriber is not running yet. In dry run mode, the recovery
+	 * parameters *won't* be written. An invalid LSN is used for printing
+	 * purposes. Additional recovery parameters are added here. It avoids
+	 * unexpected behavior such as end of recovery as soon as a consistent
+	 * state is reached (recovery_target) and failure due to multiple recovery
+	 * targets (name, time, xid, LSN).
 	 */
 	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
@@ -1048,14 +1051,12 @@ setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
 	else
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
-						  consistent_lsn);
+						  lsn);
 		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
 
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
-
-	return consistent_lsn;
 }
 
 /*
@@ -1988,13 +1989,10 @@ main(int argc, char **argv)
 	 * primary slot is in use. We could use an extra connection for it but it
 	 * doesn't seem worth.
 	 */
-	setup_publisher(dbinfo);
+	consistent_lsn = setup_publisher(dbinfo);
 
-	/*
-	 * Get the replication start point and write the required recovery
-	 * parameters into the configuration file.
-	 */
-	consistent_lsn = setup_recovery(dbinfo, opt.subscriber_dir);
+	/* Write the required recovery parameters */
+	setup_recovery(dbinfo, opt.subscriber_dir, consistent_lsn);
 
 	/*
 	 * Restart subscriber so the recovery parameters will take effect. Wait
@@ -2010,7 +2008,7 @@ main(int argc, char **argv)
 	/*
 	 * Create the subscription for each database on subscriber. It does not
 	 * enable it immediately because it needs to adjust the replication start
-	 * point to the LSN reported by setup_recovery().  It also cleans up
+	 * point to the LSN reported by setup_publisher().  It also cleans up
 	 * publications created by this tool and replication to the standby.
 	 */
 	setup_subscriber(dbinfo, consistent_lsn);
-- 
2.41.0.windows.3

v27-0003-port-replace-int-with-string.patchapplication/octet-stream; name=v27-0003-port-replace-int-with-string.patchDownload
From 1fd5fca4ea5337c89e564d0b434265909dba2f2b Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Wed, 6 Mar 2024 22:00:23 -0300
Subject: [PATCH v27 3/5] port: replace int with string

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index d9797ed8f4..85bbc3ba8f 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -28,7 +28,7 @@
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
 
-#define	DEFAULT_SUB_PORT	50432
+#define	DEFAULT_SUB_PORT	"50432"
 #define	BASE_OUTPUT_DIR		"pg_createsubscriber_output.d"
 
 /* Command-line options */
@@ -37,7 +37,7 @@ struct CreateSubscriberOptions
 	char	   *subscriber_dir; /* standby/subscriber data directory */
 	char	   *pub_conninfo_str;	/* publisher connection string */
 	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
-	unsigned short sub_port;	/* subscriber port number */
+	char	   *sub_port;		/* subscriber port number */
 	const char *sub_username;	/* subscriber username */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;			/* retain log file? */
@@ -200,7 +200,7 @@ usage(void)
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
 	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
-	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %d)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %s)\n"), DEFAULT_SUB_PORT);
 	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -r, --retain                        retain log file after success\n"));
 	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
@@ -1295,7 +1295,7 @@ start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_pat
 		 */
 		if (!dry_run)
 			appendPQExpBuffer(pg_ctl_cmd, " -l \"%s\"", logfile);
-		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %u%s\"",
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %s%s\"",
 						  opt->sub_port, socket_string);
 	}
 	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
@@ -1336,7 +1336,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 
 #define NUM_CONN_ATTEMPTS	5
 
-	pg_log_info("waiting the target server to reach the consistent state");
+	pg_log_info("waiting for the target server to reach the consistent state");
 
 	conn = connect_database(conninfo, true);
 
@@ -1743,7 +1743,8 @@ main(int argc, char **argv)
 	opt.subscriber_dir = NULL;
 	opt.pub_conninfo_str = NULL;
 	opt.socket_dir = NULL;
-	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_port = palloc(16);
+	strcpy(opt.sub_port, DEFAULT_SUB_PORT);
 	opt.sub_username = NULL;
 	opt.database_names = (SimpleStringList)
 	{
@@ -1789,8 +1790,8 @@ main(int argc, char **argv)
 				dry_run = true;
 				break;
 			case 'p':
-				if ((opt.sub_port = atoi(optarg)) <= 0)
-					pg_fatal("invalid subscriber port number");
+				pg_free(opt.sub_port);
+				opt.sub_port = pg_strdup(optarg);
 				break;
 			case 'P':
 				opt.pub_conninfo_str = pg_strdup(optarg);
@@ -1890,7 +1891,7 @@ main(int argc, char **argv)
 		exit(1);
 
 	pg_log_info("validating connection string on subscriber");
-	sub_base_conninfo = psprintf("host=%s port=%u user=%s fallback_application_name=%s",
+	sub_base_conninfo = psprintf("host=%s port=%s user=%s fallback_application_name=%s",
 								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
 
 	if (opt.database_names.head == NULL)
-- 
2.41.0.windows.3

#182vignesh C
vignesh21@gmail.com
In reply to: Shlok Kyal (#179)
Re: speed up a logical replica setup

On Thu, 7 Mar 2024 at 18:31, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

Hi,

Thanks for the feedback. I'm attaching v26 that addresses most of your comments
and some issues pointed by Vignesh [1].

I have created a top-up patch v27-0004. It contains additional test
cases for pg_createsubscriber.

Currently, two testcases (in v27-0004 patch) are failing. These
failures are related to running pg_createsubscriber on nodes in
cascade physical replication and are already reported in [1] and [2].
I think these cases should be fixed. Thoughts?

We can fix these issues, if we are not planning to fix any of them, we
can add documentation for the same.

The idea of this patch is to keep track of testcases, so that any
future patch does not break any scenario which has already been worked
on. These testcases can be used to test in our development process,
but which test should actually be committed, can be discussed later.
Thought?

Few comments for v27-0004-Add-additional-testcases.patch:
1) We could use command_fails_like to verify the reason of the error:
+# set max_replication_slots
+$node_p->append_conf('postgresql.conf', 'max_replication_slots = 3');
+$node_p->restart;
+command_fails(
+       [
+               'pg_createsubscriber', '--verbose',
+               '--dry-run', '--pgdata',
+               $node_s->data_dir, '--publisher-server',
+               $node_p->connstr('pg1'), '--socket-directory',
+               $node_s->host, '--subscriber-port',
+               $node_s->port, '--database',
+               'pg1', '--database',
+               'pg2',
+       ],
+       'max_replication_slots are less in number in publisher');
2) Add a comment saying what is being verified
+# set max_replication_slots
+$node_p->append_conf('postgresql.conf', 'max_replication_slots = 3');
+$node_p->restart;
+command_fails(
+       [
+               'pg_createsubscriber', '--verbose',
+               '--dry-run', '--pgdata',
+               $node_s->data_dir, '--publisher-server',
+               $node_p->connstr('pg1'), '--socket-directory',
+               $node_s->host, '--subscriber-port',
+               $node_s->port, '--database',
+               'pg1', '--database',
+               'pg2',
+       ],
+       'max_replication_slots are less in number in publisher');

3) We could rename this file something like
pg_create_subscriber_failure_cases or something better:
src/bin/pg_basebackup/t/041_tests.pl | 285 +++++++++++++++++++++++++++
1 file changed, 285 insertions(+)
create mode 100644 src/bin/pg_basebackup/t/041_tests.pl

diff --git a/src/bin/pg_basebackup/t/041_tests.pl
b/src/bin/pg_basebackup/t/041_tests.pl
new file mode 100644
index 0000000000..2889d60d54
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_tests.pl
@@ -0,0 +1,285 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
4) This success case is not required as this would have already been
covered in 040_pg_createsubscriber.pl:
+$node_p->append_conf('postgresql.conf', 'max_replication_slots = 4');
+$node_p->restart;
+command_ok(
+       [
+               'pg_createsubscriber', '--verbose',
+               '--dry-run', '--pgdata',
+               $node_s->data_dir, '--publisher-server',
+               $node_p->connstr('pg1'), '--socket-directory',
+               $node_s->host, '--subscriber-port',
+               $node_s->port, '--database',
+               'pg1', '--database',
+               'pg2',
+       ],
+       'max_replication_slots are accurate on publisher');

5) We could use command_fails_like to verify the reason of the error:
$node_s->append_conf('postgresql.conf', 'max_replication_slots = 1');
$node_s->restart;
command_fails(
[
'pg_createsubscriber', '--verbose',
'--dry-run', '--pgdata',
$node_s->data_dir, '--publisher-server',
$node_p->connstr('pg1'), '--socket-directory',
$node_s->host, '--subscriber-port',
$node_s->port, '--database',
'pg1', '--database',
'pg2',
],
'max_replication_slots are less in number in subscriber');

6) Add a comment saying what is being verified
$node_s->append_conf('postgresql.conf', 'max_replication_slots = 1');
$node_s->restart;
command_fails(
[
'pg_createsubscriber', '--verbose',
'--dry-run', '--pgdata',
$node_s->data_dir, '--publisher-server',
$node_p->connstr('pg1'), '--socket-directory',
$node_s->host, '--subscriber-port',
$node_s->port, '--database',
'pg1', '--database',
'pg2',
],
'max_replication_slots are less in number in subscriber');

7) This success case is not required as this would have already been
covered in 040_pg_createsubscriber.pl:
$node_s->append_conf('postgresql.conf', 'max_replication_slots = 2');
$node_s->restart;
command_ok(
[
'pg_createsubscriber', '--verbose',
'--dry-run', '--pgdata',
$node_s->data_dir, '--publisher-server',
$node_p->connstr('pg1'), '--socket-directory',
$node_s->host, '--subscriber-port',
$node_s->port, '--database',
'pg1', '--database',
'pg2',
],
'max_replication_slots are less in number in subscriber');

8) We could use command_fails_like to verify the reason of the error:
# set wal_level on publisher
$node_p->append_conf('postgresql.conf', 'wal_level = \'replica\'');
$node_p->restart;
command_fails(
[
'pg_createsubscriber', '--verbose',
'--dry-run', '--pgdata',
$node_s->data_dir, '--publisher-server',
$node_p->connstr('pg1'), '--socket-directory',
$node_s->host, '--subscriber-port',
$node_s->port, '--database',
'pg1', '--database',
'pg2',
],
'wal_level must be logical');

9) Add a comment saying what is being verified
# set wal_level on publisher
$node_p->append_conf('postgresql.conf', 'wal_level = \'replica\'');
$node_p->restart;
command_fails(
[
'pg_createsubscriber', '--verbose',
'--dry-run', '--pgdata',
$node_s->data_dir, '--publisher-server',
$node_p->connstr('pg1'), '--socket-directory',
$node_s->host, '--subscriber-port',
$node_s->port, '--database',
'pg1', '--database',
'pg2',
],
'wal_level must be logical');

10) This success case is not required as this would have already been
covered in 040_pg_createsubscriber.pl:
$node_p->append_conf('postgresql.conf', 'wal_level = \'logical\'');
$node_p->restart;
command_ok(
[
'pg_createsubscriber', '--verbose',
'--dry-run', '--pgdata',
$node_s->data_dir, '--publisher-server',
$node_p->connstr('pg1'), '--socket-directory',
$node_s->host, '--subscriber-port',
$node_s->port, '--database',
'pg1', '--database',
'pg2',
],
'wal_level is logical');

11) We could use command_fails_like to verify the reason of the error:
# set max_wal_senders on publisher
$node_p->append_conf('postgresql.conf', 'max_wal_senders = 2');
$node_p->restart;
command_fails(
[
'pg_createsubscriber', '--verbose',
'--dry-run', '--pgdata',
$node_s->data_dir, '--publisher-server',
$node_p->connstr('pg1'), '--socket-directory',
$node_s->host, '--subscriber-port',
$node_s->port, '--database',
'pg1', '--database',
'pg2',
],
'max_wal_senders is not sufficient');

12) Add a comment saying what is being verified:
# set max_wal_senders on publisher
$node_p->append_conf('postgresql.conf', 'max_wal_senders = 2');
$node_p->restart;
command_fails(
[
'pg_createsubscriber', '--verbose',
'--dry-run', '--pgdata',
$node_s->data_dir, '--publisher-server',
$node_p->connstr('pg1'), '--socket-directory',
$node_s->host, '--subscriber-port',
$node_s->port, '--database',
'pg1', '--database',
'pg2',
],
'max_wal_senders is not sufficient');

13) This success case is not required as this would have already been
covered in 040_pg_createsubscriber.pl:
$node_p->append_conf('postgresql.conf', 'max_wal_senders = 3');
$node_p->restart;
command_ok(
[
'pg_createsubscriber', '--verbose',
'--dry-run', '--pgdata',
$node_s->data_dir, '--publisher-server',
$node_p->connstr('pg1'), '--socket-directory',
$node_s->host, '--subscriber-port',
$node_s->port, '--database',
'pg1', '--database',
'pg2',
],
'max_wal_senders is sufficient');

14) This sleep is not required
# max_worker_processes on subscriber
$node_p->append_conf('postgresql.conf', 'max_worker_processes = 2');
$node_p->restart;
sleep 1;
$node_s->append_conf('postgresql.conf', 'max_worker_processes = 2');
$node_s->restart;

15) The similar comments exist in other places also, I'm not repeating them.

Regards,
Vignesh

#183Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Tomas Vondra (#180)
RE: speed up a logical replica setup

Dear Tomas, Euler,

Thanks for starting to read the thread! Since I'm not an original author,
I want to reply partially.

I decided to take a quick look on this patch today, to see how it works
and do some simple tests. I've only started to get familiar with it, so
I have only some comments / questions regarding usage, not on the code.
It's quite possible I didn't understand some finer points, or maybe it
was already discussed earlier in this very long thread, so please feel
free to push back or point me to the past discussion.

Also, some of this is rather opinionated, but considering I didn't see
this patch before, my opinions may easily be wrong ...

I felt your comments were quit valuable.

1) SGML docs

It seems the SGML docs are more about explaining how this works on the
inside, rather than how to use the tool. Maybe that's intentional, but
as someone who didn't work with pg_createsubscriber before I found it
confusing and not very helpful.

For example, the first half of the page is prerequisities+warning, and
sure those are useful details, but prerequisities are checked by the
tool (so I can't really miss this) and warnings go into a lot of details
about different places where things may go wrong. Sure, worth knowing
and including in the docs, but maybe not right at the beginning, before
I learn how to even run the tool?

Hmm, right. I considered below improvements. Tomas and Euler, how do you think?

* Adds more descriptions in "Description" section.
* Moves prerequisities+warning to "Notes" section.
* Adds "Usage" section which describes from a single node.

I'm not sure FOR ALL TABLES is a good idea. Or said differently, I'm
sure it won't work for a number of use cases. I know large databases
it's common to create "work tables" (not necessarily temporary) as part
of a batch job, but there's no need to replicate those tables.

Indeed, the documentation does not describe that all tables in the database
would be included in the publication.

I do understand that FOR ALL TABLES is the simplest approach, and for v1
it may be an acceptable limitation, but maybe it'd be good to also
support restricting which tables should be replicated (e.g. blacklist or
whitelist based on table/schema name?).

May not directly related, but we considered that accepting options was a next-step [1]/messages/by-id/TY3PR01MB9889CCBD4D9DAF8BD2F18541F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com.

Note: I now realize this might fall under the warning about DDL, which
says this:

Executing DDL commands on the source server while running
pg_createsubscriber is not recommended. If the target server has
already been converted to logical replica, the DDL commands must
not be replicated so an error would occur.

Yeah, they would not be replicated, but not lead ERROR.
So should we say like "Creating tables on the source server..."?

5) slot / publication / subscription name

I find it somewhat annoying it's not possible to specify names for
objects created by the tool - replication slots, publication and
subscriptions. If this is meant to be a replica running for a while,
after a while I'll have no idea what pg_createsubscriber_569853 or
pg_createsubscriber_459548_2348239 was meant for.

This is particularly annoying because renaming these objects later is
either not supported at all (e.g. for replication slots), or may be
quite difficult (e.g. publications).

I do realize there are challenges with custom names (say, if there are
multiple databases to replicate), but can't we support some simple
formatting with basic placeholders? So we could specify

--slot-name "myslot_%d_%p"

or something like that?

Not sure we can do in the first version, but looks nice. One concern is that I
cannot find applications which accepts escape strings like log_line_prefix.
(It may just because we do not have use-case.) Do you know examples?

BTW what will happen if we convert multiple standbys? Can't they all get
the same slot name (they all have the same database OID, and I'm not
sure how much entropy the PID has)?

I tested and the second try did not work. The primal reason was the name of publication
- pg_createsubscriber_%u (oid).
FYI - previously we can reuse same publications, but based on my comment [2]/messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com the
feature was removed. It might be too optimistic.

[1]: /messages/by-id/TY3PR01MB9889CCBD4D9DAF8BD2F18541F56F2@TY3PR01MB9889.jpnprd01.prod.outlook.com
[2]: /messages/by-id/TYCPR01MB12077756323B79042F29DDAEDF54C2@TYCPR01MB12077.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#184Andrey M. Borodin
x4mmm@yandex-team.ru
In reply to: Shlok Kyal (#181)
Re: speed up a logical replica setup

On 8 Mar 2024, at 12:03, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

<v27-0004-Add-additional-testcases.patch><v27-0001-pg_createsubscriber-creates-a-new-logical-replic.patch><v27-0005-Fix-error-for-windows.patch><v27-0002-Use-latest-replication-slot-position-as-replicat.patch><v27-0003-port-replace-int-with-string.patch>

I haven't digged into the thread, but recent version fails some CFbot's tests.

http://commitfest.cputube.org/euler-taveira.html
https://cirrus-ci.com/task/4833499115421696
==29928==ERROR: AddressSanitizer: heap-use-after-free on address 0x61a000001458 at pc 0x7f3b29fdedce bp 0x7ffe68fcf1c0 sp 0x7ffe68fcf1b8

Thanks!

Best regards, Andrey Borodin.

#185Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Hayato Kuroda (Fujitsu) (#183)
Re: speed up a logical replica setup

On 3/8/24 10:44, Hayato Kuroda (Fujitsu) wrote:

Dear Tomas, Euler,

Thanks for starting to read the thread! Since I'm not an original author,
I want to reply partially.

I decided to take a quick look on this patch today, to see how it works
and do some simple tests. I've only started to get familiar with it, so
I have only some comments / questions regarding usage, not on the code.
It's quite possible I didn't understand some finer points, or maybe it
was already discussed earlier in this very long thread, so please feel
free to push back or point me to the past discussion.

Also, some of this is rather opinionated, but considering I didn't see
this patch before, my opinions may easily be wrong ...

I felt your comments were quit valuable.

1) SGML docs

It seems the SGML docs are more about explaining how this works on the
inside, rather than how to use the tool. Maybe that's intentional, but
as someone who didn't work with pg_createsubscriber before I found it
confusing and not very helpful.

For example, the first half of the page is prerequisities+warning, and
sure those are useful details, but prerequisities are checked by the
tool (so I can't really miss this) and warnings go into a lot of details
about different places where things may go wrong. Sure, worth knowing
and including in the docs, but maybe not right at the beginning, before
I learn how to even run the tool?

Hmm, right. I considered below improvements. Tomas and Euler, how do you think?

* Adds more descriptions in "Description" section.
* Moves prerequisities+warning to "Notes" section.
* Adds "Usage" section which describes from a single node.

I'm not sure FOR ALL TABLES is a good idea. Or said differently, I'm
sure it won't work for a number of use cases. I know large databases
it's common to create "work tables" (not necessarily temporary) as part
of a batch job, but there's no need to replicate those tables.

Indeed, the documentation does not describe that all tables in the database
would be included in the publication.

I do understand that FOR ALL TABLES is the simplest approach, and for v1
it may be an acceptable limitation, but maybe it'd be good to also
support restricting which tables should be replicated (e.g. blacklist or
whitelist based on table/schema name?).

May not directly related, but we considered that accepting options was a next-step [1].

Note: I now realize this might fall under the warning about DDL, which
says this:

Executing DDL commands on the source server while running
pg_createsubscriber is not recommended. If the target server has
already been converted to logical replica, the DDL commands must
not be replicated so an error would occur.

Yeah, they would not be replicated, but not lead ERROR.
So should we say like "Creating tables on the source server..."?

Perhaps. Clarifying the docs would help, but it depends on the wording.
For example, I doubt this should talk about "creating tables" because
there are other DDL that (probably) could cause issues (like adding a
column to the table, or something like that).

5) slot / publication / subscription name

I find it somewhat annoying it's not possible to specify names for
objects created by the tool - replication slots, publication and
subscriptions. If this is meant to be a replica running for a while,
after a while I'll have no idea what pg_createsubscriber_569853 or
pg_createsubscriber_459548_2348239 was meant for.

This is particularly annoying because renaming these objects later is
either not supported at all (e.g. for replication slots), or may be
quite difficult (e.g. publications).

I do realize there are challenges with custom names (say, if there are
multiple databases to replicate), but can't we support some simple
formatting with basic placeholders? So we could specify

--slot-name "myslot_%d_%p"

or something like that?

Not sure we can do in the first version, but looks nice. One concern is that I
cannot find applications which accepts escape strings like log_line_prefix.
(It may just because we do not have use-case.) Do you know examples?

I can't think of a tool already doing that, but I think that's simply
because it was not needed. Why should we be concerned about this?

BTW what will happen if we convert multiple standbys? Can't they all get
the same slot name (they all have the same database OID, and I'm not
sure how much entropy the PID has)?

I tested and the second try did not work. The primal reason was the name of publication
- pg_createsubscriber_%u (oid).
FYI - previously we can reuse same publications, but based on my comment [2] the
feature was removed. It might be too optimistic.

OK. I could be convinced the other limitations are reasonable for v1 and
can be improved later, but this seems like something that needs fixing.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#186vignesh C
vignesh21@gmail.com
In reply to: Andrey M. Borodin (#184)
6 attachment(s)
Re: speed up a logical replica setup

On Fri, 8 Mar 2024 at 15:31, Andrey M. Borodin <x4mmm@yandex-team.ru> wrote:

On 8 Mar 2024, at 12:03, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

<v27-0004-Add-additional-testcases.patch><v27-0001-pg_createsubscriber-creates-a-new-logical-replic.patch><v27-0005-Fix-error-for-windows.patch><v27-0002-Use-latest-replication-slot-position-as-replicat.patch><v27-0003-port-replace-int-with-string.patch>

I haven't digged into the thread, but recent version fails some CFbot's tests.

http://commitfest.cputube.org/euler-taveira.html
https://cirrus-ci.com/task/4833499115421696
==29928==ERROR: AddressSanitizer: heap-use-after-free on address 0x61a000001458 at pc 0x7f3b29fdedce bp 0x7ffe68fcf1c0 sp 0x7ffe68fcf1b8

This is because of disconnect_database called twice in the error flow:
+       PQclear(res);
+
+       disconnect_database(conn, false);
+
+       if (max_repslots < num_dbs)
+       {
+               pg_log_error("subscriber requires %d replication
slots, but only %d remain",
+                                        num_dbs, max_repslots);
+               pg_log_error_hint("Consider increasing
max_replication_slots to at least %d.",
+                                                 num_dbs);
+               disconnect_database(conn, true);
+       }
+
+       if (max_lrworkers < num_dbs)
+       {
+               pg_log_error("subscriber requires %d logical
replication workers, but only %d remain",
+                                        num_dbs, max_lrworkers);
+               pg_log_error_hint("Consider increasing
max_logical_replication_workers to at least %d.",
+                                                 num_dbs);
+               disconnect_database(conn, true);
+       }

This is handled in the attached patch set. Apart from this there are a
couple of test failures which will be handled in the upcoming version.

Regards,
Vignesh

Attachments:

v27-0005-Fix-error-for-windows.patchtext/x-patch; charset=US-ASCII; name=v27-0005-Fix-error-for-windows.patchDownload
From 753c7e8322a6eb61af97760550974581fb454ce2 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 8 Mar 2024 12:26:45 +0530
Subject: [PATCH v27 5/5] Fix error for windows

'sub_base_conninfo' variable should not have 'host' set to 'opt.socket_dir' in case of Windows.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 85bbc3ba8f..571e3e0a55 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1891,8 +1891,14 @@ main(int argc, char **argv)
 		exit(1);
 
 	pg_log_info("validating connection string on subscriber");
+
+#ifdef WIN32
+	sub_base_conninfo = psprintf("port=%s user=%s fallback_application_name=%s",
+								 opt.sub_port, opt.sub_username, progname);
+#else
 	sub_base_conninfo = psprintf("host=%s port=%s user=%s fallback_application_name=%s",
 								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
+#endif
 
 	if (opt.database_names.head == NULL)
 	{
-- 
2.41.0.windows.3

v27-0004-Add-additional-testcases.patchtext/x-patch; charset=US-ASCII; name=v27-0004-Add-additional-testcases.patchDownload
From 24efeca0553c5a22f9c790e92543434ab2100d26 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 7 Mar 2024 18:00:29 +0530
Subject: [PATCH v27 4/4] Add additional testcases

Add additional testcases to test on pg_createsubscriber
---
 src/bin/pg_basebackup/t/041_tests.pl | 285 +++++++++++++++++++++++++++
 1 file changed, 285 insertions(+)
 create mode 100644 src/bin/pg_basebackup/t/041_tests.pl

diff --git a/src/bin/pg_basebackup/t/041_tests.pl b/src/bin/pg_basebackup/t/041_tests.pl
new file mode 100644
index 0000000000..2889d60d54
--- /dev/null
+++ b/src/bin/pg_basebackup/t/041_tests.pl
@@ -0,0 +1,285 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Set different parameters in postgresql.conf
+
+# set max_replication_slots
+$node_p->append_conf('postgresql.conf', 'max_replication_slots = 3');
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are less in number in publisher');
+
+$node_p->append_conf('postgresql.conf', 'max_replication_slots = 4');
+$node_p->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are accurate on publisher');
+
+$node_s->append_conf('postgresql.conf', 'max_replication_slots = 1');
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are less in number in subscriber');
+
+$node_s->append_conf('postgresql.conf', 'max_replication_slots = 2');
+$node_s->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_replication_slots are less in number in subscriber');
+
+# set wal_level on publisher
+$node_p->append_conf('postgresql.conf', 'wal_level = \'replica\'');
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'wal_level must be logical');
+
+$node_p->append_conf('postgresql.conf', 'wal_level = \'logical\'');
+$node_p->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'wal_level is logical');
+
+# set max_wal_senders on publisher
+$node_p->append_conf('postgresql.conf', 'max_wal_senders = 2');
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_wal_senders is not sufficient');
+
+$node_p->append_conf('postgresql.conf', 'max_wal_senders = 3');
+$node_p->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_wal_senders is sufficient');
+
+# set max_logical_replication_workers on subscriber
+$node_s->append_conf('postgresql.conf',
+	'max_logical_replication_workers = 1');
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_logical_replication_workers is not sufficient');
+
+$node_s->append_conf('postgresql.conf',
+	'max_logical_replication_workers = 2');
+$node_s->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_logical_replication_workers is sufficient');
+
+# max_worker_processes on subscriber
+$node_p->append_conf('postgresql.conf', 'max_worker_processes = 2');
+$node_p->restart;
+sleep 1;
+$node_s->append_conf('postgresql.conf', 'max_worker_processes = 2');
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_worker_processes is not sufficient');
+
+$node_p->append_conf('postgresql.conf', 'max_worker_processes = 3');
+$node_p->restart;
+sleep 1;
+$node_s->append_conf('postgresql.conf', 'max_worker_processes = 3');
+$node_s->restart;
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2',
+	],
+	'max_worker_processes is sufficient');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Cascade Streaming Replication
+
+# Run pg_createsubscriber on node S (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_s->host, '--subscriber-port',
+		$node_s->port, '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'standby server is primary to other node');
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+# Specify P as the publisher and C as standby
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'), '--socket-directory',
+		$node_c->host, '--subscriber-port',
+		$node_c->port, '--database',
+		'pg1', '--database',
+		'pg2'
+	],
+	'standby server is not direct standby of publisher');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_c->teardown_node;
+
+done_testing();
-- 
2.34.1

v27-0002-Use-latest-replication-slot-position-as-replicat.patchtext/x-patch; charset=US-ASCII; name=v27-0002-Use-latest-replication-slot-position-as-replicat.patchDownload
From 444cef5aa420e421b6ab2621709fc97e8506716d Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Wed, 6 Mar 2024 17:50:01 -0300
Subject: [PATCH v27 2/4] Use latest replication slot position as replication
 start point

Instead of using a temporary replication slot, use the latest
replication slot position (LSN).
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 60 ++++++++++-----------
 1 file changed, 29 insertions(+), 31 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 3ccf3348a2..d9797ed8f4 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -74,11 +74,12 @@ static void modify_subscriber_sysid(const char *pg_resetwal_path,
 									struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static void check_publisher(struct LogicalRepInfo *dbinfo);
-static void setup_publisher(struct LogicalRepInfo *dbinfo);
+static char *setup_publisher(struct LogicalRepInfo *dbinfo);
 static void check_subscriber(struct LogicalRepInfo *dbinfo);
 static void setup_subscriber(struct LogicalRepInfo *dbinfo,
 							 const char *consistent_lsn);
-static char *setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir);
+static void setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir,
+						   char *lsn);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  char *slotname);
 static char *create_logical_replication_slot(PGconn *conn,
@@ -574,11 +575,15 @@ modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOpt
 
 /*
  * Create the publications and replication slots in preparation for logical
- * replication.
+ * replication. Returns the LSN from latest replication slot. It will be the
+ * replication start point that is used to adjust the subscriptions (see
+ * set_replication_progress).
  */
-static void
+static char *
 setup_publisher(struct LogicalRepInfo *dbinfo)
 {
+	char	   *lsn = NULL;
+
 	for (int i = 0; i < num_dbs; i++)
 	{
 		PGconn	   *conn;
@@ -641,8 +646,10 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		dbinfo[i].subname = pg_strdup(replslotname);
 
 		/* Create replication slot on publisher */
-		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
-			dry_run)
+		if (lsn)
+			pg_free(lsn);
+		lsn = create_logical_replication_slot(conn, &dbinfo[i], false);
+		if (lsn != NULL || dry_run)
 			pg_log_info("create replication slot \"%s\" on publisher",
 						replslotname);
 		else
@@ -650,6 +657,8 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 
 		disconnect_database(conn, false);
 	}
+
+	return lsn;
 }
 
 /*
@@ -995,14 +1004,11 @@ setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
 }
 
 /*
- * Get a replication start location and write the required recovery parameters
- * into the configuration file. Returns the replication start location that is
- * used to adjust the subscriptions (see set_replication_progress).
+ * Write the required recovery parameters.
  */
-static char *
-setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
+static void
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir, char *lsn)
 {
-	char	   *consistent_lsn;
 	PGconn	   *conn;
 	PQExpBuffer recoveryconfcontents;
 
@@ -1016,15 +1022,12 @@ setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
 	/*
 	 * Write recovery parameters.
 	 *
-	 * Despite of the recovery parameters will be written to the subscriber,
-	 * use a publisher connection for the following recovery functions. The
-	 * connection is only used to check the current server version (physical
-	 * replica, same server version). The subscriber is not running yet. In
-	 * dry run mode, the recovery parameters *won't* be written. An invalid
-	 * LSN is used for printing purposes. Additional recovery parameters are
-	 * added here. It avoids unexpected behavior such as end of recovery as
-	 * soon as a consistent state is reached (recovery_target) and failure due
-	 * to multiple recovery targets (name, time, xid, LSN).
+	 * The subscriber is not running yet. In dry run mode, the recovery
+	 * parameters *won't* be written. An invalid LSN is used for printing
+	 * purposes. Additional recovery parameters are added here. It avoids
+	 * unexpected behavior such as end of recovery as soon as a consistent
+	 * state is reached (recovery_target) and failure due to multiple recovery
+	 * targets (name, time, xid, LSN).
 	 */
 	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
@@ -1048,14 +1051,12 @@ setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
 	else
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
-						  consistent_lsn);
+						  lsn);
 		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
 
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
-
-	return consistent_lsn;
 }
 
 /*
@@ -1988,13 +1989,10 @@ main(int argc, char **argv)
 	 * primary slot is in use. We could use an extra connection for it but it
 	 * doesn't seem worth.
 	 */
-	setup_publisher(dbinfo);
+	consistent_lsn = setup_publisher(dbinfo);
 
-	/*
-	 * Get the replication start point and write the required recovery
-	 * parameters into the configuration file.
-	 */
-	consistent_lsn = setup_recovery(dbinfo, opt.subscriber_dir);
+	/* Write the required recovery parameters */
+	setup_recovery(dbinfo, opt.subscriber_dir, consistent_lsn);
 
 	/*
 	 * Restart subscriber so the recovery parameters will take effect. Wait
@@ -2010,7 +2008,7 @@ main(int argc, char **argv)
 	/*
 	 * Create the subscription for each database on subscriber. It does not
 	 * enable it immediately because it needs to adjust the replication start
-	 * point to the LSN reported by setup_recovery().  It also cleans up
+	 * point to the LSN reported by setup_publisher().  It also cleans up
 	 * publications created by this tool and replication to the standby.
 	 */
 	setup_subscriber(dbinfo, consistent_lsn);
-- 
2.34.1

v27-0001-pg_createsubscriber-creates-a-new-logical-replic.patchtext/x-patch; charset=US-ASCII; name=v27-0001-pg_createsubscriber-creates-a-new-logical-replic.patchDownload
From 4bcf2b80da120832d4e1d5217f7ee0196517d904 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v27 1/4] pg_createsubscriber: creates a new logical replica
 from a standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber).

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
for each specified database. After that, it stops the target server. One
temporary replication slot is created to get the replication start
point. It is used as (a) a stopping point for the recovery process and
(b) a starting point for the subscriptions. Write recovery parameters
into the target data directory and start the target server. Wait until
the target server is promoted. Create one subscription per specified
database (using replication slot and publication created in a previous
step) on the target server. Set the replication progress to the
replication start point for each subscription. Enable the subscription
for each specified database on the target server. And finally, change
the system identifier on the target server.

Depending on your workload and database size, creating a logical replica
couldn't be an option due to resource constraints (WAL backlog should be
available until all table data is synchronized). The initial data copy
and the replication progress tends to be faster on a physical replica.
The purpose of this tool is to speed up a logical replica setup.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  522 +++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2049 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  214 ++
 8 files changed, 2815 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..1664101075
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,522 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. It must be run at the target server.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because it speeds up the logical replication setup. For smaller
+   databases, <link linkend="logical-replication">initial data synchronization</link>
+   is recommended.
+  </para>
+
+  <para>
+   There are some prerequisites for <application>pg_createsubscriber</application>
+   to convert the target server into a logical replica. If these are not met an
+   error will be reported.
+  </para>
+
+  <itemizedlist id="app-pg-createsubscriber-description-prerequisites">
+   <listitem>
+    <para>
+     The source and target servers must have the same major version as the
+     <application>pg_createsubscriber</application>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The given target data directory must have the same system identifier than
+     the source data directory. If a standby server is running on the target
+     data directory or it is a base backup from the source data directory,
+     system identifiers are the same.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must be used as a physical standby.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The given database user for the target data directory must have privileges
+     for creating <link linkend="sql-createsubscription">subscriptions</link>
+     and using <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     and
+     <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+     configured to a value greater than or equal to the number of specified databases.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must have
+     <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+     configured to a value greater than the number of specified databases.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The target server must accept local connections.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must accept connections from the target server.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must not be in recovery. Publications cannot be created
+     in a read-only cluster.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+     <literal>logical</literal>.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+     configured to a value greater than the number of specified databases plus
+     existing replication slots.
+    </para>
+   </listitem>
+
+   <listitem>
+    <para>
+     The source server must have
+     <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+     configured to a value greater than or equal to the number of specified
+     databases and existing <literal>walsender</literal> processes.
+    </para>
+   </listitem>
+  </itemizedlist>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    then the data directory is likely not in a state that can be recovered. It
+    is true if the target server was promoted. In such a case, creating a new
+    standby server is recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during the transformation steps.
+    Hence, connections to target server might fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Executing DDL commands on the source server while running
+    <application>pg_createsubscriber</application> is not recommended. If the
+    target server has already been converted to logical replica, the DDL
+    commands must not be replicated so an error would occur.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    should be removed. The removal might fail if the target server cannot
+    connect to the source server. In such a case, a warning message will inform
+    the objects left.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which WAL files from the source server might be used by the target server.
+    If the target server has a standby, replication will break and a fresh
+    standby should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connnections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-r</option></term>
+      <term><option>--retain</option></term>
+      <listitem>
+       <para>
+        Retain log file even after successful completion.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is already running, stop it because some parameters can only
+     be set at server start.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. All
+     <link linkend="app-pg-createsubscriber-description-prerequisites">prerequisites</link>
+     are checked. If any of them are not met, <application>pg_createsubscriber</application>
+     will terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u</literal></quote> (parameter:
+     database <parameter>oid</parameter>). Each replication slot has the
+     following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a temporary replication slot to get a consistent start location.
+     This replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
+     (parameter: PID <parameter>int</parameter>). The LSN returned by
+     <link linkend="pg-create-logical-replication-slot"><function>pg_create_logical_replication_slot()</function></link>
+     is used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
+     parameter and by the subscriptions as a replication start point. It
+     guarantees that no transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies a LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are also added so it avoids unexpected behavior during the recovery
+     process such as end of the recovery as soon as a consistent state is
+     reached (WAL should be applied until the consistent start location) and
+     multiple recovery targets that can cause a failure. This step finishes
+     once the server ends standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     The subscription has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>). The
+     replication slot name is identical to the subscription name. It does not
+     copy existing data from the source server. It does not create a
+     replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the consistent LSN
+     but replication origin name contains the subscription oid in its name.
+     Hence, the subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the consistent start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the consistent start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the consistent start point. This is the exact LSN to be used
+     as a initial location for each subscription. The replication origin name
+     is obtained since the subscription was created. The replication origin
+     name and the consistent start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the consistent start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..e9a920dbcd 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..3ccf3348a2
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2049 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "common/username.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	50432
+#define	BASE_OUTPUT_DIR		"pg_createsubscriber_output.d"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	unsigned short sub_port;	/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	bool		retain;			/* retain log file? */
+	int			recovery_timeout;	/* stop recovery after this time */
+}			CreateSubscriberOptions;
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name / replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+}			LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path,
+									struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static void check_publisher(struct LogicalRepInfo *dbinfo);
+static void setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir);
+static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
+										  char *slotname);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static char *setup_server_logfile(const char *datadir);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(struct CreateSubscriberOptions *opt,
+								 const char *pg_ctl_path, const char *logfile,
+								 bool with_options);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+								  struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static bool recovery_ended = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("pg_createsubscriber failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.");
+		pg_log_warning_hint("You must recreate the physical replica before continuing.");
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			PGconn	   *conn;
+
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].subname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %d)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -r, --retain                        retain log file after success\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	int			i = 0;
+
+	dbinfo = (struct LogicalRepInfo *) pg_malloc(num_dbs * sizeof(struct LogicalRepInfo));
+
+	for (SimpleStringListCell *cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): connection string: %s", i, dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): connection string: %s", i, dbinfo[i].subconninfo);
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static void
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+
+		/* Remember database OID */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ */
+static void
+check_publisher(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted +
+	 *                            one temporary logical replication slot
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+		pg_fatal("publisher requires wal_level >= logical");
+
+	/* One additional temporary logical replication slot */
+	if (max_repslots - cur_repslots < num_dbs + 1)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs + 1, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs + 1);
+		exit(1);
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		exit(1);
+	}
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ */
+static void
+check_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("The target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		disconnect_database(conn, true);
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		disconnect_database(conn, true);
+	}
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		disconnect_database(conn, true);
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		disconnect_database(conn, true);
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		disconnect_database(conn, true);
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		disconnect_database(conn, true);
+	}
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * repliation setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Get a replication start location and write the required recovery parameters
+ * into the configuration file. Returns the replication start location that is
+ * used to adjust the subscriptions (see set_replication_progress).
+ */
+static char *
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
+{
+	char	   *consistent_lsn;
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	/*
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection. The primary_conninfo is generated using the
+	 * connection settings.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	return consistent_lsn;
+}
+
+/*
+ * Drop physical replication slot on primary if the standby was using it. After
+ * the transformation, it has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, char *slotname)
+{
+	PGconn	   *conn;
+
+	/* Replication slot does not exist, do nothing */
+	if (!primary_slot_name)
+		return;
+
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn != NULL)
+	{
+		drop_replication_slot(conn, &dbinfo[0], slotname);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop replication slot \"%s\" on primary",
+					   slotname);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/* This temporary replication slot is only used for catchup purposes */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a directory to store any log information. Adjust the permissions.
+ * Return a file name (full path) that's used by the standby server when it
+ * starts the transformation.
+ * In dry run mode, doesn't create the BASE_OUTPUT_DIR directory, instead
+ * returns the full log file path.
+ */
+static char *
+setup_server_logfile(const char *datadir)
+{
+	char		timebuf[128];
+	struct timeval time;
+	time_t		tt;
+	int			len;
+	char	   *base_dir;
+	char	   *filename;
+
+	base_dir = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(base_dir, MAXPGPATH, "%s/%s", datadir, BASE_OUTPUT_DIR);
+	if (len >= MAXPGPATH)
+		pg_fatal("directory path for subscriber is too long");
+
+	if (!GetDataDirectoryCreatePerm(datadir))
+		pg_fatal("could not read permissions of directory \"%s\": %m",
+				 datadir);
+
+	if (!dry_run && mkdir(base_dir, pg_dir_create_mode) < 0 && errno != EEXIST)
+		pg_fatal("could not create directory \"%s\": %m", base_dir);
+
+	/* Append timestamp with ISO 8601 format */
+	gettimeofday(&time, NULL);
+	tt = (time_t) time.tv_sec;
+	strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%S", localtime(&tt));
+	snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf),
+			 ".%03d", (int) (time.tv_usec / 1000));
+
+	filename = (char *) pg_malloc0(MAXPGPATH);
+	len = snprintf(filename, MAXPGPATH, "%s/server_start_%s.log", base_dir, timebuf);
+	pg_log_debug("log file is: %s", filename);
+	if (len >= MAXPGPATH)
+		pg_fatal("log file path is too long");
+
+	return filename;
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_path,
+					 const char *logfile, bool with_options)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	char		socket_string[MAXPGPATH + 200];
+	int			rc;
+
+	socket_string[0] = '\0';
+
+#if !defined(WIN32)
+
+	/*
+	 * An empty listen_addresses list means the server does not listen on any
+	 * IP interfaces; only Unix-domain sockets can be used to connect to the
+	 * server. Prevent external connections to minimize the chance of failure.
+	 */
+	strcat(socket_string,
+		   " -c listen_addresses='' -c unix_socket_permissions=0700");
+
+	if (opt->socket_dir)
+		snprintf(socket_string + strlen(socket_string),
+				 sizeof(socket_string) - strlen(socket_string),
+				 " -c unix_socket_directories='%s'",
+				 opt->socket_dir);
+#endif
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, opt->subscriber_dir);
+	if (with_options)
+	{
+		/*
+		 * Don't include the log file in dry run mode because the directory
+		 * that contains it was not created in setup_server_logfile().
+		 */
+		if (!dry_run)
+			appendPQExpBuffer(pg_ctl_cmd, " -l \"%s\"", logfile);
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %u%s\"",
+						  opt->sub_port, socket_string);
+	}
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+					  struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	5
+
+	pg_log_info("waiting the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"retain", no_argument, NULL, 'r'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_ctl_path = NULL;
+	char	   *pg_resetwal_path = NULL;
+
+	char	   *server_start_log;
+
+	char	   *pub_base_conninfo;
+	char	   *sub_base_conninfo;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	char	   *consistent_lsn;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.retain = false;
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:np:P:rs:t:U:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(opt.subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				if ((opt.sub_port = atoi(optarg)) <= 0)
+					pg_fatal("invalid subscriber port number");
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 'r':
+				opt.retain = true;
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				canonicalize_path(opt.socket_dir);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/* Any non-option arguments? */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* Required arguments */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* If socket directory is not provided, use the current directory */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 * If subscriber username is not provided, check if the environment
+	 * variable sets it. If not, obtain the operating system name of the user
+	 * running it.
+	 */
+	if (opt.sub_username == NULL)
+	{
+		if (getenv("PGUSER"))
+		{
+			opt.sub_username = getenv("PGUSER");
+		}
+		else
+		{
+			char	   *errstr = NULL;
+
+			opt.sub_username = get_user_name(&errstr);
+			if (errstr)
+				pg_fatal("%s", errstr);
+		}
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = psprintf("host=%s port=%u user=%s fallback_application_name=%s",
+								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(opt.subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Create the output directory to store any data generated by this tool */
+	server_start_log = setup_server_logfile(opt.subscriber_dir);
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * If the standby server is running, stop it. Some parameters (that can
+	 * only be set at server start) are informed by command-line options.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, pg_ctl_path, server_start_log, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	setup_publisher(dbinfo);
+
+	/*
+	 * Get the replication start point and write the required recovery
+	 * parameters into the configuration file.
+	 */
+	consistent_lsn = setup_recovery(dbinfo, opt.subscriber_dir);
+
+	/*
+	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("stopping and starting the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	start_standby_server(&opt, pg_ctl_path, server_start_log, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the replication start
+	 * point to the LSN reported by setup_recovery().  It also cleans up
+	 * publications created by this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/* Remove primary_slot_name if it exists on primary */
+	drop_primary_replication_slot(dbinfo, primary_slot_name);
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(pg_resetwal_path, &opt);
+
+	/*
+	 * In dry run mode, the server is restarted with the provided command-line
+	 * options so validation can be applied in the target server. In order to
+	 * preserve the initial state of the server (running), start it without
+	 * the command-line options.
+	 */
+	if (dry_run)
+		start_standby_server(&opt, pg_ctl_path, NULL, false);
+
+	/*
+	 * The log file is kept if retain option is specified or this tool does
+	 * not run successfully. Otherwise, log file is removed.
+	 */
+	if (!dry_run && !opt.retain)
+		unlink(server_start_log);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..5a2b8e9a56
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,214 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Stop node C
+$node_c->teardown_node;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+ok( -d $node_s->data_dir . "/pg_createsubscriber_output.d",
+	"pg_createsubscriber_output.d/ removed after pg_createsubscriber success"
+);
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.34.1

v27-0003-port-replace-int-with-string.patchtext/x-patch; charset=US-ASCII; name=v27-0003-port-replace-int-with-string.patchDownload
From 7ed803615fdbebf2054b5cbd783fb667465e7998 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Wed, 6 Mar 2024 22:00:23 -0300
Subject: [PATCH v27 3/4] port: replace int with string

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index d9797ed8f4..85bbc3ba8f 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -28,7 +28,7 @@
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
 
-#define	DEFAULT_SUB_PORT	50432
+#define	DEFAULT_SUB_PORT	"50432"
 #define	BASE_OUTPUT_DIR		"pg_createsubscriber_output.d"
 
 /* Command-line options */
@@ -37,7 +37,7 @@ struct CreateSubscriberOptions
 	char	   *subscriber_dir; /* standby/subscriber data directory */
 	char	   *pub_conninfo_str;	/* publisher connection string */
 	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
-	unsigned short sub_port;	/* subscriber port number */
+	char	   *sub_port;		/* subscriber port number */
 	const char *sub_username;	/* subscriber username */
 	SimpleStringList database_names;	/* list of database names */
 	bool		retain;			/* retain log file? */
@@ -200,7 +200,7 @@ usage(void)
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
 	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
-	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %d)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %s)\n"), DEFAULT_SUB_PORT);
 	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -r, --retain                        retain log file after success\n"));
 	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
@@ -1295,7 +1295,7 @@ start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_pat
 		 */
 		if (!dry_run)
 			appendPQExpBuffer(pg_ctl_cmd, " -l \"%s\"", logfile);
-		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %u%s\"",
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %s%s\"",
 						  opt->sub_port, socket_string);
 	}
 	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
@@ -1336,7 +1336,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 
 #define NUM_CONN_ATTEMPTS	5
 
-	pg_log_info("waiting the target server to reach the consistent state");
+	pg_log_info("waiting for the target server to reach the consistent state");
 
 	conn = connect_database(conninfo, true);
 
@@ -1743,7 +1743,8 @@ main(int argc, char **argv)
 	opt.subscriber_dir = NULL;
 	opt.pub_conninfo_str = NULL;
 	opt.socket_dir = NULL;
-	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_port = palloc(16);
+	strcpy(opt.sub_port, DEFAULT_SUB_PORT);
 	opt.sub_username = NULL;
 	opt.database_names = (SimpleStringList)
 	{
@@ -1789,8 +1790,8 @@ main(int argc, char **argv)
 				dry_run = true;
 				break;
 			case 'p':
-				if ((opt.sub_port = atoi(optarg)) <= 0)
-					pg_fatal("invalid subscriber port number");
+				pg_free(opt.sub_port);
+				opt.sub_port = pg_strdup(optarg);
 				break;
 			case 'P':
 				opt.pub_conninfo_str = pg_strdup(optarg);
@@ -1890,7 +1891,7 @@ main(int argc, char **argv)
 		exit(1);
 
 	pg_log_info("validating connection string on subscriber");
-	sub_base_conninfo = psprintf("host=%s port=%u user=%s fallback_application_name=%s",
+	sub_base_conninfo = psprintf("host=%s port=%s user=%s fallback_application_name=%s",
 								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
 
 	if (opt.database_names.head == NULL)
-- 
2.34.1

v27-0006-Fix-double-free-issue.patchtext/x-patch; charset=US-ASCII; name=v27-0006-Fix-double-free-issue.patchDownload
From 9a14a954fd82d9e31e2b3005c517b6074b2ca437 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Mon, 11 Mar 2024 09:15:13 +0530
Subject: [PATCH v27 6/6] Fix double free issue.

While the subscriber configurations was being checked, in few error
cases, disconnect_database was being called again which was not requied.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 571e3e0a55..41b548304b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -947,7 +947,7 @@ check_subscriber(struct LogicalRepInfo *dbinfo)
 					 num_dbs, max_repslots);
 		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
 						  num_dbs);
-		disconnect_database(conn, true);
+		exit(1);
 	}
 
 	if (max_lrworkers < num_dbs)
@@ -956,7 +956,7 @@ check_subscriber(struct LogicalRepInfo *dbinfo)
 					 num_dbs, max_lrworkers);
 		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
 						  num_dbs);
-		disconnect_database(conn, true);
+		exit(1);
 	}
 
 	if (max_wprocs < num_dbs + 1)
@@ -965,7 +965,7 @@ check_subscriber(struct LogicalRepInfo *dbinfo)
 					 num_dbs + 1, max_wprocs);
 		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
 						  num_dbs + 1);
-		disconnect_database(conn, true);
+		exit(1);
 	}
 }
 
-- 
2.34.1

#187vignesh C
vignesh21@gmail.com
In reply to: Tomas Vondra (#185)
Re: speed up a logical replica setup

On Sat, 9 Mar 2024 at 00:56, Tomas Vondra <tomas.vondra@enterprisedb.com> wrote:

On 3/8/24 10:44, Hayato Kuroda (Fujitsu) wrote:

Dear Tomas, Euler,

Thanks for starting to read the thread! Since I'm not an original author,
I want to reply partially.

I decided to take a quick look on this patch today, to see how it works
and do some simple tests. I've only started to get familiar with it, so
I have only some comments / questions regarding usage, not on the code.
It's quite possible I didn't understand some finer points, or maybe it
was already discussed earlier in this very long thread, so please feel
free to push back or point me to the past discussion.

Also, some of this is rather opinionated, but considering I didn't see
this patch before, my opinions may easily be wrong ...

I felt your comments were quit valuable.

1) SGML docs

It seems the SGML docs are more about explaining how this works on the
inside, rather than how to use the tool. Maybe that's intentional, but
as someone who didn't work with pg_createsubscriber before I found it
confusing and not very helpful.

For example, the first half of the page is prerequisities+warning, and
sure those are useful details, but prerequisities are checked by the
tool (so I can't really miss this) and warnings go into a lot of details
about different places where things may go wrong. Sure, worth knowing
and including in the docs, but maybe not right at the beginning, before
I learn how to even run the tool?

Hmm, right. I considered below improvements. Tomas and Euler, how do you think?

* Adds more descriptions in "Description" section.
* Moves prerequisities+warning to "Notes" section.
* Adds "Usage" section which describes from a single node.

I'm not sure FOR ALL TABLES is a good idea. Or said differently, I'm
sure it won't work for a number of use cases. I know large databases
it's common to create "work tables" (not necessarily temporary) as part
of a batch job, but there's no need to replicate those tables.

Indeed, the documentation does not describe that all tables in the database
would be included in the publication.

I do understand that FOR ALL TABLES is the simplest approach, and for v1
it may be an acceptable limitation, but maybe it'd be good to also
support restricting which tables should be replicated (e.g. blacklist or
whitelist based on table/schema name?).

May not directly related, but we considered that accepting options was a next-step [1].

Note: I now realize this might fall under the warning about DDL, which
says this:

Executing DDL commands on the source server while running
pg_createsubscriber is not recommended. If the target server has
already been converted to logical replica, the DDL commands must
not be replicated so an error would occur.

Yeah, they would not be replicated, but not lead ERROR.
So should we say like "Creating tables on the source server..."?

Perhaps. Clarifying the docs would help, but it depends on the wording.
For example, I doubt this should talk about "creating tables" because
there are other DDL that (probably) could cause issues (like adding a
column to the table, or something like that).

5) slot / publication / subscription name

I find it somewhat annoying it's not possible to specify names for
objects created by the tool - replication slots, publication and
subscriptions. If this is meant to be a replica running for a while,
after a while I'll have no idea what pg_createsubscriber_569853 or
pg_createsubscriber_459548_2348239 was meant for.

This is particularly annoying because renaming these objects later is
either not supported at all (e.g. for replication slots), or may be
quite difficult (e.g. publications).

I do realize there are challenges with custom names (say, if there are
multiple databases to replicate), but can't we support some simple
formatting with basic placeholders? So we could specify

--slot-name "myslot_%d_%p"

or something like that?

Not sure we can do in the first version, but looks nice. One concern is that I
cannot find applications which accepts escape strings like log_line_prefix.
(It may just because we do not have use-case.) Do you know examples?

I can't think of a tool already doing that, but I think that's simply
because it was not needed. Why should we be concerned about this?

BTW what will happen if we convert multiple standbys? Can't they all get
the same slot name (they all have the same database OID, and I'm not
sure how much entropy the PID has)?

I tested and the second try did not work. The primal reason was the name of publication
- pg_createsubscriber_%u (oid).
FYI - previously we can reuse same publications, but based on my comment [2] the
feature was removed. It might be too optimistic.

OK. I could be convinced the other limitations are reasonable for v1 and
can be improved later, but this seems like something that needs fixing.

+1 to handle this.
Currently, a) Publication name = pg_createsubscriber_%u, where %u is
database oid, b) Replication slot name =  pg_createsubscriber_%u_%d,
Where %u is database oid and %d is the pid and c) Subscription name =
pg_createsubscriber_%u_%d, Where %u is database oid and %d is the pid
How about we have a non mandatory option like
--prefix_object_name=mysetup1, which will create a) Publication name =
mysetup1_pg_createsubscriber_%u, and b) Replication slot name =
mysetup1_pg_createsubscriber_%u (here pid is also removed) c)
Subscription name = mysetup1_pg_createsubscriber_%u (here pid is also
removed).

In the default case where the user does not specify
--prefix_object_name the object names will be created without any
prefix names.

Regards,
Vignesh

#188Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: vignesh C (#187)
RE: speed up a logical replica setup

Dear Vignesh,

Thanks for updating the patch, but cfbot still got angry [1]http://cfbot.cputube.org/highlights/all.html#4637.
Note that two containers (autoconf and meson) failed at different place,
so I think it is intentional one. It seems that there may be a bug related with 32-bit build.

We should see and fix as soon as possible.

[1]: http://cfbot.cputube.org/highlights/all.html#4637

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#189Amit Kapila
amit.kapila16@gmail.com
In reply to: Tomas Vondra (#180)
Re: speed up a logical replica setup

On Thu, Mar 7, 2024 at 10:44 PM Tomas Vondra
<tomas.vondra@enterprisedb.com> wrote:

4) Is FOR ALL TABLES a good idea?

I'm not sure FOR ALL TABLES is a good idea. Or said differently, I'm
sure it won't work for a number of use cases. I know large databases
it's common to create "work tables" (not necessarily temporary) as part
of a batch job, but there's no need to replicate those tables.

AFAIK that'd break this FOR ALL TABLES publication, because the tables
will qualify for replication, but won't be present on the subscriber. Or
did I miss something?

As the subscriber is created from standby, all the tables should be
present at least initially during and after creating the subscriber.
Users are later free to modify the publications/subscriptions.

I do understand that FOR ALL TABLES is the simplest approach, and for v1
it may be an acceptable limitation, but maybe it'd be good to also
support restricting which tables should be replicated (e.g. blacklist or
whitelist based on table/schema name?).

This would be useful, but OTOH could also be enhanced in a later
version unless we think it is a must for the first version.

--
With Regards,
Amit Kapila.

#190Amit Kapila
amit.kapila16@gmail.com
In reply to: vignesh C (#187)
Re: speed up a logical replica setup

On Mon, Mar 11, 2024 at 9:42 AM vignesh C <vignesh21@gmail.com> wrote:

On Sat, 9 Mar 2024 at 00:56, Tomas Vondra <tomas.vondra@enterprisedb.com> wrote:

5) slot / publication / subscription name

I find it somewhat annoying it's not possible to specify names for
objects created by the tool - replication slots, publication and
subscriptions. If this is meant to be a replica running for a while,
after a while I'll have no idea what pg_createsubscriber_569853 or
pg_createsubscriber_459548_2348239 was meant for.

This is particularly annoying because renaming these objects later is
either not supported at all (e.g. for replication slots), or may be
quite difficult (e.g. publications).

I do realize there are challenges with custom names (say, if there are
multiple databases to replicate), but can't we support some simple
formatting with basic placeholders? So we could specify

--slot-name "myslot_%d_%p"

or something like that?

Not sure we can do in the first version, but looks nice. One concern is that I
cannot find applications which accepts escape strings like log_line_prefix.
(It may just because we do not have use-case.) Do you know examples?

I can't think of a tool already doing that, but I think that's simply
because it was not needed. Why should we be concerned about this?

+1 to handle this.
Currently, a) Publication name = pg_createsubscriber_%u, where %u is
database oid, b) Replication slot name =  pg_createsubscriber_%u_%d,
Where %u is database oid and %d is the pid and c) Subscription name =
pg_createsubscriber_%u_%d, Where %u is database oid and %d is the pid
How about we have a non mandatory option like
--prefix_object_name=mysetup1, which will create a) Publication name =
mysetup1_pg_createsubscriber_%u, and b) Replication slot name =
mysetup1_pg_createsubscriber_%u (here pid is also removed) c)
Subscription name = mysetup1_pg_createsubscriber_%u (here pid is also
removed).

In the default case where the user does not specify
--prefix_object_name the object names will be created without any
prefix names.

Tomas's idea is better in terms of useability. So, we should instead
have three switches --slot-name, --publication-name, and
--subscriber-name with some provision of appending dbid's and some
unique identifier for standby. The unique identifier can help in
creating multiple subscribers from different standbys.

--
With Regards,
Amit Kapila.

#191Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#183)
3 attachment(s)
Re: speed up a logical replica setup

On Fri, Mar 8, 2024, at 6:44 AM, Hayato Kuroda (Fujitsu) wrote:

Hmm, right. I considered below improvements. Tomas and Euler, how do you think?

I'm posting a new patchset v28.

I changed the way that the check function works. From the usability
perspective, it is better to test all conditions and reports all errors (if
any) at once. It avoids multiple executions in dry run mode just to figure out
all of the issues in the initial phase. I also included tests for it using
Shlok's idea [1]/messages/by-id/CANhcyEU4q3Dwh9aX9BPOjcm4EbbhyfeNeGOAz8xfGyJMcpZfjw@mail.gmail.com although I didn't use v27-0004.

Shlok [1]/messages/by-id/CANhcyEU4q3Dwh9aX9BPOjcm4EbbhyfeNeGOAz8xfGyJMcpZfjw@mail.gmail.com reported that it was failing on Windows since the socket-directory
option was added. I added a fix for it.

Tomas pointed out the documentation [2]/messages/by-id/6423dfeb-a729-45d3-b71e-7bf1b3adb0c9@enterprisedb.com does not provide a good high level
explanation about pg_createsubscriber. I expanded the Description section and
moved the prerequisites to Nodes section. The prerequisites were grouped into
target and source conditions on their own paragraph instead of using a list. It
seems more in line with the style of some applications.

As I said in a previous email [3]/messages/by-id/e390e35e-508e-4eb8-92e4-e6b066407a41@app.fastmail.com, I removed the retain option.

[1]: /messages/by-id/CANhcyEU4q3Dwh9aX9BPOjcm4EbbhyfeNeGOAz8xfGyJMcpZfjw@mail.gmail.com
[2]: /messages/by-id/6423dfeb-a729-45d3-b71e-7bf1b3adb0c9@enterprisedb.com
[3]: /messages/by-id/e390e35e-508e-4eb8-92e4-e6b066407a41@app.fastmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v28-0001-pg_createsubscriber-creates-a-new-logical-replic.patch.gzapplication/gzip; name="=?UTF-8?Q?v28-0001-pg=5Fcreatesubscriber-creates-a-new-logical-replic.pa?= =?UTF-8?Q?tch.gz?="Download
�.9�ev28-0001-pg_createsubscriber-creates-a-new-logical-replic.patch�<kW������B�3	ph��7lv`��0�;�;���r�����m{,������$��-��ag�.`[*��J�RI��x����!?>�=������������z{�{>����������{G��'lx��S�����;s�.�����}�A����^&��(�i�������:n�Of�������c�������s�K�&��{�)����������c6����%�;/�B�c������L�a.��#�i��!Ky�6��2���?~b��<�t�26�E�����L2�q����g��L��<���;�A��8�;��tD���]��|b��m���U���c�a����p/��g�_\���J9"/�}��Z���q�<��7HY<a���(�#GO�ELt]��g�8�p����:�[m������F�;���
�9��FP8v��A��(����������a^~�q��z�<�$3��M<�,Iy��������sN����$�'bq�hf�H�O�22��9��^ >)��DS(���\��w?���kv{����
Q��@a�!}����^�K
S�>��	���0�
���7-MD/p��ZkG����I��`��!2-k�v}��k`��Y���<2��j���~�}q���Q�C�y����%�����@�&
��$f���f-�(�(	A��O����Z,S3*A���D������,4"v_�
����H��Ym��y�2};��0�I��v�7s����w����q:����H���<��|�p�TO�������!����,���?�e;����k�q�x�!��8�p$������������F1h����w�9N�6��9m�\�y����}@�<����hS��a�^��������&a���#F=��PI��e�GGX�z6����e{��!���vG-6�}��`���W����tX��"�t:~0�0��`n�p��4���ol���=99�{������%�������,������l1x�6�'�#�Ah ��� �A�+/C���?��B��:l�_�n�n�V)
Q\n�qs{��m !�o���Y�s4	���U:�7���3K����������]��7�av��`}���@��Y���E>O���������|���g��O�RD��4����(��������`8<!�����c�&�������3������egu;�b������k�99�X���8�@DC�������$N2��Bb#�)�Gs�����!s7}:�`6����_�%t�^�+�h�� ����>���`���&�s7z��(��
G��A6�D����Dp��s���8��
8�l�P�-�<Mb��`�����{5{��,�Qk�E�4�Y��BN<Eq"Q ��}��^� �����?�������7���8!2��Q���&Y���NU�i���fq��A� ���Q��"�u����W-��h��w�'T��9��������(P`k��Lqkc}���$���C�"nw����<0+ 2�<�N �UA��Q]
z��i�NRY\�"��
��A��h-md��HHz���I��i3���	�lV�`�zC�yN�U�KXf���l�qJ��N��I��!#����r�E�{H�B��N1JE�)�M ��-�C�zAoB�O�{��y���Y��zR!!�gg>F���=#o��Q��
�15�}��]�A `<9�S�����0�*��L%\�%%2	���K���w!M����\�?%}p�(������sBd<��-�������IZ�!sp	A�p�+�BD����F����Y��5r�����!p����+�c��F5�c�:i�.i)�R�H,��\\XI&�h�a������v`����S5&=�i�R��P���%>���_����v�����B���Y�
��44��������%��) y��
����B�"d�A���Y���y�F���<7�.
�a��LJg�u7���c�y3]K�$Q���k�Z��K�����1R:*K��� �qRgo��m6��1�x��` ��E������Ql�V^(��&�;(u3�9�l�����&�����X7�<i�[���"G�LJ�8��'��Nu�2I]c���2�A�2�L����H*!+��R;chGz3���B^�ETc��{0���%��!|�C���Z	�4���~_�Z�sk��^�� ����`���$*�`)�Jn��I�����B��8��o�P�L0N�:
9���?�Q�+X�g.�����Sh����z���:���pR�.���������.A�r-����4�e��+�:1�e\�4�|����z�q�T}�9�j�����y>/����ahs�N����*���C[��Zt�k�
<����W����d2}�omX<
h��p5�i�f��k[U�Q����Jhq�SY* CKM���R��&4�+y�uI'7�S�h7�D��(�.�����q�g�n�~�S.J����F��K���*[4��jw>��k�$�����i���g�kE�C"������8�S'5,0!�%�����V�P���xY�+��
g>�$R5������0�p����+������7���!��uS�2�P����$>�[��~
�N��Y�r��e|�M�O���D��dSC�$y���J��1 ]�UUS��L8�f�����k+!��^����r{����*�����TR=��&	�,�������"Bs0U�6sT�����7+�EY�g�R:���Gf�A*=P����uH��U�rM8D�za&z5:H��2�
�`��4��Yg�S�0�
]��6���Q�'d�H��G����4)I��GS)uE��dL5�+��N�7�L�OB�|Q&�J�)��Id'N�i9T������$�<-
F�;��N5�����j����jD��Y�l>�ds���E��T��o�{x��Ib�����f��l�^�}'�C��|�6
-�X���c������S-+�����e5M�uHs<�a2GB���a}��`��4���.[���Pt5jJL*��69���������6�f���S�BT��,��j��	��R*������X��>�{��}Wm��U���v�J���p]��G�NrE
���;z��,D
t�3%��Y��-"�Z�/!H,�4p\Df�)�"8- ���dC
 ������&�Y�p����e����G@(�L:W_�;����]�
_���P��q�ek
@���t%ok��r���
�&�����r�����D/J���>v
�
l{���Q���"������k��G�Q��8+������t����PGJ�*jA6+j�J�V��������n$��b�<�K�}��WU�n�#�����<� ���3���T�j�����KF���q�^Bs��?�N���D��X�Q�s��
���e������M~9��U!����L0� X�P-����X�=�e��E@���k�
�WU�\K�m~��� ��2�Y.jYVX�DrC4_O�X*2���E@�2&!!31��V�X����=/_J�VC��h��;~6�va�m�Q�[� K�V������\S�u!M<��Yu��s�R�:I%��L����k�'�
d�����'vTE��b��Z�4���T��������T,��T�D�F����M2u����eU�<{!�#9Ft)!J����yD���_eJ��.��\���J.�MY�=�L@-�t�(l��R�Vp�����������^t�J���,$_�������X5��K����!I{
����CN���2iQ;�W��j��2Xt��Z�����a�����8�zj�MP��U���a-xfV�F���-hH�gE+%U*�[U��zD������q��L���Yy�,�W�1'��L�O������j�����t�$1�v�g��dR�,/�y�)`��2�=���jj���uu��Fa���W��-���G�|��Z�u�����\bz 
�V��������p�XG	����H��l���&�Ki�J�,�K��J�S�������Yw��qd��Z�5��X����a�3��������H��A����B��hw����;(����[JY7�����������$�rL��H����;��%4�
Z�'GB�5(5�I"��,fD���]/�(��&��d��d�RV�
���)�,iD���d��5_��%~ekk�[���jdr�tB
F�D��SU�����,k2H�;��6�d1g�fV���Z�v��i����+�bx!#^�3|t�w���:�������,�����������oB���qg`��])��y���l���A+�)�_&M���ZF��D|��r�,&|�f������-R����y��n���sG�K}$���e�����@�.�����k��
��o�*���<X���lh9�Q�
�V�h?��^�Xn��uV�u1?�$`[��W�������w�U�m�r�����[��3������V�T01	"L��n����-�`^{�t��KJ�����;$��r,�61vo��*��U9��l������Y���������+I,w�\/`���B�����Mf�M@/�.�X���Ulj��^��^������P!GY�7��8e����=��t��,��0�Lv}��Q�P=;"�\e�lV��6���KzMn.�8�p���Ts��'���m���z������$j,�����E(r�Xr�8��`�+�mGT��Tz��\�d=���(��:�)��f�~��d��FR���\�wjR0�ObY4�����K$kv��������=�������J�C�AS�F�"��wa��2v",I�b��tZH9�((�$��@�tR���h+�Y���i�s��FChK6����
kV�r��A���������a��\��a%���u��<�hN�-=C�]v��vMI�sy��p%Jm^����H��=9��m���,���X����Fh�Q���w�O)k����-�����:.�}T����&�����Dt��s[�qL�7�f�����L�����m�(B��4�J���o.������8��H�B(w��kf���H�
^�k�hR?����
+{�q�\��"��l7���Iv�������Q�g6'�9����OE7�d*��}r��Olc��km�9>���������Q�y�9;��Fv 3����)��7����������~f���)�Q_5���`-rQ����~����
;�Wu]����O]r��M&�����&�o�2��o�1[��S��t���;�:�>��L���A�������'���"���My��OD�?5/o*ga��g�R���?U�������&��������?��{�����Co0��s)(I�������>^�t@D�6���.C�����@x L���ot`������
O�@��x2
�����dw��=��Q�h�4�����z�k/8����?����f�v��cYPa&��^\����^D���SV�U(�jt��wh(��@X�
������o�_���?���:�Xy��a���	�� 0�W[o�"������B��_m]_��w��������Wo^m�}0��^��j����������vW�\�w����:�t��?�>b/N���"3����������*3����'�R1uF�R�=�A*gu������������|�~�*��'�|�uqys{q�y��,P���o4�$.����d!P[��VLK,Zm��&�2xf�M�����pxy��!3
Z�"#���d`���3Y������mq�&]��������R���:�8
HS�����W��R}��YcA�Z��:�U�VDd���E���C(C�7��o�����L�yil��4�(�<9��~xx4�������>\�a1a-��f;\{�������Rf��|�n=��~k��m��a�N�!����E9���;�i�j�������k
�N0ab��x��k��nc�(6���A���CD�<��4�������.��0�,u�`���h�q>�����Z��yS�wW������4cNZ�G��G�����`����mp���B��w�'X�y�Y6t�������<���?J����DI|�%�4����4,}S�cUz(^�$��'(������x17
�C`��?�������#��o��:HF�-��=v�,��,6,�8{b?s�4��V#���g����{z��q6�y�O��4��U�U�U���Na(��r����1`;���V��|k����XwOT"���������E�D$��{�P�������m_��?��F�_��Q!��5B�n>l��X���T�w��u��0�f�����b"��!�l�<j&
��� ����1w&�)y������}D~q��{vx�Z����IC��`��k_�bR�|��2���y�}�toV7���#*E��}R7�����N����G��'����D+W~i<	)�������Q6��
�x������>�����8�O��������in:��x�]F)���G@@��I:�e����0[�?x�{~t�����'��g+�|����>H���N��R��VVb`�)#l���M��r(,��;IBg������
�[��3rN�@�|�@��O��D
�[���hz���C�6O��'+������3�
���o*8�|���[��p1�IJ*��Njf:,���~�J���,��(�+��b���cZ���F���	�����m��z#������l0�=$?����p���b�L�O��u�#��Wf}���Z��O�&d��XJ�f�C����~^��=XbZc3����`;�0|�-��v����x��Z�y�B'2<���jEX�gK��fE�����<��\wz]���4��K����T#|�8|���;�Ux��Pr�xmS��H����I�k<��bH]}��B�.��u����������r��:���f"�?lQR�����Vxha�tXf����R��,�6<H�U5�`8�(���Ph�R�	�>e���P9^Q�����*�z�V�<y���`�R['*>e�<�H�aJ1�����;���dNS`�=�8��^i��^t<���t��W�u2�%���)��.�2�z�>��pt�.i��I��
#��u�����
��]�9?��t���g�:w�z����&���E|:(�%��>�2��a6-����R(��c�����kQ97/j�����DKd+Lm6�=:��]/�[���v�UT�I'�"d1��{�!p`,��%Z� ���C��O�2�#�3�&�����{��
:�lm��6R
h�.K�����^����6�R���8��O[22�T}Ns !<:7�mw�u��|�D�q�g
�Yo�������4��W����}�{x��>;8�q�he�t�m	,U�����5@�MI�<M^�'X��t��7D����	�%
b�����M0�@�V�J=X#�r���2x��
��:w0a#���}�j�
K��������>;<:����g��_��Lm�)����m����q����a�Bg\�����{��i���CM�*^y
�D?,�\Q{_�Q�%�RT��s�3�XH���<����):��gyQ�1��I��v�I��5�9�%�M3 Q�U�(������2���,+�w
��5��8k��c�c�g�:u@��l��"@���U����pC��`�c�1��6�.{��0\����]nl2��Bn�>�������������J�r=���=T���b�����J�����r����i����5LjcV�����pEJMIx3z���)����O�����S�q9��}"l������5d�s��Fh�&������(��XaL�;��	je��SC��pq
*_D�
���^���$u&��rC����'�7�M�*��7G�VI�l��'��#f���z���g6@$V7]�L<2��]b!;���	�x�b@�7B�d-"K�;S~]e��Au0��S�C�K��L����9n��|�����f!X/�|��_�T��#8��S=o��,���R�cI�d�5����L�����W�����e)��,��1�f�R
��
�ch��C���~~b�)�����gL�f#�L���u�����������Z��$��@��Rm�c������$�n>>�[�w�VU+o�b0D[j���5����;���]��f��D������������i�����r����4t�'6m�>��\���Z�0�7����>��km�U�k����n0���3�mN��Z���������3Y�����X�����\=�N�T��j�]�����i�����G�>v���;2���D���M���@���m�Gi�0u�5G)��&C���m�l���+�-���f7�vxJ����=����g"5Nv����� }~��� B� i��d�����~;?=��?)+�����c�Q��
0��� ��
#��hgQc�����������bm���1G����.��j�%�jG2�%Dp'��2��H]��s��C�G9�X�]=D-��[��l{�8�'5	r����Mz(�6A�P����^d,6�?��$O��|��k������d�>
�r� i��?���.��[����Js��'�����d��Ey���P�����X]_��J|?�8z>E�D��K���S���<h��F�b�<*3�z�o����x�����*<���}6htG����Xty��7�(
�X1��3e��JN�z���#l����A��5����v�l�����&��^�0�����_���|�3v��*0a��Q��b3��F�8����oT����-�_�qgw�?��������T����(�n�DE3�h�������[{����<K�d,,_��M��&��Z�&)=���~J�Y��|�7S����z����/�.Wz0��=��x����)����
�s�08C�Y�!��`�*�$�Ma
[J�M��U�a9��|�E9�7C�HVr�]�`:���\940����5wZ#���OX�v�`���_�+�{��xl���6����v6��/�N�`A��.��,i�����������_��8�zi�9�o]��Zv#j&�Y!v�M�����h�+�p���?�N�=q|F�^�z]�W�=��tG�u��Y��W�_��
���i����`.1��^�����5s����{�a�����e>�*	TZ���Y�IX�3$f������@x08�L0������F�~r�P��<��~�8�s�5��HM�Z>�[:R��� C��h�
��,2�oC��K`54��-���"����#�i��pv�`�2n���e�P����Y#�fe7��Q,,�Z�� n�X0|�������
T�Nq�@��1���LnC�����a,u��ZO�-���a�#S�>>x}2V"(C��*����e��t��*lR�3bC��MH

����U��L�*�i��h0F�D���2��Y����l���K�a���'��vI�,)Q�i�Jo�e8�6F��[��!�����v���V�pW��L�;��P
M'��`�*�������h���nQd���������4��D�<&U���6�\��4���,���o%�Q���=�
��7���[(�.	�YJt�����6V���^�����P��R����C��D�1Z	Q������>5K��8~`>���i�j=f'���$��E��|�� �?�a�L�?PL�����I��^�`M������fyW�K;���=��a�`��3�8�cf��RX~�%R�1��z�0��w��=�
�^����
��|�k<�����$��u�4���o���$%���OZ�Xf�b�d�\���H5��JM�����@�e7+���i�>�J�f���u���RnIdBy�p�c���i�hL1+|��c�9�a����j'wQuCR�J�`�}���U�s�f�9iy9x���'�w^����az �-�^cX�����(��uI�:��Jb�q�,2��z����?�u~z����5E%+E��-�`��~��8yyz�N��O����a�CS�a�����5��>kI�;��RK�i*���`o�7��g6S�����4�@�9�;C�2�9c���nA=	A�l8T\?A�4�+�&(hj������#� ��T.)dL�s����n���0uv�,����X��WV���a�^�A����W{,������PC��$Zb�Z��d�����^��"�'�m�?9�Or����}�2�?�3_���c�:�����Gl/;����}���8��' ��������2CFEi6N�I>����C����f��A�����V#B_e��x0L)��R���n��0��~4��\$.��J�Kb!B
�(I%����� ��X�)����F%0r�F��7�3���<{<-P^�}��.��8��;���/� ?_
4rq�������=[>�J~�]zK�$v!���|<��&�7C�]0RK"gYr�^���L���I��,�B��.�a��KWbJ����`@k=�r�P��<V����T !c�[Tn�:�X;�Sr�B�KQ�4��&x�6� 4j���^��(!��>���F�O��.0���l����D�d�$��O?���	�A�B2�c���<���wF?��7\L3	��,��un��L>4���eB�z�Y�4`6&�"�6���O���~���kS�����`�# �F�M�����N��J�h���Y:�����s��$:�Xg����[��L�L��u���[��S��^6%%f4JF�DWe��G����AR���n��4�;�7|a�Eu��A�%�}����"P��3�@�WS�"{q�[��-(�;�Q��`BN���`����.����q*z>��(���c�,���hH�W�x��Q��\2(K����#�J�X�[)	r���0�����;��q���1��%���FC��n��2b�XI������������D���e��i# U�C�{��i3��Y�R�a,�]�9���g5q��&&Z1<o�B9^se
��"~���|N�O{K�	��!��EL��X:'Q����m�$lS�*��p�]T�����5jm�n�n��8�
���d���K;Z��������;�;����OL�
�^���85���&�Z"
�RiR��q�Xh��j�!g9<��0��Q���������_���eCi��������X�"�R�����4B��.WH�b�u�Q�|����6Ev��d�
����TS�������h�,`��m�&E�����/l�8�M����{y��HP����"�e���`�5#@,���4_�;�#�����a���=R�bb>�c�aU��P�o������Min�wdz�8� �+�h�Q����Xi�A(I��T�O����@+�]��$�}*�,�b���m��|]	
H����tmI�	"�f�H}
#��QS��=yC�$��Wh
��CT�Hdn
�t��j�����	H�R�V��sF��K8�Y)���*h�����3���t���W�|�����b��r�,�����!F�NnwN4�/��
0�2
�S��O��lv���?�}��6F�`�`�����([��y��l,��v��
8������q�8��$���X��J�vu��NS���Q����	|��`��_5��������%:�T���'�DV�)���e�<�L|O�R�|6�;��
�g�0N)O��MfT�.t&7&�8��g��D��Z�e�Ak�W�$�yW$��Z�f]����k����FCK3���������`�a�2�-1i�I!~�n��7�gMn�n��I�nn�������\��m��m�
h�s������a����@z
�3��	>s�n�D�
�b��lN��e�OD�R�&2��&zs��*b+O��k�}������u�`�e�XGX`G6����p�Jg�����MZ�&M�����{�
i&�S����M���)�d��"���lbTjS�>�w&y?��6?���O�����0��o]�=��Z��e{���x�����Z�Z���y��nv+FX�|���^���Q��F�Vn���������e]�]8N���4G�e��e���4������F��(/��\��J�s��C�!������Z�}w[o�?�l4C�pS���)��VW���h��m�G�n��� kLk2�mb��vLv�5��z�E(��,�}��h|�������7�p����K����3_��]&-�vl��B�<����/���fX��!�QI>aP��b�9*7��f�<D5��@�e�Vk��S�������63��6��c��;��
��A��$��C���Sx��kR�����K�T��<#���i!<�hX�U��5���I�] �nh���}�����7��T�Vp&��\j�Qg�{��B�'ki@K/����,���.����f��3,�<g�y�z��s��<o�}�����T����q?����%m���h
��L��LE���\�$=P���h����Y��|���Y��f�	�#n9�NE�	��4ez��q
�+�lE�-�
��OY��2��<�i�J��%NRtn��tE���b�U%|��SW���,���
t������I.��p^:��`�����1��$��eX������,*���B��������W�;hy��8�<�P nf��t�2�3��2(�`�03�h�����������Nv��7%(�T��v���>SoN*�I@�I���	�@<�f�`�L�^�z~p�\��z���5����P���<��m������u����$�8�(,���~:�;���+9=>:HO^�<6D
k���������x%�q��?O��">I���X����7E������4_��G�e�������f�Z�2q�,�L��T>���)��u�6�|�)�8���������VB�)���q�i@;w��6jwc��fG$QZ�#��?����4��G�����Y������<�*�T;�NjM|y{��{���6�����uMp�:�[���E[S�(���y���~��t��4y�������A�����7���g�����o{O��\���}v���VB%l�>"/D)�BT�e�������u�x��v����		W;�i��ZF=�3Z/=���!��q��cq?�GE�|.�>�uY�/��Z������#�A�&�X���Y�Y��=��H�E����Z�I�������������X��9���:<M��tz�0�����I�C���� 3�k��[����d����H�j�RAf$�,\�kV���� Y��V��S��B^��
R��q��V����h��pA��7M�j��O����^���PL$��R��d��T�*p�oN��T�3/Tb��1Rw�:�<ig�Rd�	&e�cNQ�}��y�����T�9h��|�\�y���X���)�!��e6	�����/����?1(�$�z*���N���u^��OMV%��*��]
�5�g�����K�1�jB��A��{j����i.;�����������^���.�xK"Km��3�lh��1�X�m���D	��Y�l�>fcY�s/[+�9=??>�
QYd��b�]���d������p�#�n���0���@�T��
i�HNnJ���@���
�3]�K�m$��6C�s�Z��X4kV.�CrL<T����"l�D���:�tB�L&��]�����c�tL=�No���� )"b:��U�C?�t�+h
jCq�������g�y&����=U��o$$�P�A�Loj�\��Uf�7@�p�^6���^De��8�����|�GQ���-/���k"��7�Y{+�1�h�1*�Mu��UD[+&��6�@��Z��5�9ih����d��M���s��������7mC���Y�7��'����N��N>���i��O8T�,�J������Y�{��]���,���d�YEbF}dt������U�*�a++t�D�����z�^�_�{L"����!9Jb}7ZG��?�vG��b�k�K'�T�`��L���v1^��bcC5]R��#��P�"����: Io2d E�;��O���Q�/*"6	&2���e����U���J_��@K9�v��tw�q�����v���XA�_�
5'MrL?{qA�������N�f&T�����7�/X�S������ctS���c~Y�F��V�!�J��fk0�t��7���H���������M���e�t/\=8��]���Z���5aV,K�P5��B^^[��H�/i#%ca�NGe��^MGV�ul����n��U��v����<��]{#I�V��@W�~�`n��*��Ds�`�������t��i,��CR�C��B?"��0RrC���k���8B�!��rZK'5z�=����DA�~�������^Q��[�x�z�k���U�/���`Ep6�V#SJ��2S~�q����-���Y
N�R�UY��1�+5�s��<q��Ua���p�uE.o���<��r�<����9?���8���G�*��p!qvC��2Q�CZ��2���&�"�h} Ld$Q%�H(�\�o��Ty�0e����[.������O�g��m�r���@�P6����o������y�Z2���V��}��5��^g_c=���?�z���ow�@[l��_C���6|G����O�2�������h���^z��^kgY�g�i��m
LZ��+ BT}:0Dw!���#�N�AB������D�ys@�@h�A/ n=���l��w:��/�E�����f3�C������Ox������
�#V(�H��	r��
���x�eR�.9}�Y:44(���z(����lS�}YD�7�9e\V�fn�U\��d=���"��������z�p@�9���V�m��0��|r����vz�1�w� 9:b����M�|BWW����	��l|�����
��������)<����@����w R�:9�W�E����������
����+�D��sc�RgR�V�T��%�ni�O����)?��\\h�t��[[�S� ���^i�A�
��������E�2U[�/����j�X
�tg��<�p���������hKE�%�=b���]�K���u��r��5JL��C1����L�5*�g��*�0�;��hL����>b�\�����H�����;���A5P��V%��BS��V7����DX	GPE�+�W6l����)3��nD�2�f����S�l�����������&�����V-����B2f��;����=���5E���d�.��!�at�?��(<��Z|"�R����Fs����%p�#w�La����>{��$m��������/����cSX��e�>�0K�cG�G�S���	���B�}�$m�R�~�����:9k���l����7��~!b�� ���L�T%�b��<Q�Ne`\��S�c��eaeo�(K��L�,]t��Fi���I�5M�h��o�m����J������:g�ZNv��a�"�n\I�E��B�����N��B���3�4U�Ay�m����)���rIL���� �=�����\n��������\"_�:�t\��jv58B���>�q��|n�qH{x�8�����'���}��'��������ti%+%�s���e�[��#6�|�-�PQ�-�!���I'����ZX��;�#�Z>�Y�;R7����~��AY�>{�D��8��k�/���>z8A���a2�-�F��<[�hZ���������5����&��������=��<������r�,�O��h���f�t�n�l��89��1A7�'�~Y.�/�U��
d5�%����Z����WTx�a���v��=�E-�Q�jO��r";H`  x�cU�2!*�7��^�5^�)����{�������V���b�l����m�tcN�r ��Ob��
Q{���!����������:p�#�L�T��)�9�P�OX�����s[WI3��p�����z���6 	p�����0����E�Gy����������L,�{��xI{�������������C\��z�����Y�������&�73'c��s|[��IuQ!p� ���N'v�~*~��q��M0�%�o$�������.&�w$/�O�����l�9��l�.Y������W�v_�/u���R�9+��%���Yw�C�.����������0����R>@�
<]1�'~�SJ����������O�O��K���\F������9w��:����g�r��#r(d�������L3u��n,��;d�U��6�;�����+�]�C�:5��a��E>�B(�L�1��}r�1X��T��9�KQt���	�T�pao 39s��b��rl1)d�{/��.����Ln��RU�����C��&������>���$�H�2�]��>t�R{��1��=�j���(��O
�U�n�'�3YN��y����C��N(��3���nl���J���3�Nh�K1�R^&������E.+rzM��^(#5���AK�D�969��c}��� g�z
��r�TA;-2���6I�<����'��2���?��e5Go>��;�]�R�>�����y����DwSF��4�+HGr*Z���[�����]Z�����/��.&��x�??�W�>;���� �L��\�u�Qm b9Oh�����H�F""�2����+�%aV��8 k|�|��J����,h��esR��:,r�����;*q7�t�=>@�73ei�\B���|� B%'qti�^9��"�?.�����n]��g����~������'K@
�3���0=���g�px�$���nRX3�`�4��D�����*jDz{V����x~����������C�m��(	���XY�*������I�Lr���~]���&��"�e������cY���B#�#�$F�p�.0
�����S����������p37�P�X���hp���\^���j�v�NB�q����%V�Lxmb��K�<�C��d��B2��p���GfV6�V�/���:��%�h��<�?���L�8���-4BD�����!5�?)sLT��l��5r;�@��Nde<�N�*'���YU�Cb�-P�����ZIm�����d����yyk�n�]���"�>�+c��5��Y]��h����wM2�C�����P�P�{��{B�����Y�_kf������������[����G�������a�[����0/?7��9{��q�3[�����;�|��uF1�-�|;����f�.Z�2��Ilj�u%T�Yo��7�����%�x�si^������l0�x����e���c�����M�-��_��i	�|b�����}�G�8b� ���kS� x�{jz����ym3�e�� �tS�w�@�@�m�������!�����e0�2��*G�����wZ���Tz<���?o�S�;���T�����������k&��@%�8��^[���=�����O��}$���������W�������ZC�xM�3,��_��mh��jIr��o����V�����Ky<[�4�r[?J[b�D�kf*u;�N����h��	(}��5�.���m8?+�h�|����U� Ce���SL�_"%RMY�_�����z���^���!�zr|z�1R*x�V3��������`��)u���n6��)��(rX����b���0`7V[���8G�������~���������'�f�&e7���L(�I��!�b2����3���Ee*�rok8"��*#S�a��`�IIp�#�@F���C5�KTS�7�����+�yH'X�����u�^�2����x��Yc@rZ�=�<=>y\<�<>���aE+��-��u�g7�	lB��xo'Qgx�-����e/6����w��]eIVN��V�u�q��o��H�0'���!�+�FX����y[�C����O�����I��IF������M�C42����L��_�s�C��(�^�z���y����6��|oD��A5�_%��mN��=s�'zi"B�jqJ
�k����z�l��&������U��y@Z�.<��O ��,e��?��G��+m�{J�8�kD�V�I�	��Yv6�%)�#[�����U���k���/��������>��?8��
����������:@[b{�4����/����b��S~(:���w�R��3����@QIt���kI|�P������ib-W��x���amW�-5����k&&h��n�G�EP�,����^IM�c������>��y�"�T,S��t�1���8�?�����f�z������d]�
?�����[��sm8���A-�������!/��D9��T�p�s��,�+�V����R��`O�5�P���<o��4~CD�O���R"��T�i�3�_�D�����a�V�	=��J?���_'�q�%�R��<��(DI�)}�w��w!7����YxS����/����� ����cb�~p���
�$V�'�h��q2���(����nQ���7��L�dI��!xzk�o�'����g0��%�59��Q�M���L!����muP9����$���%����<�\�t�l��M!d�}���F�7�K�^�Fzp$����D���Q,�����
����������$���K��.:�_P}�M8���[��J�6����������R�!8�h�r�7s��7��g�>#|�e�
��*��7���\���1�'K�[-��a�O��	�l�DWg
�Ii�f3Y������f�t`EU��e�%P�������������4*��-��`hIE�z���"�QC����9zCi)C�Y2�Rj�����_J���E���xBUC�5��|������7��]X���HA�9���x�k�QC���O{}��0�u.[��jF������EN������������Zk>#-�Pl��>r81�������
�Lh?
���S���)os\w�_����}�{���t@)���{���S:Q��+�N���{t��2��c����a��O����$Hx|>��{���V�������e�i��DQ�gp���[H{c`��)���q���gs���
G�9�����&��0��r�Fs���E>w�"r���=�������}�.�#f�rCe��9�����m�#5{
V�����mq���dd�����WF������`�S��������>2�����\N�R���������������t�`�<_E5+0H �!�ep��������TP�����8O`)>0F����p�z����mU�i�������7{%Pb�V<,���X\usl0�xQ �o��[B���/p��J>8U8�oQ����%Q(3�Y�Nt�T���W���^�$���Q(��X�e���pVR|#�K��6�������r��h�wa�p��8��c�\B�q��2�$����_�D���r�^R��I>��*K��v�i���)�t��n$o2���28CV'�8�6G�m��^��U7��W*b�J*�Y���[,T�a^�)��c�Y����y��@�U2A�s���Lb�U5�|v�	�d��������S8�T|���D$2�l�h�m�;�����7�t�:|�j2
x>qh/��x%��.q����VE-Pq�.�$Z(�������fY�j��XH��XhCh��y+��>���C%����x0K3�B����e��	Bzr���k�reI�WT��tI��z���-� J$�?��������&Y,�*��J-��7G(_�����u��@��em94
����Q���*�~s���"�8��'�P@C�g�'"���S�Zd><%�����9)2������ZF=�Q�Esl4ExS���%[@���i�

qD0aE�!x������� V*�|Z��C��{xg�w7�B7�]�O��6'�[���H��h�\����av��
n{������G���l�?�^t]~��n��j��J6{����t0X]__�[��K��jn%���������/��_'{`v�����m$;[;��:Z�� �����0�|����%�m#B%����N�DP'T:;E����S� y��H��"y�yO��g�(`k���������{l
V~�j!��'��1�w�StC��/���R�!��%2k�g��S�!=�������m�4����i<~��
<��_q�������pZJ�����=�C7��b��4L�Qx��$�p3Yk�8J�m�e�[����G��J��9�b��_���:9������5x����)��5�����eN(���4�����(��B��g �k� �*/��q�%�����D�c���edT/�$�H��$o�U~��������K���O�IP.������O��[�n|��V�f���.�z�����j��J2�|���e�D-rl�m�K
�R�';�"�2��E�\t�/�*^��_���<+\��f��>\��p<��_Z&����z���Q�?�����A5������m�:��w��2��������H�=��>�m����$��|��	�6����#o�S2�l'������6��������Yr����_�q����]_����Z�V��N���w$	S"SR�����6�iX�p&~��r1���9��v��t.������%;>��k�w�m�A��v�V�`�NQyM�}���v�Dn��|�k�����~���?���?�s04`�������Z-��U������S��c1��U�q���<C�������I<�`5����0D�����9?+E?F�����'t�!_E?W������g��,o�M
h|�����4�U4���������������(�Q?IZ�����^���Q��|������DI�=[,M��������Yv#+�kGt��EV������S�@�<�s�l"Nc~c�����t����@,�G���\�u;���O��m*e
����K�����kfr%���[5�%rL�������c��>��0<Z��+|�@P����2�SA��,�0��i�����9�^,A����b1b/���-�U���M.g���w�z}�t��������S��;��'�9g�K3r�/)'X��Ut����	�������T�V��*��?���v�nS1�9L+J��\����e�%����L�^��f����E���\B����`-�NQ���]lS��5�j��_R����B���H���TA:�-�a������n��	g��E�M��(
^�5��%�

)6
��1��=n%LU��hd��G�.�<��sy����_�{����]��:Z�������"���*�c{RPP�&�����P����=+�~�8�;6j��8f�v��!�-a���78�e�I8�5�D�6����vr7�er�GM���-lc���f��r|a(��E58,s��J=3������I�������~�W�A�k:��>�PUN#�1)a��y�F1�kJnS!�pg��k����d������[H�/�,�P��^{�h2<��WT��j��L=���v���?L���p�����'����������,+����sM8)��d����h,��uK��i��6���ppk�`g�v�3�Y����F3Yx�;v���W(?��c��)����q�)SS����x��M��A*��7�z)�V�z�#������G�yJ�G�E��V�q+Qz����_\�|I�|��bI�S��JVw6����Y]����(n
v28-0002-Use-last-replication-slot-position-as-replicatio.patch.gzapplication/gzip; name="=?UTF-8?Q?v28-0002-Use-last-replication-slot-position-as-replicatio.patc?= =?UTF-8?Q?h.gz?="Download
v28-0003-port-replace-int-with-string.patch.gzapplication/gzip; name="=?UTF-8?Q?v28-0003-port-replace-int-with-string.patch.gz?="Download
#192Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#191)
4 attachment(s)
RE: speed up a logical replica setup

Dear Euler,

Thanks for updating the patch! I have not reviewed yet, but cfbot did not
accept your patch [1]https://cirrus-ci.com/build/5253379782344704. It seems that you missed updating the windows part
in 0003 patch:

```
#if !defined(WIN32)
-       sub_base_conninfo = psprintf("host=%s port=%u user=%s fallback_application_name=%s",
+       sub_base_conninfo = psprintf("host=%s port=%s user=%s fallback_application_name=%s",
                                                                 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
 #else                                                  /* WIN32 */
        sub_base_conninfo = psprintf("port=%u user=%s fallback_application_name=%s"
```

Since the change is quite trivial, I felt no need to update the versioning.
PSA the new set to keep the cfbot quiet. These could pass tests on my CI.

[1]: https://cirrus-ci.com/build/5253379782344704

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

Attachments:

v28-0001-pg_createsubscriber-creates-a-new-logical-replic.patchapplication/octet-stream; name=v28-0001-pg_createsubscriber-creates-a-new-logical-replic.patchDownload
From 769b072428c672c3c354b56a14ab2601d169949d Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v28 1/4] pg_createsubscriber: creates a new logical replica
 from a standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber). All tables
in the specified database(s) are included in the logical replication
setup. A pair of publication and subscription objects are created for
each database.

The main advantage of pg_createsubscriber over the common logical
replication setup is the initial data copy. It also reduces the catchup
phase.

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
using FOR ALL TABLES and a replication slot for each specified database.
Write recovery parameters into the target data directory and start the
target server. It specifies the latest replication slot LSN up to which
the recovery will proceed. Wait until the target server is promoted.
Create one subscription per specified database (using replication slot
and publication created in a previous step) on the target server. Set
the replication progress to the latest replication slot LSN (replication
start point) for each subscription. Enable the subscription for each
specified database on the target server. And finally, change the system
identifier on the target server.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  472 ++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2001 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  267 +++
 8 files changed, 2770 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..8ca9de5119
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,472 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. All tables in the specified
+    database are included in the logical replication setup. A pair of
+    publication and subscription objects are created for each database. It
+    must be run at the target server.
+  </para>
+
+  <para>
+    After a successful run, the state of the target server is analagous to a
+    fresh logical replication setup. The main difference between the logical
+    replication setup and <application>pg_createsubscriber</application>
+    is the initial data copy. Only the synchronization phase is done, which
+    ensures each table is brought up to a synchronized state.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because most of the execution time is spent making the initial data
+   copy. For smaller databases,
+   <link linkend="logical-replication">initial data synchronization</link> is
+   recommended.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   There are some prerequisites for
+   <application>pg_createsubscriber</application> to convert the target server
+   into a logical replica. If these are not met an error will be reported. The
+   source and target servers must have the same major version as the
+   <application>pg_createsubscriber</application>. The given target data
+   directory must have the same system identifier than the source data
+   directory. If a standby server is running on the target data directory or it
+   is a base backup from the source data directory, system identifiers are the
+   same. The given database user for the target data directory must have
+   privileges for creating
+   <link linkend="sql-createsubscription">subscriptions</link> and using
+   <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+  </para>
+
+  <para>
+   The target server must be used as a physical standby. The target server
+   must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   and
+   <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases. The target server must have
+   <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+   configured to a value greater than the number of specified databases. The
+   target server must accept local connections.
+  </para>
+
+  <para>
+   The source server must accept connections from the target server. The
+   source server must not be in recovery. Publications cannot be created in a
+   read-only cluster. The source server must have
+   <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+   <literal>logical</literal>.
+   The source server must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   configured to a value greater than the number of specified databases plus
+   existing replication slots. The source server must have
+   <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases and existing <literal>walsender</literal> processes.
+  </para>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    then the data directory is likely not in a state that can be recovered. It
+    is true if the target server was promoted. In such a case, creating a new
+    standby server is recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during the transformation steps.
+    Hence, connections to target server might fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Executing DDL commands on the source server while running
+    <application>pg_createsubscriber</application> is not recommended. If the
+    target server has already been converted to logical replica, the DDL
+    commands must not be replicated so an error would occur.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    should be removed. The removal might fail if the target server cannot
+    connect to the source server. In such a case, a warning message will inform
+    the objects left.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which WAL files from the source server might be used by the target server.
+    If the target server has a standby, replication will break and a fresh
+    standby should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is already running, stop it because some parameters can only
+     be set at server start.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. If any of the prerequisites are not met,
+     <application>pg_createsubscriber</application> will terminate with an
+     error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication is created using
+     <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>.
+     It has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u</literal></quote> (parameter:
+     database <parameter>oid</parameter>). Each replication slot has the
+     following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a temporary replication slot to get a consistent start location.
+     This replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
+     (parameter: PID <parameter>int</parameter>). The LSN returned by
+     <link linkend="pg-create-logical-replication-slot"><function>pg_create_logical_replication_slot()</function></link>
+     is used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
+     parameter and by the subscriptions as a replication start point. It
+     guarantees that no transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies a LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are also added so it avoids unexpected behavior during the recovery
+     process such as end of the recovery as soon as a consistent state is
+     reached (WAL should be applied until the consistent start location) and
+     multiple recovery targets that can cause a failure. This step finishes
+     once the server ends standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     The subscription has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>). The
+     replication slot name is identical to the subscription name. It does not
+     copy existing data from the source server. It does not create a
+     replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the consistent LSN
+     but replication origin name contains the subscription oid in its name.
+     Hence, the subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the consistent start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the consistent start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the consistent start point. This is the exact LSN to be used
+     as a initial location for each subscription. The replication origin name
+     is obtained since the subscription was created. The replication origin
+     name and the consistent start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the consistent start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..e9a920dbcd 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..56641f583c
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2001 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "common/username.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	50432
+#define	BASE_OUTPUT_DIR		"pg_createsubscriber_output.d"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	unsigned short sub_port;	/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	int			recovery_timeout;	/* stop recovery after this time */
+}			CreateSubscriberOptions;
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name / replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+}			LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path,
+									struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static void check_publisher(struct LogicalRepInfo *dbinfo);
+static void setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir);
+static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
+										  char *slotname);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(struct CreateSubscriberOptions *opt,
+								 const char *pg_ctl_path, bool with_options);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+								  struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static bool recovery_ended = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("pg_createsubscriber failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.");
+		pg_log_warning_hint("You must recreate the physical replica before continuing.");
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			PGconn	   *conn;
+
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].subname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %d)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	int			i = 0;
+
+	dbinfo = (struct LogicalRepInfo *) pg_malloc(num_dbs * sizeof(struct LogicalRepInfo));
+
+	for (SimpleStringListCell *cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): connection string: %s", i, dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): connection string: %s", i, dbinfo[i].subconninfo);
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static void
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+
+		/* Remember database OID */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ *
+ * XXX Does it disallow a synchronous replica?
+ */
+static void
+check_publisher(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	bool		failed = false;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted +
+	 *                            one temporary logical replication slot
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		failed = true;
+	}
+
+	/* One additional temporary logical replication slot */
+	if (max_repslots - cur_repslots < num_dbs + 1)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs + 1, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs + 1);
+		failed = true;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ *
+ * XXX Does it disallow a time-delayed replica?
+ *
+ * XXX In a cascaded replication scenario (P -> S -> C), if the target server
+ * is S, it cannot detect there is a replica (server C) because server S starts
+ * accepting only local connections and server C cannot connect to it. Hence,
+ * there is not reliable way to provide a suitable error saying the server C
+ * will be broken at the end of this process (due to pg_resetwal).
+ */
+static void
+check_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+	bool		failed = false;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("The target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		failed = true;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * replication setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Get a replication start location and write the required recovery parameters
+ * into the configuration file. Returns the replication start location that is
+ * used to adjust the subscriptions (see set_replication_progress).
+ */
+static char *
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
+{
+	char	   *consistent_lsn;
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	/*
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection. The primary_conninfo is generated using the
+	 * connection settings.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	return consistent_lsn;
+}
+
+/*
+ * Drop physical replication slot on primary if the standby was using it. After
+ * the transformation, it has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, char *slotname)
+{
+	PGconn	   *conn;
+
+	/* Replication slot does not exist, do nothing */
+	if (!primary_slot_name)
+		return;
+
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn != NULL)
+	{
+		drop_replication_slot(conn, &dbinfo[0], slotname);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop replication slot \"%s\" on primary",
+					   slotname);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/* This temporary replication slot is only used for catchup purposes */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_path,
+					 bool with_options)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	char		socket_string[MAXPGPATH + 200];
+	int			rc;
+
+	socket_string[0] = '\0';
+
+#if !defined(WIN32)
+
+	/*
+	 * An empty listen_addresses list means the server does not listen on any
+	 * IP interfaces; only Unix-domain sockets can be used to connect to the
+	 * server. Prevent external connections to minimize the chance of failure.
+	 */
+	strcat(socket_string,
+		   " -c listen_addresses='' -c unix_socket_permissions=0700");
+
+	if (opt->socket_dir)
+		snprintf(socket_string + strlen(socket_string),
+				 sizeof(socket_string) - strlen(socket_string),
+				 " -c unix_socket_directories='%s'",
+				 opt->socket_dir);
+#endif
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, opt->subscriber_dir);
+	if (with_options)
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %u%s\"",
+						  opt->sub_port, socket_string);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+					  struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	5
+
+	pg_log_info("waiting for the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_ctl_path = NULL;
+	char	   *pg_resetwal_path = NULL;
+
+	char	   *pub_base_conninfo;
+	char	   *sub_base_conninfo;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	char	   *consistent_lsn;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:np:P:s:t:U:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(opt.subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				if ((opt.sub_port = atoi(optarg)) <= 0)
+					pg_fatal("invalid subscriber port number");
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				canonicalize_path(opt.socket_dir);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/* Any non-option arguments? */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* Required arguments */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* If socket directory is not provided, use the current directory */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 * If subscriber username is not provided, check if the environment
+	 * variable sets it. If not, obtain the operating system name of the user
+	 * running it.
+	 */
+	if (opt.sub_username == NULL)
+	{
+		if (getenv("PGUSER"))
+		{
+			opt.sub_username = getenv("PGUSER");
+		}
+		else
+		{
+			char	   *errstr = NULL;
+
+			opt.sub_username = get_user_name(&errstr);
+			if (errstr)
+				pg_fatal("%s", errstr);
+		}
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+#if !defined(WIN32)
+	sub_base_conninfo = psprintf("host=%s port=%u user=%s fallback_application_name=%s",
+								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
+#else							/* WIN32 */
+	sub_base_conninfo = psprintf("port=%u user=%s fallback_application_name=%s",
+								 opt.sub_port, opt.sub_username, progname);
+#endif
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(opt.subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * If the standby server is running, stop it. Some parameters (that can
+	 * only be set at server start) are informed by command-line options.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, pg_ctl_path, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	setup_publisher(dbinfo);
+
+	/*
+	 * Get the replication start point and write the required recovery
+	 * parameters into the configuration file.
+	 */
+	consistent_lsn = setup_recovery(dbinfo, opt.subscriber_dir);
+
+	/*
+	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("stopping and starting the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	start_standby_server(&opt, pg_ctl_path, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the replication start
+	 * point to the LSN reported by setup_recovery().  It also cleans up
+	 * publications created by this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/* Remove primary_slot_name if it exists on primary */
+	drop_primary_replication_slot(dbinfo, primary_slot_name);
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(pg_resetwal_path, &opt);
+
+	/*
+	 * In dry run mode, the server is restarted with the provided command-line
+	 * options so validation can be applied in the target server. In order to
+	 * preserve the initial state of the server (running), start it without
+	 * the command-line options.
+	 */
+	if (dry_run)
+		start_standby_server(&opt, pg_ctl_path, false);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..3bc4f3dc18
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,267 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Stop node C
+$node_c->teardown_node;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Check some unmet conditions on node P
+$node_p->append_conf('postgresql.conf', q{
+wal_level = replica
+max_replication_slots = 1
+max_wal_senders = 1
+max_worker_processes = 2
+});
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary contains unmet conditions on node P');
+# Restore default settings here but only apply it after testing standby. Some
+# standby settings should not be a lower setting than on the primary.
+$node_p->append_conf('postgresql.conf', q{
+wal_level = logical
+max_replication_slots = 10
+max_wal_senders = 10
+max_worker_processes = 8
+});
+
+# Check some unmet conditions on node S
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 1
+max_logical_replication_workers = 1
+max_worker_processes = 2
+});
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'standby contains unmet conditions on node S');
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 10
+max_logical_replication_workers = 4
+max_worker_processes = 8
+});
+# Restore default settings on both servers
+$node_s->restart;
+$node_p->restart;
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.43.0

v28-0002-Use-last-replication-slot-position-as-replicatio.patchapplication/octet-stream; name=v28-0002-Use-last-replication-slot-position-as-replicatio.patchDownload
From 53cb05445854f186841052c8ab58798ae1058e51 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Wed, 6 Mar 2024 17:50:01 -0300
Subject: [PATCH v28 2/4] Use last replication slot position as replication
 start point

Instead of using a temporary replication slot, use the last
replication slot position (LSN).
---
 doc/src/sgml/ref/pg_createsubscriber.sgml   | 17 ++----
 src/bin/pg_basebackup/pg_createsubscriber.c | 60 ++++++++++-----------
 2 files changed, 33 insertions(+), 44 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 8ca9de5119..a889cea386 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -350,19 +350,10 @@ PostgreSQL documentation
      <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
      database <parameter>oid</parameter>, PID <parameter>int</parameter>).
      These replication slots will be used by the subscriptions in a future step.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Create a temporary replication slot to get a consistent start location.
-     This replication slot has the following name pattern:
-     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
-     (parameter: PID <parameter>int</parameter>). The LSN returned by
-     <link linkend="pg-create-logical-replication-slot"><function>pg_create_logical_replication_slot()</function></link>
-     is used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
-     parameter and by the subscriptions as a replication start point. It
-     guarantees that no transaction will be lost.
+     The last replication slot LSN is used as a stopping point in the
+     <xref linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication start point. It guarantees that no
+     transaction will be lost.
     </para>
    </step>
 
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 56641f583c..2a0cd47a8d 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -73,11 +73,12 @@ static void modify_subscriber_sysid(const char *pg_resetwal_path,
 									struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static void check_publisher(struct LogicalRepInfo *dbinfo);
-static void setup_publisher(struct LogicalRepInfo *dbinfo);
+static char *setup_publisher(struct LogicalRepInfo *dbinfo);
 static void check_subscriber(struct LogicalRepInfo *dbinfo);
 static void setup_subscriber(struct LogicalRepInfo *dbinfo,
 							 const char *consistent_lsn);
-static char *setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir);
+static void setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir,
+						   char *lsn);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  char *slotname);
 static char *create_logical_replication_slot(PGconn *conn,
@@ -570,11 +571,15 @@ modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOpt
 
 /*
  * Create the publications and replication slots in preparation for logical
- * replication.
+ * replication. Returns the LSN from latest replication slot. It will be the
+ * replication start point that is used to adjust the subscriptions (see
+ * set_replication_progress).
  */
-static void
+static char *
 setup_publisher(struct LogicalRepInfo *dbinfo)
 {
+	char	   *lsn = NULL;
+
 	for (int i = 0; i < num_dbs; i++)
 	{
 		PGconn	   *conn;
@@ -637,8 +642,10 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		dbinfo[i].subname = pg_strdup(replslotname);
 
 		/* Create replication slot on publisher */
-		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
-			dry_run)
+		if (lsn)
+			pg_free(lsn);
+		lsn = create_logical_replication_slot(conn, &dbinfo[i], false);
+		if (lsn != NULL || dry_run)
 			pg_log_info("create replication slot \"%s\" on publisher",
 						replslotname);
 		else
@@ -646,6 +653,8 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 
 		disconnect_database(conn, false);
 	}
+
+	return lsn;
 }
 
 /*
@@ -1012,14 +1021,11 @@ setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
 }
 
 /*
- * Get a replication start location and write the required recovery parameters
- * into the configuration file. Returns the replication start location that is
- * used to adjust the subscriptions (see set_replication_progress).
+ * Write the required recovery parameters.
  */
-static char *
-setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
+static void
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir, char *lsn)
 {
-	char	   *consistent_lsn;
 	PGconn	   *conn;
 	PQExpBuffer recoveryconfcontents;
 
@@ -1033,15 +1039,12 @@ setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
 	/*
 	 * Write recovery parameters.
 	 *
-	 * Despite of the recovery parameters will be written to the subscriber,
-	 * use a publisher connection for the following recovery functions. The
-	 * connection is only used to check the current server version (physical
-	 * replica, same server version). The subscriber is not running yet. In
-	 * dry run mode, the recovery parameters *won't* be written. An invalid
-	 * LSN is used for printing purposes. Additional recovery parameters are
-	 * added here. It avoids unexpected behavior such as end of recovery as
-	 * soon as a consistent state is reached (recovery_target) and failure due
-	 * to multiple recovery targets (name, time, xid, LSN).
+	 * The subscriber is not running yet. In dry run mode, the recovery
+	 * parameters *won't* be written. An invalid LSN is used for printing
+	 * purposes. Additional recovery parameters are added here. It avoids
+	 * unexpected behavior such as end of recovery as soon as a consistent
+	 * state is reached (recovery_target) and failure due to multiple recovery
+	 * targets (name, time, xid, LSN).
 	 */
 	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
@@ -1065,14 +1068,12 @@ setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
 	else
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
-						  consistent_lsn);
+						  lsn);
 		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
 
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
-
-	return consistent_lsn;
 }
 
 /*
@@ -1947,13 +1948,10 @@ main(int argc, char **argv)
 	 * primary slot is in use. We could use an extra connection for it but it
 	 * doesn't seem worth.
 	 */
-	setup_publisher(dbinfo);
+	consistent_lsn = setup_publisher(dbinfo);
 
-	/*
-	 * Get the replication start point and write the required recovery
-	 * parameters into the configuration file.
-	 */
-	consistent_lsn = setup_recovery(dbinfo, opt.subscriber_dir);
+	/* Write the required recovery parameters */
+	setup_recovery(dbinfo, opt.subscriber_dir, consistent_lsn);
 
 	/*
 	 * Restart subscriber so the recovery parameters will take effect. Wait
@@ -1969,7 +1967,7 @@ main(int argc, char **argv)
 	/*
 	 * Create the subscription for each database on subscriber. It does not
 	 * enable it immediately because it needs to adjust the replication start
-	 * point to the LSN reported by setup_recovery().  It also cleans up
+	 * point to the LSN reported by setup_publisher().  It also cleans up
 	 * publications created by this tool and replication to the standby.
 	 */
 	setup_subscriber(dbinfo, consistent_lsn);
-- 
2.43.0

v28-0003-port-replace-int-with-string.patchapplication/octet-stream; name=v28-0003-port-replace-int-with-string.patchDownload
From dd9e4c8955adc98bce1dd11d0e89e020a803c9d6 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Wed, 6 Mar 2024 22:00:23 -0300
Subject: [PATCH v28 3/4] port: replace int with string

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 2a0cd47a8d..052153e009 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -28,7 +28,7 @@
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
 
-#define	DEFAULT_SUB_PORT	50432
+#define	DEFAULT_SUB_PORT	"50432"
 #define	BASE_OUTPUT_DIR		"pg_createsubscriber_output.d"
 
 /* Command-line options */
@@ -37,7 +37,7 @@ struct CreateSubscriberOptions
 	char	   *subscriber_dir; /* standby/subscriber data directory */
 	char	   *pub_conninfo_str;	/* publisher connection string */
 	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
-	unsigned short sub_port;	/* subscriber port number */
+	char	   *sub_port;		/* subscriber port number */
 	const char *sub_username;	/* subscriber username */
 	SimpleStringList database_names;	/* list of database names */
 	int			recovery_timeout;	/* stop recovery after this time */
@@ -197,7 +197,7 @@ usage(void)
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
 	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
-	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %d)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %s)\n"), DEFAULT_SUB_PORT);
 	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
 	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
@@ -1260,7 +1260,7 @@ start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_pat
 	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
 					  pg_ctl_path, opt->subscriber_dir);
 	if (with_options)
-		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %u%s\"",
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %s%s\"",
 						  opt->sub_port, socket_string);
 	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
 	rc = system(pg_ctl_cmd->data);
@@ -1704,7 +1704,8 @@ main(int argc, char **argv)
 	opt.subscriber_dir = NULL;
 	opt.pub_conninfo_str = NULL;
 	opt.socket_dir = NULL;
-	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_port = palloc(16);
+	strcpy(opt.sub_port, DEFAULT_SUB_PORT);
 	opt.sub_username = NULL;
 	opt.database_names = (SimpleStringList)
 	{
@@ -1749,8 +1750,8 @@ main(int argc, char **argv)
 				dry_run = true;
 				break;
 			case 'p':
-				if ((opt.sub_port = atoi(optarg)) <= 0)
-					pg_fatal("invalid subscriber port number");
+				pg_free(opt.sub_port);
+				opt.sub_port = pg_strdup(optarg);
 				break;
 			case 'P':
 				opt.pub_conninfo_str = pg_strdup(optarg);
@@ -1848,7 +1849,7 @@ main(int argc, char **argv)
 
 	pg_log_info("validating connection string on subscriber");
 #if !defined(WIN32)
-	sub_base_conninfo = psprintf("host=%s port=%u user=%s fallback_application_name=%s",
+	sub_base_conninfo = psprintf("host=%s port=%s user=%s fallback_application_name=%s",
 								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
 #else							/* WIN32 */
 	sub_base_conninfo = psprintf("port=%u user=%s fallback_application_name=%s",
-- 
2.43.0

v28-0004-fix.patchapplication/octet-stream; name=v28-0004-fix.patchDownload
From 9ca8775325bb751f86551f1b60ffdf4e0ce34a75 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 13 Mar 2024 07:30:05 +0000
Subject: [PATCH v28 4/4] fix

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 052153e009..c79ed9044c 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1852,7 +1852,7 @@ main(int argc, char **argv)
 	sub_base_conninfo = psprintf("host=%s port=%s user=%s fallback_application_name=%s",
 								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
 #else							/* WIN32 */
-	sub_base_conninfo = psprintf("port=%u user=%s fallback_application_name=%s",
+	sub_base_conninfo = psprintf("port=%s user=%s fallback_application_name=%s",
 								 opt.sub_port, opt.sub_username, progname);
 #endif
 
-- 
2.43.0

#193Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Euler Taveira (#191)
5 attachment(s)
Re: speed up a logical replica setup

Hi,

Currently when the pg_createsubscriber error out and exit in between
execution, the standby server started by the pg_createsubscriber may
still be running. I think this standby server should be stopped at
exit of pg_createsubscriber(when it errors out), as this server is
started by pg_createsubscriber and it may be running with
configurations that may not be desired by the user (such as " -c
listen_addresses='' -c unix_socket_permissions=0700"). Thoughts?

Added a top-up patch v28-0005 to fix this issue.
I am not changing the version as v28-0001 to v28-0004 is the same as above.

Thanks and regards,
Shlok Kyal

Attachments:

v28-0001-pg_createsubscriber-creates-a-new-logical-replic.patchapplication/octet-stream; name=v28-0001-pg_createsubscriber-creates-a-new-logical-replic.patchDownload
From 769b072428c672c3c354b56a14ab2601d169949d Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v28 1/4] pg_createsubscriber: creates a new logical replica
 from a standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber). All tables
in the specified database(s) are included in the logical replication
setup. A pair of publication and subscription objects are created for
each database.

The main advantage of pg_createsubscriber over the common logical
replication setup is the initial data copy. It also reduces the catchup
phase.

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
using FOR ALL TABLES and a replication slot for each specified database.
Write recovery parameters into the target data directory and start the
target server. It specifies the latest replication slot LSN up to which
the recovery will proceed. Wait until the target server is promoted.
Create one subscription per specified database (using replication slot
and publication created in a previous step) on the target server. Set
the replication progress to the latest replication slot LSN (replication
start point) for each subscription. Enable the subscription for each
specified database on the target server. And finally, change the system
identifier on the target server.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  472 ++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2001 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  267 +++
 8 files changed, 2770 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..8ca9de5119
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,472 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. All tables in the specified
+    database are included in the logical replication setup. A pair of
+    publication and subscription objects are created for each database. It
+    must be run at the target server.
+  </para>
+
+  <para>
+    After a successful run, the state of the target server is analagous to a
+    fresh logical replication setup. The main difference between the logical
+    replication setup and <application>pg_createsubscriber</application>
+    is the initial data copy. Only the synchronization phase is done, which
+    ensures each table is brought up to a synchronized state.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because most of the execution time is spent making the initial data
+   copy. For smaller databases,
+   <link linkend="logical-replication">initial data synchronization</link> is
+   recommended.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <para>
+    Other options are also available:
+
+    <variablelist>
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   There are some prerequisites for
+   <application>pg_createsubscriber</application> to convert the target server
+   into a logical replica. If these are not met an error will be reported. The
+   source and target servers must have the same major version as the
+   <application>pg_createsubscriber</application>. The given target data
+   directory must have the same system identifier than the source data
+   directory. If a standby server is running on the target data directory or it
+   is a base backup from the source data directory, system identifiers are the
+   same. The given database user for the target data directory must have
+   privileges for creating
+   <link linkend="sql-createsubscription">subscriptions</link> and using
+   <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+  </para>
+
+  <para>
+   The target server must be used as a physical standby. The target server
+   must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   and
+   <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases. The target server must have
+   <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+   configured to a value greater than the number of specified databases. The
+   target server must accept local connections.
+  </para>
+
+  <para>
+   The source server must accept connections from the target server. The
+   source server must not be in recovery. Publications cannot be created in a
+   read-only cluster. The source server must have
+   <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+   <literal>logical</literal>.
+   The source server must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   configured to a value greater than the number of specified databases plus
+   existing replication slots. The source server must have
+   <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases and existing <literal>walsender</literal> processes.
+  </para>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    then the data directory is likely not in a state that can be recovered. It
+    is true if the target server was promoted. In such a case, creating a new
+    standby server is recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during the transformation steps.
+    Hence, connections to target server might fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Executing DDL commands on the source server while running
+    <application>pg_createsubscriber</application> is not recommended. If the
+    target server has already been converted to logical replica, the DDL
+    commands must not be replicated so an error would occur.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    should be removed. The removal might fail if the target server cannot
+    connect to the source server. In such a case, a warning message will inform
+    the objects left.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which WAL files from the source server might be used by the target server.
+    If the target server has a standby, replication will break and a fresh
+    standby should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is already running, stop it because some parameters can only
+     be set at server start.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. If any of the prerequisites are not met,
+     <application>pg_createsubscriber</application> will terminate with an
+     error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication is created using
+     <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>.
+     It has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u</literal></quote> (parameter:
+     database <parameter>oid</parameter>). Each replication slot has the
+     following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a temporary replication slot to get a consistent start location.
+     This replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
+     (parameter: PID <parameter>int</parameter>). The LSN returned by
+     <link linkend="pg-create-logical-replication-slot"><function>pg_create_logical_replication_slot()</function></link>
+     is used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
+     parameter and by the subscriptions as a replication start point. It
+     guarantees that no transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies a LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are also added so it avoids unexpected behavior during the recovery
+     process such as end of the recovery as soon as a consistent state is
+     reached (WAL should be applied until the consistent start location) and
+     multiple recovery targets that can cause a failure. This step finishes
+     once the server ends standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     The subscription has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
+     database <parameter>oid</parameter>, PID <parameter>int</parameter>). The
+     replication slot name is identical to the subscription name. It does not
+     copy existing data from the source server. It does not create a
+     replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the consistent LSN
+     but replication origin name contains the subscription oid in its name.
+     Hence, the subscription will be enabled in a separate step.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the consistent start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the consistent start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the consistent start point. This is the exact LSN to be used
+     as a initial location for each subscription. The replication origin name
+     is obtained since the subscription was created. The replication origin
+     name and the consistent start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the consistent start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..e9a920dbcd 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..56641f583c
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2001 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/restricted_token.h"
+#include "common/username.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	50432
+#define	BASE_OUTPUT_DIR		"pg_createsubscriber_output.d"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *subscriber_dir; /* standby/subscriber data directory */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	unsigned short sub_port;	/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	int			recovery_timeout;	/* stop recovery after this time */
+}			CreateSubscriberOptions;
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name / replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+}			LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const char *pg_resetwal_path,
+									struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static void check_publisher(struct LogicalRepInfo *dbinfo);
+static void setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static char *setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir);
+static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
+										  char *slotname);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo,
+											 bool temporary);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(struct CreateSubscriberOptions *opt,
+								 const char *pg_ctl_path, bool with_options);
+static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
+static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+								  struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static bool recovery_ended = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("pg_createsubscriber failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.");
+		pg_log_warning_hint("You must recreate the physical replica before continuing.");
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			PGconn	   *conn;
+
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].subname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %d)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	int			i = 0;
+
+	dbinfo = (struct LogicalRepInfo *) pg_malloc(num_dbs * sizeof(struct LogicalRepInfo));
+
+	for (SimpleStringListCell *cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): connection string: %s", i, dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): connection string: %s", i, dbinfo[i].subconninfo);
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(opt->subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(opt->subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   opt->subscriber_dir, DEVNULL);
+
+	pg_log_debug("command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication.
+ */
+static void
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+
+		/* Remember database OID */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 31 characters (20 + 10 +
+		 * '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u",
+				 dbinfo[i].oid);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 42
+		 * characters (20 + 10 + 1 + 10 + '\0'). PID is included to reduce the
+		 * probability of collision. By default, subscription name is used as
+		 * replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%d",
+				 dbinfo[i].oid,
+				 (int) getpid());
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher */
+		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
+			dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ *
+ * XXX Does it disallow a synchronous replica?
+ */
+static void
+check_publisher(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	bool		failed = false;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted +
+	 *                            one temporary logical replication slot
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		failed = true;
+	}
+
+	/* One additional temporary logical replication slot */
+	if (max_repslots - cur_repslots < num_dbs + 1)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs + 1, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs + 1);
+		failed = true;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ *
+ * XXX Does it disallow a time-delayed replica?
+ *
+ * XXX In a cascaded replication scenario (P -> S -> C), if the target server
+ * is S, it cannot detect there is a replica (server C) because server S starts
+ * accepting only local connections and server C cannot connect to it. Hence,
+ * there is not reliable way to provide a suitable error saying the server C
+ * will be broken at the end of this process (due to pg_resetwal).
+ */
+static void
+check_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+	bool		failed = false;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("The target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		failed = true;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * replication setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Get a replication start location and write the required recovery parameters
+ * into the configuration file. Returns the replication start location that is
+ * used to adjust the subscriptions (see set_replication_progress).
+ */
+static char *
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
+{
+	char	   *consistent_lsn;
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	/*
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection. The primary_conninfo is generated using the
+	 * connection settings.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection for the following recovery functions. The
+	 * connection is only used to check the current server version (physical
+	 * replica, same server version). The subscriber is not running yet. In
+	 * dry run mode, the recovery parameters *won't* be written. An invalid
+	 * LSN is used for printing purposes. Additional recovery parameters are
+	 * added here. It avoids unexpected behavior such as end of recovery as
+	 * soon as a consistent state is reached (recovery_target) and failure due
+	 * to multiple recovery targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  consistent_lsn);
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	return consistent_lsn;
+}
+
+/*
+ * Drop physical replication slot on primary if the standby was using it. After
+ * the transformation, it has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, char *slotname)
+{
+	PGconn	   *conn;
+
+	/* Replication slot does not exist, do nothing */
+	if (!primary_slot_name)
+		return;
+
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn != NULL)
+	{
+		drop_replication_slot(conn, &dbinfo[0], slotname);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop replication slot \"%s\" on primary",
+					   slotname);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								bool temporary)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	/* This temporary replication slot is only used for catchup purposes */
+	if (temporary)
+	{
+		snprintf(slot_name, NAMEDATALEN, "pg_createsubscriber_%d_startpoint",
+				 (int) getpid());
+	}
+	else
+		snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", temporary ? "true" : "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	if (!temporary)
+		dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_path,
+					 bool with_options)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	char		socket_string[MAXPGPATH + 200];
+	int			rc;
+
+	socket_string[0] = '\0';
+
+#if !defined(WIN32)
+
+	/*
+	 * An empty listen_addresses list means the server does not listen on any
+	 * IP interfaces; only Unix-domain sockets can be used to connect to the
+	 * server. Prevent external connections to minimize the chance of failure.
+	 */
+	strcat(socket_string,
+		   " -c listen_addresses='' -c unix_socket_permissions=0700");
+
+	if (opt->socket_dir)
+		snprintf(socket_string + strlen(socket_string),
+				 sizeof(socket_string) - strlen(socket_string),
+				 " -c unix_socket_directories='%s'",
+				 opt->socket_dir);
+#endif
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, opt->subscriber_dir);
+	if (with_options)
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %u%s\"",
+						  opt->sub_port, socket_string);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *pg_ctl_path, const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+					  struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	5
+
+	pg_log_info("waiting for the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the temporary
+ * replication slot. The goal is to set up the initial location for the logical
+ * replication that is the exact LSN that the subscriber was promoted. Once the
+ * subscription is enabled it will start streaming from that location onwards.
+ * In dry run mode, the subscription OID and LSN are set to invalid values for
+ * printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pg_ctl_path = NULL;
+	char	   *pg_resetwal_path = NULL;
+
+	char	   *pub_base_conninfo;
+	char	   *sub_base_conninfo;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	char	   *consistent_lsn;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	opt.subscriber_dir = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:np:P:s:t:U:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'D':
+				opt.subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(opt.subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				if ((opt.sub_port = atoi(optarg)) <= 0)
+					pg_fatal("invalid subscriber port number");
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				canonicalize_path(opt.socket_dir);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/* Any non-option arguments? */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* Required arguments */
+	if (opt.subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* If socket directory is not provided, use the current directory */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 * If subscriber username is not provided, check if the environment
+	 * variable sets it. If not, obtain the operating system name of the user
+	 * running it.
+	 */
+	if (opt.sub_username == NULL)
+	{
+		if (getenv("PGUSER"))
+		{
+			opt.sub_username = getenv("PGUSER");
+		}
+		else
+		{
+			char	   *errstr = NULL;
+
+			opt.sub_username = get_user_name(&errstr);
+			if (errstr)
+				pg_fatal("%s", errstr);
+		}
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+#if !defined(WIN32)
+	sub_base_conninfo = psprintf("host=%s port=%u user=%s fallback_application_name=%s",
+								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
+#else							/* WIN32 */
+	sub_base_conninfo = psprintf("port=%u user=%s fallback_application_name=%s",
+								 opt.sub_port, opt.sub_username, progname);
+#endif
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(opt.subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(opt.subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", opt.subscriber_dir);
+
+	/*
+	 * If the standby server is running, stop it. Some parameters (that can
+	 * only be set at server start) are informed by command-line options.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, pg_ctl_path, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	setup_publisher(dbinfo);
+
+	/*
+	 * Get the replication start point and write the required recovery
+	 * parameters into the configuration file.
+	 */
+	consistent_lsn = setup_recovery(dbinfo, opt.subscriber_dir);
+
+	/*
+	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("stopping and starting the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	start_standby_server(&opt, pg_ctl_path, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the replication start
+	 * point to the LSN reported by setup_recovery().  It also cleans up
+	 * publications created by this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/* Remove primary_slot_name if it exists on primary */
+	drop_primary_replication_slot(dbinfo, primary_slot_name);
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(pg_resetwal_path, &opt);
+
+	/*
+	 * In dry run mode, the server is restarted with the provided command-line
+	 * options so validation can be applied in the target server. In order to
+	 * preserve the initial state of the server (running), start it without
+	 * the command-line options.
+	 */
+	if (dry_run)
+		start_standby_server(&opt, pg_ctl_path, false);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..3bc4f3dc18
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,267 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Stop node C
+$node_c->teardown_node;
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Check some unmet conditions on node P
+$node_p->append_conf('postgresql.conf', q{
+wal_level = replica
+max_replication_slots = 1
+max_wal_senders = 1
+max_worker_processes = 2
+});
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary contains unmet conditions on node P');
+# Restore default settings here but only apply it after testing standby. Some
+# standby settings should not be a lower setting than on the primary.
+$node_p->append_conf('postgresql.conf', q{
+wal_level = logical
+max_replication_slots = 10
+max_wal_senders = 10
+max_worker_processes = 8
+});
+
+# Check some unmet conditions on node S
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 1
+max_logical_replication_workers = 1
+max_worker_processes = 2
+});
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'standby contains unmet conditions on node S');
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 10
+max_logical_replication_workers = 4
+max_worker_processes = 8
+});
+# Restore default settings on both servers
+$node_s->restart;
+$node_p->restart;
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.43.0

v28-0002-Use-last-replication-slot-position-as-replicatio.patchapplication/octet-stream; name=v28-0002-Use-last-replication-slot-position-as-replicatio.patchDownload
From 53cb05445854f186841052c8ab58798ae1058e51 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Wed, 6 Mar 2024 17:50:01 -0300
Subject: [PATCH v28 2/4] Use last replication slot position as replication
 start point

Instead of using a temporary replication slot, use the last
replication slot position (LSN).
---
 doc/src/sgml/ref/pg_createsubscriber.sgml   | 17 ++----
 src/bin/pg_basebackup/pg_createsubscriber.c | 60 ++++++++++-----------
 2 files changed, 33 insertions(+), 44 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 8ca9de5119..a889cea386 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -350,19 +350,10 @@ PostgreSQL documentation
      <quote><literal>pg_createsubscriber_%u_%d</literal></quote> (parameters:
      database <parameter>oid</parameter>, PID <parameter>int</parameter>).
      These replication slots will be used by the subscriptions in a future step.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Create a temporary replication slot to get a consistent start location.
-     This replication slot has the following name pattern:
-     <quote><literal>pg_createsubscriber_%d_startpoint</literal></quote>
-     (parameter: PID <parameter>int</parameter>). The LSN returned by
-     <link linkend="pg-create-logical-replication-slot"><function>pg_create_logical_replication_slot()</function></link>
-     is used as a stopping point in the <xref linkend="guc-recovery-target-lsn"/>
-     parameter and by the subscriptions as a replication start point. It
-     guarantees that no transaction will be lost.
+     The last replication slot LSN is used as a stopping point in the
+     <xref linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication start point. It guarantees that no
+     transaction will be lost.
     </para>
    </step>
 
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 56641f583c..2a0cd47a8d 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -73,11 +73,12 @@ static void modify_subscriber_sysid(const char *pg_resetwal_path,
 									struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static void check_publisher(struct LogicalRepInfo *dbinfo);
-static void setup_publisher(struct LogicalRepInfo *dbinfo);
+static char *setup_publisher(struct LogicalRepInfo *dbinfo);
 static void check_subscriber(struct LogicalRepInfo *dbinfo);
 static void setup_subscriber(struct LogicalRepInfo *dbinfo,
 							 const char *consistent_lsn);
-static char *setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir);
+static void setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir,
+						   char *lsn);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  char *slotname);
 static char *create_logical_replication_slot(PGconn *conn,
@@ -570,11 +571,15 @@ modify_subscriber_sysid(const char *pg_resetwal_path, struct CreateSubscriberOpt
 
 /*
  * Create the publications and replication slots in preparation for logical
- * replication.
+ * replication. Returns the LSN from latest replication slot. It will be the
+ * replication start point that is used to adjust the subscriptions (see
+ * set_replication_progress).
  */
-static void
+static char *
 setup_publisher(struct LogicalRepInfo *dbinfo)
 {
+	char	   *lsn = NULL;
+
 	for (int i = 0; i < num_dbs; i++)
 	{
 		PGconn	   *conn;
@@ -637,8 +642,10 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		dbinfo[i].subname = pg_strdup(replslotname);
 
 		/* Create replication slot on publisher */
-		if (create_logical_replication_slot(conn, &dbinfo[i], false) != NULL ||
-			dry_run)
+		if (lsn)
+			pg_free(lsn);
+		lsn = create_logical_replication_slot(conn, &dbinfo[i], false);
+		if (lsn != NULL || dry_run)
 			pg_log_info("create replication slot \"%s\" on publisher",
 						replslotname);
 		else
@@ -646,6 +653,8 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 
 		disconnect_database(conn, false);
 	}
+
+	return lsn;
 }
 
 /*
@@ -1012,14 +1021,11 @@ setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
 }
 
 /*
- * Get a replication start location and write the required recovery parameters
- * into the configuration file. Returns the replication start location that is
- * used to adjust the subscriptions (see set_replication_progress).
+ * Write the required recovery parameters.
  */
-static char *
-setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
+static void
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir, char *lsn)
 {
-	char	   *consistent_lsn;
 	PGconn	   *conn;
 	PQExpBuffer recoveryconfcontents;
 
@@ -1033,15 +1039,12 @@ setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
 	/*
 	 * Write recovery parameters.
 	 *
-	 * Despite of the recovery parameters will be written to the subscriber,
-	 * use a publisher connection for the following recovery functions. The
-	 * connection is only used to check the current server version (physical
-	 * replica, same server version). The subscriber is not running yet. In
-	 * dry run mode, the recovery parameters *won't* be written. An invalid
-	 * LSN is used for printing purposes. Additional recovery parameters are
-	 * added here. It avoids unexpected behavior such as end of recovery as
-	 * soon as a consistent state is reached (recovery_target) and failure due
-	 * to multiple recovery targets (name, time, xid, LSN).
+	 * The subscriber is not running yet. In dry run mode, the recovery
+	 * parameters *won't* be written. An invalid LSN is used for printing
+	 * purposes. Additional recovery parameters are added here. It avoids
+	 * unexpected behavior such as end of recovery as soon as a consistent
+	 * state is reached (recovery_target) and failure due to multiple recovery
+	 * targets (name, time, xid, LSN).
 	 */
 	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
@@ -1065,14 +1068,12 @@ setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir)
 	else
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
-						  consistent_lsn);
+						  lsn);
 		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
 
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
-
-	return consistent_lsn;
 }
 
 /*
@@ -1947,13 +1948,10 @@ main(int argc, char **argv)
 	 * primary slot is in use. We could use an extra connection for it but it
 	 * doesn't seem worth.
 	 */
-	setup_publisher(dbinfo);
+	consistent_lsn = setup_publisher(dbinfo);
 
-	/*
-	 * Get the replication start point and write the required recovery
-	 * parameters into the configuration file.
-	 */
-	consistent_lsn = setup_recovery(dbinfo, opt.subscriber_dir);
+	/* Write the required recovery parameters */
+	setup_recovery(dbinfo, opt.subscriber_dir, consistent_lsn);
 
 	/*
 	 * Restart subscriber so the recovery parameters will take effect. Wait
@@ -1969,7 +1967,7 @@ main(int argc, char **argv)
 	/*
 	 * Create the subscription for each database on subscriber. It does not
 	 * enable it immediately because it needs to adjust the replication start
-	 * point to the LSN reported by setup_recovery().  It also cleans up
+	 * point to the LSN reported by setup_publisher().  It also cleans up
 	 * publications created by this tool and replication to the standby.
 	 */
 	setup_subscriber(dbinfo, consistent_lsn);
-- 
2.43.0

v28-0003-port-replace-int-with-string.patchapplication/octet-stream; name=v28-0003-port-replace-int-with-string.patchDownload
From dd9e4c8955adc98bce1dd11d0e89e020a803c9d6 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Wed, 6 Mar 2024 22:00:23 -0300
Subject: [PATCH v28 3/4] port: replace int with string

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 2a0cd47a8d..052153e009 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -28,7 +28,7 @@
 #include "fe_utils/simple_list.h"
 #include "getopt_long.h"
 
-#define	DEFAULT_SUB_PORT	50432
+#define	DEFAULT_SUB_PORT	"50432"
 #define	BASE_OUTPUT_DIR		"pg_createsubscriber_output.d"
 
 /* Command-line options */
@@ -37,7 +37,7 @@ struct CreateSubscriberOptions
 	char	   *subscriber_dir; /* standby/subscriber data directory */
 	char	   *pub_conninfo_str;	/* publisher connection string */
 	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
-	unsigned short sub_port;	/* subscriber port number */
+	char	   *sub_port;		/* subscriber port number */
 	const char *sub_username;	/* subscriber username */
 	SimpleStringList database_names;	/* list of database names */
 	int			recovery_timeout;	/* stop recovery after this time */
@@ -197,7 +197,7 @@ usage(void)
 	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
 	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
 	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
-	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %d)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %s)\n"), DEFAULT_SUB_PORT);
 	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
 	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
 	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
@@ -1260,7 +1260,7 @@ start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_pat
 	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
 					  pg_ctl_path, opt->subscriber_dir);
 	if (with_options)
-		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %u%s\"",
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %s%s\"",
 						  opt->sub_port, socket_string);
 	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
 	rc = system(pg_ctl_cmd->data);
@@ -1704,7 +1704,8 @@ main(int argc, char **argv)
 	opt.subscriber_dir = NULL;
 	opt.pub_conninfo_str = NULL;
 	opt.socket_dir = NULL;
-	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_port = palloc(16);
+	strcpy(opt.sub_port, DEFAULT_SUB_PORT);
 	opt.sub_username = NULL;
 	opt.database_names = (SimpleStringList)
 	{
@@ -1749,8 +1750,8 @@ main(int argc, char **argv)
 				dry_run = true;
 				break;
 			case 'p':
-				if ((opt.sub_port = atoi(optarg)) <= 0)
-					pg_fatal("invalid subscriber port number");
+				pg_free(opt.sub_port);
+				opt.sub_port = pg_strdup(optarg);
 				break;
 			case 'P':
 				opt.pub_conninfo_str = pg_strdup(optarg);
@@ -1848,7 +1849,7 @@ main(int argc, char **argv)
 
 	pg_log_info("validating connection string on subscriber");
 #if !defined(WIN32)
-	sub_base_conninfo = psprintf("host=%s port=%u user=%s fallback_application_name=%s",
+	sub_base_conninfo = psprintf("host=%s port=%s user=%s fallback_application_name=%s",
 								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
 #else							/* WIN32 */
 	sub_base_conninfo = psprintf("port=%u user=%s fallback_application_name=%s",
-- 
2.43.0

v28-0004-fix.patchapplication/octet-stream; name=v28-0004-fix.patchDownload
From 9ca8775325bb751f86551f1b60ffdf4e0ce34a75 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 13 Mar 2024 07:30:05 +0000
Subject: [PATCH v28 4/4] fix

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 052153e009..c79ed9044c 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1852,7 +1852,7 @@ main(int argc, char **argv)
 	sub_base_conninfo = psprintf("host=%s port=%s user=%s fallback_application_name=%s",
 								 opt.socket_dir, opt.sub_port, opt.sub_username, progname);
 #else							/* WIN32 */
-	sub_base_conninfo = psprintf("port=%u user=%s fallback_application_name=%s",
+	sub_base_conninfo = psprintf("port=%s user=%s fallback_application_name=%s",
 								 opt.sub_port, opt.sub_username, progname);
 #endif
 
-- 
2.43.0

v28-0005-Stop-standby-server-if-started-by-pg_createsubsc.patchapplication/octet-stream; name=v28-0005-Stop-standby-server-if-started-by-pg_createsubsc.patchDownload
From b99e4a04432aad2b885db60c8bc390bde79625ab Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 13 Mar 2024 16:52:28 +0530
Subject: [PATCH v28 5/5] Stop standby server if started by pg_createsubscriber

Stop standby server if it is started in pg_createsubscriber
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 43 +++++++++++--------
 .../t/040_pg_createsubscriber.pl              |  3 --
 2 files changed, 26 insertions(+), 20 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index c79ed9044c..7d78f4bac3 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -88,9 +88,9 @@ static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
 								  const char *slot_name);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
 static void start_standby_server(struct CreateSubscriberOptions *opt,
-								 const char *pg_ctl_path, bool with_options);
-static void stop_standby_server(const char *pg_ctl_path, const char *datadir);
-static void wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+								 bool with_options);
+static void stop_standby_server(const char *datadir);
+static void wait_for_end_recovery(const char *conninfo,
 								  struct CreateSubscriberOptions *opt);
 static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
@@ -114,6 +114,10 @@ static int	num_dbs = 0;
 
 static bool recovery_ended = false;
 
+static char *data_dir = NULL;
+static char *pg_ctl_path = NULL;
+static bool standby_server_started = false;
+
 enum WaitPMResult
 {
 	POSTMASTER_READY,
@@ -184,6 +188,10 @@ cleanup_objects_atexit(void)
 			}
 		}
 	}
+
+	/* stop the standby server if it is started in pg_createsubscriber */
+	if (standby_server_started)
+		stop_standby_server(data_dir);
 }
 
 static void
@@ -1231,8 +1239,7 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc)
 }
 
 static void
-start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_path,
-					 bool with_options)
+start_standby_server(struct CreateSubscriberOptions *opt, bool with_options)
 {
 	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
 	char		socket_string[MAXPGPATH + 200];
@@ -1267,10 +1274,11 @@ start_standby_server(struct CreateSubscriberOptions *opt, const char *pg_ctl_pat
 	pg_ctl_status(pg_ctl_cmd->data, rc);
 	destroyPQExpBuffer(pg_ctl_cmd);
 	pg_log_info("server was started");
+	standby_server_started = true;
 }
 
 static void
-stop_standby_server(const char *pg_ctl_path, const char *datadir)
+stop_standby_server(const char *datadir)
 {
 	char	   *pg_ctl_cmd;
 	int			rc;
@@ -1281,6 +1289,7 @@ stop_standby_server(const char *pg_ctl_path, const char *datadir)
 	rc = system(pg_ctl_cmd);
 	pg_ctl_status(pg_ctl_cmd, rc);
 	pg_log_info("server was stopped");
+	standby_server_started = false;
 }
 
 /*
@@ -1290,7 +1299,7 @@ stop_standby_server(const char *pg_ctl_path, const char *datadir)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
+wait_for_end_recovery(const char *conninfo,
 					  struct CreateSubscriberOptions *opt)
 {
 	PGconn	   *conn;
@@ -1332,7 +1341,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 		{
 			if (++count > NUM_CONN_ATTEMPTS)
 			{
-				stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+				stop_standby_server(opt->subscriber_dir);
 				pg_log_error("standby server disconnected from the primary");
 				break;
 			}
@@ -1345,7 +1354,7 @@ wait_for_end_recovery(const char *conninfo, const char *pg_ctl_path,
 		/* Bail out after recovery_timeout seconds if this option is set */
 		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
 		{
-			stop_standby_server(pg_ctl_path, opt->subscriber_dir);
+			stop_standby_server(opt->subscriber_dir);
 			pg_log_error("recovery timed out");
 			disconnect_database(conn, true);
 		}
@@ -1665,7 +1674,6 @@ main(int argc, char **argv)
 	int			c;
 	int			option_index;
 
-	char	   *pg_ctl_path = NULL;
 	char	   *pg_resetwal_path = NULL;
 
 	char	   *pub_base_conninfo;
@@ -1745,6 +1753,7 @@ main(int argc, char **argv)
 			case 'D':
 				opt.subscriber_dir = pg_strdup(optarg);
 				canonicalize_path(opt.subscriber_dir);
+				data_dir = opt.subscriber_dir;
 				break;
 			case 'n':
 				dry_run = true;
@@ -1921,7 +1930,7 @@ main(int argc, char **argv)
 
 		pg_log_info("standby is up and running");
 		pg_log_info("stopping the server to start the transformation steps");
-		stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+		stop_standby_server(opt.subscriber_dir);
 	}
 
 	/*
@@ -1930,7 +1939,7 @@ main(int argc, char **argv)
 	 * transformation steps.
 	 */
 	pg_log_info("starting the standby with command-line options");
-	start_standby_server(&opt, pg_ctl_path, true);
+	start_standby_server(&opt, true);
 
 	/* Check if the standby server is ready for logical replication */
 	check_subscriber(dbinfo);
@@ -1959,11 +1968,11 @@ main(int argc, char **argv)
 	 * until accepting connections.
 	 */
 	pg_log_info("stopping and starting the subscriber");
-	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
-	start_standby_server(&opt, pg_ctl_path, true);
+	stop_standby_server(opt.subscriber_dir);
+	start_standby_server(&opt, true);
 
 	/* Waiting the subscriber to be promoted */
-	wait_for_end_recovery(dbinfo[0].subconninfo, pg_ctl_path, &opt);
+	wait_for_end_recovery(dbinfo[0].subconninfo, &opt);
 
 	/*
 	 * Create the subscription for each database on subscriber. It does not
@@ -1978,7 +1987,7 @@ main(int argc, char **argv)
 
 	/* Stop the subscriber */
 	pg_log_info("stopping the subscriber");
-	stop_standby_server(pg_ctl_path, opt.subscriber_dir);
+	stop_standby_server(opt.subscriber_dir);
 
 	/* Change system identifier from subscriber */
 	modify_subscriber_sysid(pg_resetwal_path, &opt);
@@ -1990,7 +1999,7 @@ main(int argc, char **argv)
 	 * the command-line options.
 	 */
 	if (dry_run)
-		start_standby_server(&opt, pg_ctl_path, false);
+		start_standby_server(&opt, false);
 
 	success = true;
 
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 3bc4f3dc18..e7c5ccb577 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -105,9 +105,6 @@ command_fails(
 	],
 	'primary server is in recovery');
 
-# Stop node C
-$node_c->teardown_node;
-
 # Insert another row on node P and wait node S to catch up
 $node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
 $node_p->wait_for_replay_catchup($node_s);
-- 
2.34.1

#194Euler Taveira
euler@eulerto.com
In reply to: Shlok Kyal (#193)
1 attachment(s)
Re: speed up a logical replica setup

On Wed, Mar 13, 2024, at 10:09 AM, Shlok Kyal wrote:

Added a top-up patch v28-0005 to fix this issue.
I am not changing the version as v28-0001 to v28-0004 is the same as above.

Thanks for your review!

I'm posting a new patch (v29) that merges the previous patches (v28-0002 and
v28-0003). I applied the fix provided by Hayato [1]/messages/by-id/TYCPR01MB12077FD21BB186C5A685C0BF3F52A2@TYCPR01MB12077.jpnprd01.prod.outlook.com. It was an oversight during
a rebase. I also included the patch proposed by Shlok [2]/messages/by-id/CANhcyEW6-dH28gLbFc5XpDTJ6JPizU+t5g-aKUWJBf5W_Zriqw@mail.gmail.com that stops the target
server on error if it is running.

Tomas suggested in [3]/messages/by-id/6423dfeb-a729-45d3-b71e-7bf1b3adb0c9@enterprisedb.com that maybe the PID should be replaced with something
else that has more entropy. Instead of PID, it uses a random number for
replication slot and subscription. There is also a concern about converting
multiple standbys that will have the same publication name. It added the same
random number to the publication name so it doesn't fail because the
publication already exists. Documentation was changed based on Tomas feedback.

The user name was always included in the subscriber connection string. Let's
have the libpq to choose it. While on it, a new routine (get_sub_conninfo)
contains the code to build the subscriber connection string.

As I said in [4]/messages/by-id/d898faad-f6d7-4b0d-b816-b9dcdf490685@app.fastmail.com, there wasn't a way to inform a different configuration file.
If your cluster has a postgresql.conf outside PGDATA, when pg_createsubscriber
starts the server it will fail. The new --config-file option let you inform the
postgresql.conf location and the server is started just fine.

I also did some changes in the start_standby_server routine. I replaced the
strcat and snprintf with appendPQExpBuffer that has been used to store the
pg_ctl command.

[1]: /messages/by-id/TYCPR01MB12077FD21BB186C5A685C0BF3F52A2@TYCPR01MB12077.jpnprd01.prod.outlook.com
[2]: /messages/by-id/CANhcyEW6-dH28gLbFc5XpDTJ6JPizU+t5g-aKUWJBf5W_Zriqw@mail.gmail.com
[3]: /messages/by-id/6423dfeb-a729-45d3-b71e-7bf1b3adb0c9@enterprisedb.com
[4]: /messages/by-id/d898faad-f6d7-4b0d-b816-b9dcdf490685@app.fastmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v29-0001-pg_createsubscriber-creates-a-new-logical-replic.patch.gzapplication/gzip; name="=?UTF-8?Q?v29-0001-pg=5Fcreatesubscriber-creates-a-new-logical-replic.pa?= =?UTF-8?Q?tch.gz?="Download
�_��ev29-0001-pg_createsubscriber-creates-a-new-logical-replic.patch�=ks����3��r"�4�I���k-9����Y�sR�=��� ��)Y[����H�1Ok��\I$�F���/4�7i2~��
<�ppt2<q\�29�
�g������
�g��]�[���6���`0���0��:x���.R��8>�2���GO�TH��{^^t����#�.;b�#up�����g��s���s������������������o,��{)��2K/c��3���,��,���s��$�?�'�v����?1���v:o3�2cc�R@&��8��t�3��A&gq���� f^E��	]:2�S��.;I>���t��7�������e�w�,;B� ����>�]��J�#V���� ���nY�j&��#y�'�%�HY<a������FO�EL4�]��g�8�p����:�;-taX�p���r���q��p:�4�GF82!���D&`8tJ�zX�2�y���U3�������F�q�Y���������od�Ag9����X,2
������3�����<����'B�|E4a���%�{��Gvys��.���%j��2�3�/#�6��|Ja*��C:>S��S	tR�f�����^CKb-�H2Y�8��LQ���=2�f���Hvv*on
�8�<����>
�$�=�������G�Z���q�-_���U�L`�6I�v��b� �g����s	S��n�b������JP��4)2�}Y,�ZS���H)�Yun�20�K��DD(�]���h�Ix�L)d�IE@i{���8��^_���i�S>������7���?�c����kY�%�7<9a{���l����*Cb���p$����>�I�7e1�bPWk�{�~�8������3�c�%�q�0��g�l#�G��lm*�:�����>�����I�@����t�)#j������/"`=
������C����gg���c�����x8\��������Q����d�����%�=^�������;<8;;���z�
�������$���#u�)�G{��9���1��_'^���x:Y�N��KR��v3��-Z��^����������Y@@JMC����]�c[HH���uQ�M����Jg���t����8 ����|3]������������`}���@��Y0��<L�u����>���}����X>�*������%�f�%-���_��O�N�x@2���C?�h����v����.*�W���`[�V��!�X����A�������=���h(@^�����r��I�uX(@lD3�(d����-A���Ef����P�XB�!>���
�A.Y�/F0�q	�!�
F�jo��CDyx�?��f�PHOD����Dp��s���8��|n��|4-�<Mb�/`������;{�������������|��D
Y ���yG/�
D����l>������/�����!2��Q^��!Y���Nu�i���f��@������4�AX`��Ak�Fj������@K�8�l�;�*����?F�V��m�_(�5�O�8����>`����P���3�;L��mv'���������D����NJY\�"��-��Q��h-m��:Q����RT�(v��
P����`���I�U�+Xv�����%���k����sCV�u��3�e'�A���;�8���6��r��E��	>��#�V�f�Y�V�I��4?=�s<��>E�,�#���17�}��]�#P�x$s���I�a`ULg�N)�<�+J.dfMyW�,���`4HR�ss�����q��+�r�b&BBd�������:����B�K`��]�}f��G��h6:�.*���{����
��<L�,������d�v����,5��rs����H2%F�8�G �����3@�0%P��1���MJ	B�����L��y	���V���Nf^�J	�����Zm������
���/�s���+�TH���J!SZE���R����L@Y�����(y��5j�1��M���G�y3]K�$Q���k�Z��WK�h������l\s��8i��E�6��	��t<Zb0�
L��f�KxZW|/�D+/�����
�����r6C��_Q��B�rL�b�+&OF��)����,��$N�����Y&�k���"@1�X���l��QTBV��������,f��������8
�h0<< ��!>��yFf���o�h,�?,�yk��^�� ����`���$*�`)�J�;�)�������8����PX��'_�@��_|/��,��K2�>��)��
��y=��`�T\o	8��K�B�'i���u�KP�Z��t�<Mq����N�l��9�#_�F�������z�/U�k���B����������SW�=�b�^���M�L/���5A;RE�sy��+��e<�L7��-�g-c����6�.�p�V�m��@A��Z��T�
�=ZM���R��&4�+y�uI�6�%��h7�D��(�.�����q�g�~|?�)�%LLt(�D�%X]������n�8�(���>�i��������AQ�P��k���1��|���&��$�]+��3��zG������u�m�u�"y����W��S���b�]�&,BK�Uj\zd|JX�Bt%S*������� U�
-3iV7��I[�0��dOqN}�jP�Xk���"Q�b�K�C"����6��	d����A���j�^��+���o0�����!����+�����.1��ge����)�&)
�1�>�X����M���������)��1 ]M`V��������A�z�1��Lf�����'���AEPfY��DqFU�n�����c*E�������@H'��������u/�0��oVp�R��R)��x��]}H����e�fqZ6s�&��G���m3��3��j}[��+����F)S]CY���e�nY��dH��	Qdd���Z�:ViR��P�S%uEI�bLuSL�S�M���'i6P&�8�
R2�w�8S9T������$�<#
V�{��^7���dZ�q���T�<f�
���H6�{�^�J������@�U�$��J�h����27�N��>��tc����Y�<��g��x�ee\��}dc���6�� �#!��76��aB+��l�;-]��
S��z%�MN}�/>o<�9�������Bm4��E!�'If�t��eA)���JT�c�,��Pn`���nS�SFp��;1������<\q�����e����5�����B�-~V/z������4XDF'FR�W/#���0@b����
��w�����4>.
����X�-����?S��'��.I�x�`����&�Y~�a������9N7j�m�f�g�W.']��6�h�,G��L1:�9�S�M�G�>��6���n�M��i��?*p-�Kmg�J�o�a�����S�!����fE�HVI��d�d~��2��2y������ta>�<p+�=�z6E��N!7�M��D�<��8�m��/�F���z��b��KS���q�����	��i�(8s�b����[p�D�`�P���0���M��!B�n~g��&��������o&$XF�[�uAWW7&��Edat���o�4���6H��3*��"���#�W�BW/V��o"�&$��y�-s4�O��:�R���HbU��l�f�D�m��T���$f�fj�v�J7�mZ���e�+���)�S�:R��z�y"��J�%��B�V�i�BeLBT�$��-��e�'�����c�-Y�B5H�lP�%w-[W�w��A�l��F���J��������&~#�4K'�>&9��o�y6k�:1�3,��uW)-\Bo��Ij���J��}�?��S�H��cp
�b{n����n�P@0q�X��@�,����	}�,@��s8s���h�����
�9L�QT����P�d��O�R�h��(,k=Q#;�Y�q���~��>� ���a#UC�k�.�V��	���G'[i��&�9U�6rh�S��
<Y�P�[B?T���=#xv��������h����2F���F�ih����X��hAdE��JN��s�pPDM�����B|{&�Z�d��3\<�����5$F����*�V�9Kf#��&��^�d�i��W+�59�
\��q2~��eo�mBUg�.?p��L��&m���
]�������A��Cr#�����Q��`�LpW=�^}v.�RFZ�R��JO�V��� �%��������?���j����c��@+�5*>]�&VlS�������&���&���f�O�sb��g�����A�5,���Tr�*.��Y��  ;�������L��1:�O�[X^D�]������h�Qj8ZM
W
��1�9�x����*�8�QJ��f�<�2A,�AY~��)//hh1�k\"����5
xB5��qg`g�hRb��`��0��}���D�N�T6�����	M, �����?�2�x5��t�\	�\���(�hW)�-rS��H��
]�.Z�d���e�|<��I]t�@�#�5�e�\�5��,����9I��2���^�X���Q�H�MP���PXD��n�u�Z<4	���5a���M����Fm���	{),���v�Vf""�h��]�N3�ctmO:����^��UF���H{=�D�-�+O �JDkc7�J���S��������+i���5|��OT���}���+������i�U���������uX��x��~�nn;����f�W� �g��<5�%�N
3������Wn��5j9��<�?JN�������s��VG�,��;���;�*7Hi�oEhc>��]�W��+zl��R}�tW�5\�~<-G%���47J���RH/V�=��F������r1l>���k�����'�RK��;-ytS�F�i��*`�hi�_Ay�[���i,)�4sZ�V�snH�k�>UuP,�x����B�����Z[�@4���#<�������+�0~Rl��u������������iYD��jY��T�
&���o����Zm7eAL�l�`���Z��F�#��L?������%�������e�E�����L��Q�9��ql�7�\:W���}�G�z5/���Q�^�)�����_]�SiAQ`q���`��Pn�����,�
����
���~��F���e��F��8��e�[�Iv��������(��6��9W����O�0�d*�}ry�lk��Km�9>��������6(����]B`U#����*{J��-a�T_�1
9���Z�	":���
�l�\�7�,������y#��U_���g����O�{���������d�Ma6�����tG������%\I�n�� 4��Fy��yM�\���|S���Q�/�����YP��Q�T���U".��o���&���`x:���i��?��|~�
���\
J�ti3$�~w��XQ�M;{m�W�V�Uq <�x~07p�UhS\E8O����x2
����3��`��=��Q�h�4�x"���t?�����e/�����X�4@l���_�.3�0�|�*��
�X�n{��m!��B8g/v������tPB��CGT+OS:|\y3�y&	�|���5�y����o�����U�Q�y�zz�#�ub�������{�����������kh�vyZ�)�{M��w��:U��W�g'���"3���	�����a�*3����'�R'"p2Q�]*w�_�jVo���]�������>^����(���_�\]��]����b(���7�m����d!����s1-Y�h��Bp�������5+���������!2���92o
P���9�5h���<�-�������y3��X���_�Gi
R�a-4���T_�u�X��i�U�VDd��H��[=�P��o
�_�����L�}��<;i���yr�����d��y����G|%�����6��p��.��C��,��RW�aP��]����0�~o�{/�}Q�Zm����JN�.;�An���k
���01�u���%�F�1~���y� �V��#�S�t�jz@Xy�z]�b���0
y����8�/�]ow[��yS�7oo�a�����9e8��A��J�����ELp�m`i�o!I�����<�q��
��r��>�22O��_{���������)v���������
��=�	dg/Y��u�%����d�������{���8��{+1X���U]]U]�+z��5O���L�'
Z�"���I_d��@���W��/�����g>m�87�+���dsk{;��P�q��	#�����t�c;���	��<`�=���(E�6�>��_���E�D��l����������?�M�����]�E��K�����`�����?��������[����%�;��)�������F��_��g@k�Z�#t"%/l�������v��G��?�����	������a&����g���@�n�u�����c��>�Oj�0�a�q|�����M���z��ulJ�o��������;e���������"�%tp4.��'�=��O'��pB�O�g��Ms�A�H�3�1J�e<�q8����t�y�d+{����O���g�����J�����M��b, -4�z��$��E�W�~�C�s�[&8�!�I|�T#y�H��0��!����Sh���PM4'�uF
JA}��o*���8y=�}^�����d��@/�*b"�dW@b�
��	����[y�hqX���W�Fp��C��%/|N/�|*��`�7����X���n�/]��f��WOZ�����c\|Y�Ck'������������.LM�����^0�>��@��Y�e;xSM��W���iQ�l"������d��r�����p�_Y�jw3�:��������w@��z�8�q�.��v���x'�Hpq��������{�~W�?:������Y�
�4v���s�kC�6�����^�-U� ��DP%MG��eM�D�?m5��R]��@�*�S�
���yZ�g`����z�����)�n8�;�A�����Y*�
��*����^��j��y�������G���r^�!#��F:��N�Kys�t#shLA
~�0A.��7y��O�=��"�W���7��6m�������za���|0��U�|���M�,�,-f���m���ros�����3	VR"�~^�n�N���5��a$	}��w������E�Cm�j�kQ���<��@My���er$�&4���gT�'�1�a��H��n2���&�t�0�L�q���J��+��Ev��%m;�v�	���LL�������}���QO�r���"s���Y�8c�
-�������i�CM��-��'�=�!�_�}g^�n�����')����~�����iz��t������6����+���
F+t�� �'y��z}x�9]^���(A��3�K@���9t�Z&`�6�T�"=���*[���	V<F����lj}?HO`;U~����4E=|kP���o�D4�&=W�JE�<��i�f��Q��e�����7�~i���N�g���������=F���Tc>jcCo����7��T���cL�$��i���aa�"	b��+�A����e��wN��J,Q��D���5hS�el�=p�<�aL_
s�sZU�9��t�C�({�������0�,��W
���&����Z1�z��r���\����
7����������
j_�{��s�!o���c�X���O`I�	fb�1%2M�hS��s5G�JF*���Xe:YC��-y �de^H*�!P��HP,�F�
n�`P�H�2�r�7����(���it��S��1�/����Q�	*���D$���f'
���������O���$�����ADb����ZY�����^g9��;�`A��B����aaF�Iks�\�3�p��!w��)M��yc4�
�dH���J�*���1���Q��Y�w�pig������4����S�&
��@�R�
HI�F(��E���`���}��$�h�`S�C�K����Y9q�������+������~7�����i���p�,z���B���.Zg]<���M����[�9��_���g��`��i�o7-�B3Q�����p�`$2��:w�����/V��i��#4K1�������-����>u�Q����`��K�S{?x?��iR�*�>8E�56��Hh���1�t}��� ZeT��h�RZ��*�=�{���c�b�����%�����^���wp���FT�BQs��0��s
����������7�9�_�����(����}�#^r�Q3����:�Lx�Th���*�
�{��U�Tfm��?��������-V��(T��G��`S�b`��\S�k"DI�V��pl�L�����n}����[p{A|��@����c���{�c>6:�'!�TJ������kL�5vX������@����C
Y��X|hD����	����<����l��'���M/�c~��?w���[��~�������V�x��* 9�E�=\����C+�������uY�����h�X��H
.hV����Y���m���Y���vmNl�o~U�����^����%�Q@�:��a��.��O�$����J�����G����	;&c�W��������Wn���-��c�0Iw����ipJ�4$����.6#P\n�&&�C�����#���z���$��83��8��08Y��t)0�������U��L���y�l� ��?��H�Y}N����!��?N�X���yf<�X_�f��U~�����,<��=��tC����XL�}&J�5F���,7m����Vq��)����F������^�Dq���k���z���s�����z8���}X������Vx��Tx!+u�Vr�����}�`����v�Q�^H(�Z�NuK@D�b1������E,�������~�t��<M��/,_��O��'I5�JG
� ���"E�)6�����V�� �N%���������M�r�3��^v|'o
�0c����g�SH�\�R�1T�`:b��$�{�>�������ocf�����$�^�O�V�tdZWZ?-�T������]��� D����
_"Z�K,i[��Dz�6�O]5h��/����q����������b�Y��s6�|]��1�`���f�@���v�����
�H!�eQ�������EQ7;�����>����D�x���(p���|�JN'���8��2t�ey�����
�
d�Mi�P �����"�y���7��m��K@;�����/���?�3����t��poM��(�5KVX������$�i�+��m���l�;�O�^�xM�W���F�����f��=�{������z]�@�{��o3!��s�C�U�Z�}e�K��eY��e`�����*��p:��2�P�>(���w��GH�����v;�����g��!�c�p�{�&0�9�i�������GI���";�d������EJ�(�rx��Y�w�"��7D>���n+�%���<��b��i��L
6�W/���+,�/_�}�����8�SW��Z4�����H�~�D�b��{��;F��
5�C&w���YM���2L��!�KrR3�5�}�Z�-O��u0�M���h��i���K]��E3"�#`���p�4���H�@�xN�8u��:�U��H�,����h�G;E���4���c&�	�,3��[�,���/��j�R��R�)n];�����<u��
u=x:���[����}��c�f���MZG<�b+ ���R���k#;�Z�l���M�
i5���5�M;�kv�KV���<%��*\�X`�>$���Rs��/�P0l���`=�[c�6�e�����b�J�2�3�&}�h%d������}`O���L�"g�!�O�E�k�� �<���$q�-�����9ajZ�|{���`�V,�>IJ6Oq������y���L�Jx�fG�	�=���C�SxF3�~��[[
���J3��:s�:5��Y���q����>�����@�r!����G�~�^#�.��!$U(��4s�&)����e��LZ��,��G��f^t�)~�0��s��f��+�3��X��'w�H&�]]e�Le�&�D&��[F���P����,�g@�����
v�j���q�wK; ����w��4=�[�c�.�����X�G���wz�%_q3���������6�uG'�������UI�i��+��8�h�i��� ��'��W�d�����V7<�-���f����~+=}}|���b|�Xw+�
ne_�	��US�~:l�(y���������'�wl6Sn�����4�@�9�;c�2�9c�8�nB=	A��%r\7C�4���PH����P�FhA���X����C� $�������M���@��2b�I����aw���"�A��=*uO�D��X�!OY-l�Jk�d��������$�'�n�?9�Vpj��]�e�;G��~.�H3�@����>����n������n2^������Ot�u��d{�.�"�<��d8��!��C���b#����.nP��U��W��`S����Oe�K�\����#b.2��'�
+<1�H@��f,,hR�AA
(	@���l���Xz,����J�<��7�����g����j,>vyX��f{���q>ig�I��@~�> h�bGe^qZ��[�����wi�%����8x��p<��&�5��0R"gYv�^���L�j��Y��,�Q��(��a�KW�bN����@^�#?��5.j����B���+�D�T�'ac�ZT~��Tv��v�5)�K���8,���|.�f�U7������_��~L�����8he}�^��O�v%�'��}�A0��C^}��3�F�l8��u��=<zQ��L��W��������|jX�l%����QcR����|J��SO~�!�n���_����g�=���������;��k������u��{������b���6��d+NY����������?��H�(_��A�������T��}��& �tw�q
��z|-������6X��_��������"�9x�t#b�������br��m��!�K0�.��S���:<3Me���T�*G=?��Kn��-�h�!E��<$�������)u��4&*�$�D�����P�b��Z�1����0�����K@�tF�I������7�����^��n��{��X�S�������{��ER�"�Z�R�+9I�x���)~�������*��`������Q���R/b��`�8�"v�uU�o~�?�G���l�$��uM��%
=��*�����F�zs-�E�&^Vb������=Q�o��ob�'�G�{�z�L����a��E
r�{Hg;�x��6)��������i�\�29�pH,h�#g��X��R�.���B�i�\��"l��R�]M�P~�EU(P�j;[�����/�?n�G46{Gl��NG~���&�N������E�"�%�X��T�����sI����u�,�j����#8�������:X����Z^��3P#�l �:�fe��	*����(+��#&D(��s�W������� ��^T�N$���P�,)�{��K�	�3���I������c.����-v�R����-q��Xg�� ������lq���������'��'����Ko�0�q����ubL�MaDz����|@l	�w��(`++����x�1��l��1�X�=ATOI�U�c���5�@��o1I��.�����[61*�piu[J�G_�D�����I�}^������k_�	�W��sG�jQty+����W��<$|��T�>��]��d����gI�+���a��G;����!�<P��W��1�zZ k���=���?��fva[�����MV���{�S|��� �����C�����_���{������r[!��V�N����{�v�iw�Z�]f��@�d��t����p�� ^�R�R4��m�	|O�xA�]�j�%oS�~���0R^�J���vb}5O����0�ga�G
�rU�AC
J�dZ���
;\d�\��~u`�9l�'�+����<h}��l����O�����bt�q�JwM� �y�,O�W0�`t�7�?%����2VtR���S�t���19�,b���n	�.
M��w���d�+��9MO��[4]K�(��:g�`R{P�/;��}"��B3��^/0��[T1[q�%M�C,i����O����D��zc����j�=*�j�z6hN4�7���5>h8���J����g��o�9m���o��hy$�����|{2��������>��G��3���Q_����\Ix�#'���o��K_��zq����M���\(�y���9UO��-�������_T���$O
�|
sr�����:���~��W�lF\3��G�<�M�AH�v:b���(
��k���<
�Q{�`����p��������*�������l1n3����X����EE���dJ�5K���
�X��W����x��>�����^�c�����m��E�ra�`���n/���r���YZf+^��[A��J)��qN<s�E;��{+�A����X��=Z�)�N��V-����83����8\����'[��"W�
JJ��!�1e� M��K�/j����0�����MtcOX��nC�o�kv����:��������)#��e&pF���Dm��>����_���SI���'� ^�f��M���O��LN`�G�s'���!��d�i�����������'-:��3����3Q@�qM�@��J����-p�a���&�
�@!�*�P@^��iU��ED���HA0�����e�������=*��w-I���WE��3�����
��k$5A{U)%0����/�\T���0t���Al=o_�cH[�aa�_$zh�0/uF�������Z�j�7�;����ON`^�
�7o�3=y<�g�|��d����g��jT_{��a�����vO�������e������S�g*�I���!)�]3a���H�MI����g�'�/�T����-:d;PB�~��f�2��������w������n{m�}�P���a��o�w_c�	�����/R�"�vO)���mi����7Wq����&IQ��X����+D��%����2_�r���f�������(�R}���x,���h��%m���M�|����f�������[�*6�FR���y����C6w>��n���"����M���RT���?�]J�����$��QE�����������m��E�}�8�g9�����UR�����I�*��H�D8u���v�����$�~Id��:�t��o���4��%������9�=�c��!m���P	����$�R��!n������F��-�����C�	W{�i��\F��3��d������������yn���k������K�-x����V!��������:��h�D��	�`��&�;����)����r�9=>hh_��Sy0�����%�Ao���P�K`���:]E�x��T����Q$�,r��u"-Y�pV�Bv0}ER��D�:���M��d�2���g�
ni�YS��(T�v��m"B��������z=����	c������,��"�����o<u��B�nh#5����V6)��NF$��N\7r�����vaK�$�%��>����cu����`$���o����'��U���,���nW�S��+��*��i�;�dP��������E\�AX��5�y�
��:�s�����+8	��0f"�_d�lL����01�cTo�)��@N^���Y3���_��7��7&L�,��5�I����d�>P���4��V"��ar�t��hJ��P�f�qk���z�-���%^SP$c,�2
���Yv���RV�%�+���#*��|��L��9.�����~�:�Tx@�5/7���?K�F��8D���g���YQ)�1�Vx!|t"_�R�
��6���a��k ^�7�������D��R F�����T9k��hSVK�����aN8B��O�%		���e���������,!���d�����q.������������bW���W��G'/��i��E�V{�
������������l&�c����+:eH�Fw�=�bM�^.f���uD@>z? �:������=U0�92]�������D��6�Anb�����j��Z�%b?��E�����AF^g��Y��!��+���1�&�����b��+��%���:�5Y�c`�!�v��tw�qg�._������X��_�N='ymTX?�lA����a�?���>��r��!������l���1��zJ�6�C��K ��V��o�p�m-"���dy�l
c����k1�)�!���c*	�D4�2�����.�$����1�"=+���	�@�h�65U��*���m����F��Y%b������4��7�yw���c(|s�S���\v���jO�^VF\a
��BV������(���K;?�6X� ��t��
CM�-�r�}��0)	8Etc�~�$����H1�
1���:�Rw5��[<K�Ft8��^7}�}�d�'������~��$(>��{��������q�a��8?�d?pX��.]�O�o�U���ef�w��j��uD��D�9`�'�8�r�R�������� �o�*��xs�|�����|[/].�#�I(���h�]���:m��n%#�8�>��Z/^�Y�5���
��J:����_&��7�O����[�oN�O^B{�7��J7��_���,�5���������%0!����E����e�x��h?!1�:��a���1 ����
�[�!�?[�F��J]1?�a�b�����x���	���=l4���D��p��80Fs�k�	�;%�J��dr.���3)����\�Qw�s�u���0we[�3���k�o]V�F��R�n�T�*v����u YB���DP�`������$�Mn�>�D�v�;�{E��S[�����?�
�(���
�x�W������!W��x��!��	)��[���'�c���K�������
zW��|;s�mBv��l��Q��JlV;�)yR�&��F�Ju?��?������D�K�]P��A'�a�^N�V��`Y�K�H�R��	N$r�f�$g�$�O�z���C%�U��6YD���G4e����B�s!�n�#�z �|xK)���
(�n�*���.�u�Rhk������nLQl0�P�dr��+I-�63Ve�z�X	[��|�Ag�m5-q��DYy�+���K9����R�TvFT�{�'���?�#i�es3t�%~�B[���{���/���'Rx E���)Z�DQ���+��(���N_6[�{�����C��<9=x��=�s46�����#��v���a����u?��L�(�����|n�z�b)��yz��������U��pj�ePi+$c�9A�{a�]���fE�<~�=�H��7��fpg�������a����&�K��w�`9t�S1���|{j�6,�7z\k�1v9�o��b��������0Q�8��i���k��b�LCX�F��j���
�h(�yH1�}
� ��k�x�Kv���s�ckt!L��!�r��1�#b�N�"0��W0�?��^����=rM�@���k���-^3�$>�����iq���*^��Y�s�H���M�+�)C�1
�;~��8(q(��aZ�%h����M{[e+�x�v�"���p��-�w@��C�O���\�%��'I�)o-8��D[�E�]�����f��e���Y44���y�M��4=x[�g�8���������]3�<��'���r�,�6��f����[.c�S6pqN�K��_������C�,�N�������Z	F�!b�������1	T��a��D��]Oo0���^R�@�g�G�i�	��rSB�3F�q�����-�<_.FU:D��Pe�����5_*O3*[5m�H�I'������T������0�t�wII,�6.�����'���j�5D\���U��x�v��}���2�����n#t�uu!JGG�B
�t����v�"��t'
�{�{��LTw�s9R� �=�B'"R,p���*��;�;���c}���mN��j!�-!#�����S���8�q����-�D�(Z�IB�:�������ld��5�����������v��������$������6��=_0��m6�G/_6_�-u���T�93�����h?_{A��]����:�!�V�ds1%������=O!���s��/L���wrtl��4����^���������Qz�rv\�3n�r��r�_������������/��;c	����9CCi�5���.��]��R�@�����2�^E ��	�!&�7����,�~"���N��"����e���m��=�g��������|����WhUgr'$��{6���nV�>��!ra�O=��FI��f���Ak��$z��fx��4��F���������F7�'6t��,�z�\'���c�C��e�����	g&��4�����/u.
�h���c��Z|����E{�z\�I���%t
����.�����j�,�T�3n���	zk��=f�I����0�o�y�%+�A�.�9z��������rf����uHW���P��v&�N��t����
���G�����J�����s�F�f�>`K��7Wb�-�`�����=��2�@b�>���{��X���[L�E�${Y%�����D�Q�D��b"��S�xkS����9c�k�z&��65�\��McN
��eZY�����k�����=��I<�.��z0M���@}�JG�E��E��J�2�r��L/S%i�^0�������
����y��S��������?c���	h�/�o�_�0_Xi�I_�
Z.�������������9��x���/������h�x�Q��Kwx��z_]I=�%�,k&igG��.��Y����_`��,��]����,Q�o���ym����U�r��[+�x�)�J����t#�759\�
8�$7�9�o���KXw]����I�o�����7�Pl�P����"�h��=kt,�`DxL���p�*�{ff6�
�93o9��exh���?���L�8���%4�AD�����!�6�(���j6*@y�qk��)0�t����-f[8���]��Q���Cb�PS������I�yx�R��_����d���]���"�&�������!@�-�O{|�1����O���������S�����<1��k��_�1��R����w�=��4�y3�YW�U���_����}XcX���
�s�xZ�����G�+@e��(f�v����u�|3����f�9�%nf�k����eK0���|m������.����������~��]f�Q�C?����-��;_x��	�^�]���<-���?�WA v�����:����,m�A�����oY:��������z����V�s���
w�8��}�x�J���Q��{�Y�����>hQz�i�x��pG�����y�;4���F
m������+/��@��q#��-$0� ����[������{����=�;z�<xU������8G)�<U�
��hn���:q��a�f��+���)	X����K�*��Yh��mo���B���$�N�a>�����zR���y��GeI�]�_:8}�)�X9�s����o�?
�����]F����`<>V������(�qgtS��4��������~����5#��,�;~��z�����5���6�e�SD(�$r��2��*��g0�1����x8���U�6Yn������7����&���&e'��9O(1I6R6�uk�Mb[���3G��
���X�_lh��-	�vlj�(�t{8�fg�R�L
�vA�7��5�"��pV��aa����Pc�7
�����G����G������>�t�Q4��Z^�I5[w2!��M`j���>�GL6���`HpV�*�*��O|)��9q�?�E��J�2�B��/�"e���8���
�D��~<��cy[����/���	�A��A6�K��I?
h����QBLIs���iJ�l/�����<>�Z�Z�FB�Q���cC�Dh-L(��<�������65�ME��<���k����\x����8���W��U����V������,��"w;S��w�8{]���M�F��l���;�����* ��W��U��!2k��I7�Pmn@������?:�"�
��X*���!B�-1_�	B�RKj��qN�9l%*W����R���R}U��$��u�U��K���z�Q^I�&��qe��������������@����Q���t&����u��z��L_���Fb��[��WqQ��E�$�6C\A+��B��/:t��^�_��i�������|/�pgg��g&���G�3/(����������#Z�4i
�Y�:_X*N�H)U�<��i�������
��*�s�M�5��`B�C�������x7�=e�	%XmB����'�8������g�_3:���0��Hw:eR����.9$�)-v��Y�3��H���������T����	*���2�\��T>e���K8��+w��*��
�-��G����]`�����6�����x.i�%�[b�
;C�,[�F[[�l��(
%Pr�4�p������_�6�V����n�R^V^�VsK���%zAi*���U{�n���+�z^&x�,���?�h>�w)�1�`����d{�h��	d}A�bo3��^3��G�e�������\�Hm|������"~c!'n"6�:[2�Z�yf�oY�b	����K2�bK�[8z�Dq-�
1 E-�[S��8�tx��������y�tk
��o���4
wNAJ*�Z�lj��tA��(O�R��ah�t;O6��'�t4�x|<��{yn����88r�I�� �4�����o�]�Sqi=���)8��Y{��=��J�������y�X
aZg��mQS����|8w�"�������&�����B�	�PVJ+Pl�������6��J�g��.�m�a�}�`��n
��e5��lKiyS���m�*E��)���&�����b�����u7���n��q�S������Oy��eV {��A��x��%#������6J��F������Sh<�O�m��fT�U2��B8##�j���t��
Sm�`~����B2c�c�,��u���r��()���r���P�U�
��� ���)#bCI��J���b�i��,�
����q����:_���M�����R�1�������@MX�Z2=.���8�g��K������AbR�TxuM�A��s*T8�|>�L�hX�����fRv��I�H�d����2�$jm�s�A(L��2!�����x��P�w���EJ�l��%"��B%&��%�Y���d,�t%���J#�'���aF`��f�p��U!o%#J��HLR�<Y�]����e�A{��21���84QIAje�E����~�O��&�$'�3����Q>���dY����[�I��&�Gp��!�c����G���W^�h}x
]�H���T�	2\��i�]�#�RV:d;��6%�P��SA�I��gB��6���m�}0*�����[q�9�(����<^���a$��EQE#|��W�� ��q���}8�y#�� ���G�!�1��Y���3�����K�������������v��5Q��
Q�(��P���-�j��DC��Q�{y����
�7F�/��������������M��Mx%�Y��q:��ln=�J#q�~rv��V�5k����������k�e�lld�;����?�������z���>m����������_����V������O�_����m�������N=���y��7�/��3X�=N��L���F����kJ��v����`�m�k(���1�"���9&f$O�&UPr������G���G�lO�~���\��O^�EN�O����8��c��*��4J<�B�
C� $��9�������?C%��x�
x��?����d�J��k��RJ�������������>Qx�]%�H��������-�7/ieAy�!W%�I��� R_�c���~�S��lgK|��n�R?0A)����LJ9�)~���K*���d@��Uyi�)G�a8K��0�b��������9� ;��>���~��=�s�'�_$�����\We��[�`G7���e�
���=�v�;�+E��j��|���xd�C$�vA�e���&G��Jw��f���/:��j�e��q^RN�v#Yp��u8g�/�&���y����?�����@UW�����J����w��>n�����5��HB0��=k���������;�q�g4Kv�6ny��h��Y;��a<�'���7+�Z�'�����#~������V��w�0�j�RNc'����:��)��9sf�H���"
��3b���^,�`��>�;��0N/��p*|$=thY�v�O�Z��N��8��|Q�������)j�����`
mC
��>�o{D����_��Oa(�^�x��`��������\�hQ����ujq,r�C
2.y~�g�����b�abWO8�M���y#�8��/aB�s~�~�b8�1#O��}�\�kw�������x��5����;��xG�x���s��;sx����<a�j����)��zTf�tga��9�'�OA~��������a��~g�gy<�2�{6_��;��x�+y���UP��u����x�87���~�p}	�������6����s�C��[c��uf����I�������&H�����f\�6w�Vd�����_�������H�����
��m�
��Ty�?��?Iv����)��;P���p�//���X8�#�p=�k��iL��a:���7N�w����A��8/�����9^��=���c-�kT�m�3��x��gFQ�g0�V��J��/�����V��S��9�&V&�;��&��w-����D�j�����o-���2��
=�8^�<���4�wY�������e�(+�K�4;�����!�}�!la�!]]��5�A�M8��fJ:�y��8`��%zOlT�`:�E�L���xd��G&�N�<}�k�����?�w��";�_�WQ�z��N��,>f�mM�px��j��Dej���(��)�����a�q�s��+�m����{�XYt����r#��A�"�����,���8j�q!�K��m��@��P��l8���0����|}�c��$�d�_�Vb��+� �5��_`
��2���PB�<
x#��Q%������2\����OR��V{U\����gQ-��+&��{L������� `"4Usee�>�����|��a�|�[����?e65�	p��i6�e��<���U�I��J�g��t��^]��;-U�:Xv���v��`':��jD�>�7����c��W~�����1��.��[�r3��ae"���V��x�O�����S+y�l�A+pd��|����:O�b��(P�����h%�&Y{�^R�@�|�����k�� K��FW��z�������������&
_n
#195Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#194)
Re: speed up a logical replica setup

On Fri, Mar 15, 2024 at 9:23 AM Euler Taveira <euler@eulerto.com> wrote:

Did you consider adding options for publication/subscription/slot
names as mentioned in my previous email? As discussed in a few emails
above, it would be quite confusing for users to identify the logical
replication objects once the standby is converted to subscriber.

*
+static void
+cleanup_objects_atexit(void)
{
...
conn = connect_database(dbinfo[i].pubconninfo, false);
+ if (conn != NULL)
+ {
+ if (dbinfo[i].made_publication)
+ drop_publication(conn, &dbinfo[i]);
+ if (dbinfo[i].made_replslot)
+ drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+ disconnect_database(conn, false);
+ }
+ else
+ {
+ /*
+ * If a connection could not be established, inform the user
+ * that some objects were left on primary and should be
+ * removed before trying again.
+ */
+ if (dbinfo[i].made_publication)
+ {
+ pg_log_warning("There might be a publication \"%s\" in database
\"%s\" on primary",
+    dbinfo[i].pubname, dbinfo[i].dbname);
+ pg_log_warning_hint("Consider dropping this publication before
trying again.");
+ }

It seems we care only for publications created on the primary. Isn't
it possible that some of the publications have been replicated to
standby by that time, for example, in case failure happens after
creating a few publications? IIUC, we don't care for standby cleanup
after failure because it can't be used for streaming replication
anymore. So, the only choice the user has is to recreate the standby
and start the pg_createsubscriber again. This sounds questionable to
me as to whether users would like this behavior. Does anyone else have
an opinion on this point?

I see the below note in the patch:
+    If <application>pg_createsubscriber</application> fails while processing,
+    then the data directory is likely not in a state that can be recovered. It
+    is true if the target server was promoted. In such a case, creating a new
+    standby server is recommended.

By reading this it is not completely clear whether the standby is not
recoverable in case of any error or only an error after the target
server is promoted. If others agree with this behavior then we should
write the detailed reason for this somewhere in the comments as well
unless it is already explained.

--
With Regards,
Amit Kapila.

#196vignesh C
vignesh21@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#188)
2 attachment(s)
Re: speed up a logical replica setup

On Mon, 11 Mar 2024 at 10:33, Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear Vignesh,

Thanks for updating the patch, but cfbot still got angry [1].
Note that two containers (autoconf and meson) failed at different place,
so I think it is intentional one. It seems that there may be a bug related with 32-bit build.

We should see and fix as soon as possible.

I was able to reproduce this random failure and found the following reason:
The Minimum recovery ending location 0/5000000 was more than the
recovery_target_lsn specified is "0/4001198". In few random cases the
standby applies a few more WAL records after the replication slot is
created; this leads to minimum recovery ending location being greater
than the recovery_target_lsn because of which the server will fail
with:
FATAL: requested recovery stop point is before consistent recovery point

I have fixed it by pausing the replay in the standby server before the
replication slots get created.
The attached v29-0002-Keep-standby-server-s-minimum-recovery-point-les.patch
patch has the changes for the same.
Thoughts?

Regards,
Vignesh

Attachments:

v29-0002-Keep-standby-server-s-minimum-recovery-point-les.patchtext/x-patch; charset=US-ASCII; name=v29-0002-Keep-standby-server-s-minimum-recovery-point-les.patchDownload
From e98e1ba0661e3d658de8d46ff9082f6cd4040b41 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Sat, 16 Mar 2024 18:47:35 +0530
Subject: [PATCH v29 2/2] Keep standby server's minimum recovery point less
 than the consistent_lsn.

Standby server should not get ahead of the replication slot's lsn. If
this happens we will not be able to promote the standby server as it
will not be able to reach the consistent point because the standby
server's Minimum recovery ending location will be greater than the
consistent_lsn. Fixed this by pausing the replay before the replication
slots are created.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 93 ++++++++++++++++++++-
 1 file changed, 92 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index c565f8524a..18e8757403 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -95,6 +95,8 @@ static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
 static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
 static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
 									 const char *lsn);
+static void pause_replay_stantby_server(struct LogicalRepInfo *dbinfo,
+										struct CreateSubscriberOptions *opt);
 static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
 
 #define	USEC_PER_SEC	1000000
@@ -917,7 +919,9 @@ check_subscriber(struct LogicalRepInfo *dbinfo)
 	appendPQExpBuffer(str,
 					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
 					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
-					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_wal_replay_pause()', 'EXECUTE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_get_wal_replay_pause_state()', 'EXECUTE')",
 					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
 
 	pg_log_debug("command is: %s", str->data);
@@ -949,6 +953,18 @@ check_subscriber(struct LogicalRepInfo *dbinfo)
 					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
 		failed = true;
 	}
+	if (strcmp(PQgetvalue(res, 0, 3), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_wal_replay_pause()");
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 4), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_get_wal_replay_pause_state()");
+		failed = true;
+	}
 
 	destroyPQExpBuffer(str);
 	PQclear(res);
@@ -1026,6 +1042,72 @@ check_subscriber(struct LogicalRepInfo *dbinfo)
 		exit(1);
 }
 
+/*
+ * Pause replaying at the standby server.
+ */
+static void
+pause_replay_stantby_server(struct LogicalRepInfo *dbinfo,
+							struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	/* Connect to subscriber. */
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	pg_log_info("Pausing the replay in standby server");
+	pg_log_debug("command is: SELECT pg_catalog.pg_wal_replay_pause()");
+
+	if (!dry_run)
+	{
+		int			timer = 0;
+
+		res = PQexec(conn, "SELECT pg_catalog.pg_wal_replay_pause()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not pause replay in standby server: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+
+		/* Wait till replay has paused */
+		for(;;)
+		{
+			pg_log_debug("command is: SELECT pg_catalog.pg_get_wal_replay_pause_state()");
+			res = PQexec(conn, "SELECT pg_catalog.pg_get_wal_replay_pause_state()");
+			if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			{
+				pg_log_error("could not get pause replay state in standby server: %s",
+							PQresultErrorMessage(res));
+				disconnect_database(conn, true);
+			}
+
+			if (strcmp(PQgetvalue(res, 0, 0), "paused") == 0)
+			{
+				PQclear(res);
+				break;
+			}
+
+			PQclear(res);
+
+			/* Bail out after recovery_timeout seconds if this option is set */
+			if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+			{
+				pg_log_error("timed out waiting to pause replay");
+				disconnect_database(conn, true);
+			}
+
+			/* Keep waiting */
+			pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+			timer += WAIT_INTERVAL;
+		}
+	}
+
+	disconnect_database(conn, false);
+}
+
 /*
  * Create the subscriptions, adjust the initial location for logical
  * replication and enable the subscriptions. That's the last step for logical
@@ -1948,6 +2030,15 @@ main(int argc, char **argv)
 	 */
 	check_publisher(dbinfo);
 
+	/*
+	 * Standby server should not get ahead of the replication slot's lsn. If
+	 * this happens we will not be able to promote the standby server as it
+	 * will not be able to reach the consistent point because the standby
+	 * server's Minimum recovery ending location will be greater than the
+	 * consistent_lsn.
+	 */
+	pause_replay_stantby_server(dbinfo, &opt);
+
 	/*
 	 * Create the required objects for each database on publisher. This step
 	 * is here mainly because if we stop the standby we cannot verify if the
-- 
2.34.1

v29-0001-pg_createsubscriber-creates-a-new-logical-replic.patchtext/x-patch; charset=US-ASCII; name=v29-0001-pg_createsubscriber-creates-a-new-logical-replic.patchDownload
From a0aa20ff421a12e4f94358d4144c1fc3106a3009 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v29 1/2] pg_createsubscriber: creates a new logical replica
 from a standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber). All tables
in the specified database(s) are included in the logical replication
setup. A pair of publication and subscription objects are created for
each database.

The main advantage of pg_createsubscriber over the common logical
replication setup is the initial data copy. It also reduces the catchup
phase.

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
using FOR ALL TABLES and a replication slot for each specified database.
Write recovery parameters into the target data directory and start the
target server. It specifies the LSN of the last replication slot
(replication start point) up to which the recovery will proceed. Wait
until the target server is promoted. Create one subscription per
specified database (using publication and replication slot created in a
previous step) on the target server. Set the replication progress to the
replication start point for each subscription. Enable the subscription
for each specified database on the target server. And finally, change
the system identifier on the target server.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  477 ++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2005 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  265 +++
 8 files changed, 2777 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..d038572eb0
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,477 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. All tables in the specified
+    database are included in the logical replication setup. A pair of
+    publication and subscription objects are created for each database. It
+    must be run at the target server.
+  </para>
+
+  <para>
+    After a successful run, the state of the target server is analagous to a
+    fresh logical replication setup. The main difference between the logical
+    replication setup and <application>pg_createsubscriber</application>
+    is the initial data copy. Only the synchronization phase is done, which
+    ensures each table is brought up to a synchronized state.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because most of the execution time is spent making the initial data
+   copy. For smaller databases,
+   <link linkend="logical-replication">initial data synchronization</link> is
+   recommended.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--config-file=<replaceable class="parameter">filename</replaceable></option></term>
+      <listitem>
+       <para>
+        Use the specified main server configuration file for the target
+        data directory. The <application>pg_createsubscriber</application>
+        uses internally the <application>pg_ctl</application> command to start
+        and stop the target server. It allows you to specify the actual
+        <filename>postgresql.conf</filename> configuration file if it is stored
+        outside the data directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   There are some prerequisites for
+   <application>pg_createsubscriber</application> to convert the target server
+   into a logical replica. If these are not met an error will be reported. The
+   source and target servers must have the same major version as the
+   <application>pg_createsubscriber</application>. The given target data
+   directory must have the same system identifier than the source data
+   directory. If a standby server is running on the target data directory or it
+   is a base backup from the source data directory, system identifiers are the
+   same. The given database user for the target data directory must have
+   privileges for creating
+   <link linkend="sql-createsubscription">subscriptions</link> and using
+   <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+  </para>
+
+  <para>
+   The target server must be used as a physical standby. The target server
+   must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   and
+   <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases. The target server must have
+   <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+   configured to a value greater than the number of specified databases. The
+   target server must accept local connections.
+  </para>
+
+  <para>
+   The source server must accept connections from the target server. The
+   source server must not be in recovery. Publications cannot be created in a
+   read-only cluster. The source server must have
+   <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+   <literal>logical</literal>.
+   The source server must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases plus existing replication slots. The source server must have
+   <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases and existing <literal>walsender</literal> processes.
+  </para>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    then the data directory is likely not in a state that can be recovered. It
+    is true if the target server was promoted. In such a case, creating a new
+    standby server is recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during the transformation steps.
+    Hence, regular connections to the target server should fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Since DDL commands are not replicated by logical replication, avoid
+    executing DDL commands that change the database schema while running
+    <application>pg_createsubscriber</application>. If the target server has
+    already been converted to logical replica, the DDL commands must not be
+    replicated which might cause an error.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    are removed. The removal might fail if the target server cannot connect to
+    the source server. In such a case, a warning message will inform the
+    objects left. If the target server is running, it will be stopped.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    If the target server is a synchronous replica, transaction commits on the
+    primary might wait for replication while running
+    <application>pg_createsubscriber</application>.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which WAL files from the source server might be used by the target server.
+    If the target server has a standby, replication will break and a fresh
+    standby should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is already running, stop it because some parameters can only
+     be set at server start.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. If any of the prerequisites are not met,
+     <application>pg_createsubscriber</application> will terminate with an
+     error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication is created using
+     <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>.
+     It has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameter:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>). Each
+     replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+     The last replication slot LSN is used as a stopping point in the
+     <xref linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication start point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies a LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are also added so it avoids unexpected behavior during the recovery
+     process such as end of the recovery as soon as a consistent state is
+     reached (WAL should be applied until the replication start location) and
+     multiple recovery targets that can cause a failure. This step finishes
+     once the server ends standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     The subscription has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     The replication slot name is identical to the subscription name. It does
+     not copy existing data from the source server. It does not create a
+     replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the replication
+     start point before starting the replication.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the replication start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the replication start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the replication start point. This is the exact LSN to be used
+     as a initial replication location for each subscription. The replication
+     origin name is obtained since the subscription was created. The replication
+     origin name and the replication start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the replication start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..e9a920dbcd 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..c565f8524a
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2005 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/pg_prng.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	"50432"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *config_file;	/* configuration file */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	char	   *sub_port;		/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	int			recovery_timeout;	/* stop recovery after this time */
+}			CreateSubscriberOptions;
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name / replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+}			LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_sub_conninfo(struct CreateSubscriberOptions *opt);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(SimpleStringList dbnames,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static void check_publisher(struct LogicalRepInfo *dbinfo);
+static char *setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static void setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir,
+						   char *lsn);
+static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
+										  char *slotname);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(struct CreateSubscriberOptions *opt,
+								 bool restricted_access);
+static void stop_standby_server(const char *datadir);
+static void wait_for_end_recovery(const char *conninfo,
+								  struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+/* standby / subscriber data directory */
+static char *subscriber_dir = NULL;
+
+static bool recovery_ended = false;
+static bool standby_running = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("pg_createsubscriber failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.");
+		pg_log_warning_hint("You must recreate the physical replica before continuing.");
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			PGconn	   *conn;
+
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].subname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].subname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+
+	if (standby_running)
+		stop_standby_server(subscriber_dir);
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %s)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_("      --config-file=FILENAME         use specified main server configuration\n"
+			 "                                     file when running target cluster\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Build a subscriber connection string. Only a few parameters are supported
+ * since it starts a server with restricted access.
+ */
+static char *
+get_sub_conninfo(struct CreateSubscriberOptions *opt)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	appendPQExpBuffer(buf, "port=%s", opt->sub_port);
+#if !defined(WIN32)
+	appendPQExpBuffer(buf, " host=%s", opt->socket_dir);
+#endif
+	if (opt->sub_username != NULL)
+		appendPQExpBuffer(buf, " user=%s", opt->sub_username);
+	appendPQExpBuffer(buf, " fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(SimpleStringList dbnames, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	int			i = 0;
+
+	dbinfo = (struct LogicalRepInfo *) pg_malloc(num_dbs * sizeof(struct LogicalRepInfo));
+
+	for (SimpleStringListCell *cell = dbnames.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): connection string: %s", i, dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): connection string: %s", i, dbinfo[i].subconninfo);
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   subscriber_dir, DEVNULL);
+
+	pg_log_debug("pg_resetwal command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication. Returns the LSN from latest replication slot. It will be the
+ * replication start point that is used to adjust the subscriptions (see
+ * set_replication_progress).
+ */
+static char *
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	pg_prng_state prng_state;
+	char	   *lsn = NULL;
+
+	pg_prng_seed(&prng_state, (uint64) (getpid() ^ time(NULL)));
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+		char		pubname[NAMEDATALEN];
+		char		replslotname[NAMEDATALEN];
+		uint32		rand;
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		res = PQexec(conn,
+					 "SELECT oid FROM pg_catalog.pg_database "
+					 "WHERE datname = pg_catalog.current_database()");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain database OID: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+
+		/* Remember database OID */
+		dbinfo[i].oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+		PQclear(res);
+
+		/* Random unsigned integer */
+		rand = pg_prng_uint32(&prng_state);
+
+		/*
+		 * Build the publication name. The name must not exceed NAMEDATALEN -
+		 * 1. This current schema uses a maximum of 40 characters (20 + 10 + 1
+		 * + 8 + '\0').
+		 */
+		snprintf(pubname, sizeof(pubname), "pg_createsubscriber_%u_%x",
+				 dbinfo[i].oid, rand);
+		dbinfo[i].pubname = pg_strdup(pubname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/*
+		 * Build the replication slot name. The name must not exceed
+		 * NAMEDATALEN - 1. This current schema uses a maximum of 40
+		 * characters (20 + 10 + 1 + 8 + '\0'). A random number is included to
+		 * reduce the probability of collision. By default, subscription name
+		 * is used as replication slot name.
+		 */
+		snprintf(replslotname, sizeof(replslotname),
+				 "pg_createsubscriber_%u_%x",
+				 dbinfo[i].oid,
+				 rand);
+		dbinfo[i].subname = pg_strdup(replslotname);
+
+		/* Create replication slot on publisher */
+		if (lsn)
+			pg_free(lsn);
+		lsn = create_logical_replication_slot(conn, &dbinfo[i]);
+		if (lsn != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+
+	return lsn;
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ *
+ * XXX Does it not allow a synchronous replica?
+ */
+static void
+check_publisher(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	bool		failed = false;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		failed = true;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
+		failed = true;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ *
+ * XXX Does it not allow a time-delayed replica?
+ *
+ * XXX In a cascaded replication scenario (P -> S -> C), if the target server
+ * is S, it cannot detect there is a replica (server C) because server S starts
+ * accepting only local connections and server C cannot connect to it. Hence,
+ * there is not a reliable way to provide a suitable error saying the server C
+ * will be broken at the end of this process (due to pg_resetwal).
+ */
+static void
+check_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+	bool		failed = false;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("The target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		failed = true;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * replication setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Write the required recovery parameters.
+ */
+static void
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir, char *lsn)
+{
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	/*
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection. The primary_conninfo is generated using the
+	 * connection settings.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * The subscriber is not running yet. In dry run mode, the recovery
+	 * parameters *won't* be written. An invalid LSN is used for printing
+	 * purposes. Additional recovery parameters are added here. It avoids
+	 * unexpected behavior such as end of recovery as soon as a consistent
+	 * state is reached (recovery_target) and failure due to multiple recovery
+	 * targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  lsn);
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+}
+
+/*
+ * Drop physical replication slot on primary if the standby was using it. After
+ * the transformation, it has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, char *slotname)
+{
+	PGconn	   *conn;
+
+	/* Replication slot does not exist, do nothing */
+	if (!primary_slot_name)
+		return;
+
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn != NULL)
+	{
+		drop_replication_slot(conn, &dbinfo[0], slotname);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop replication slot \"%s\" on primary",
+					   slotname);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->subname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(struct CreateSubscriberOptions *opt, bool restricted_access)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	int			rc;
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, subscriber_dir);
+	if (restricted_access)
+	{
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %s\"", opt->sub_port);
+#if !defined(WIN32)
+
+		/*
+		 * An empty listen_addresses list means the server does not listen on
+		 * any IP interfaces; only Unix-domain sockets can be used to connect
+		 * to the server. Prevent external connections to minimize the chance
+		 * of failure.
+		 */
+		appendPQExpBufferStr(pg_ctl_cmd, " -o \"-c listen_addresses='' -c unix_socket_permissions=0700");
+		if (opt->socket_dir)
+			appendPQExpBuffer(pg_ctl_cmd, " -c unix_socket_directories='%s'",
+							  opt->socket_dir);
+		appendPQExpBufferChar(pg_ctl_cmd, '"');
+#endif
+	}
+	if (opt->config_file != NULL)
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-c config_file=%s\"",
+						  opt->config_file);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	standby_running = true;
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	standby_running = false;
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	10
+
+	pg_log_info("waiting for the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid and a
+		 * random number.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it. By
+ * default, the subscription name is used as replication slot name. It is
+ * not required to copy data. The subscription will be created but it will not
+ * be enabled now. That's because the replication progress must be set and the
+ * replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, copy_data = false, enabled = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the last
+ * replication slot that was created. The goal is to set up the initial
+ * location for the logical replication that is the exact LSN that the
+ * subscriber was promoted. Once the subscription is enabled it will start
+ * streaming from that location onwards.  In dry run mode, the subscription OID
+ * and LSN are set to invalid values for printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"config-file", required_argument, NULL, 1},
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pub_base_conninfo;
+	char	   *sub_base_conninfo;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	char	   *consistent_lsn;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	subscriber_dir = NULL;
+	opt.config_file = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = palloc(16);
+	strcpy(opt.sub_port, DEFAULT_SUB_PORT);
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:np:P:s:t:U:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				/* Ignore duplicated database names */
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				break;
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				pg_free(opt.sub_port);
+				opt.sub_port = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				canonicalize_path(opt.socket_dir);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			case 1:
+				opt.config_file = pg_strdup(optarg);
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/* Any non-option arguments? */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* Required arguments */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* If socket directory is not provided, use the current directory */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_sub_conninfo(&opt);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(opt.database_names, pub_base_conninfo,
+								sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * If the standby server is running, stop it. Some parameters (that can
+	 * only be set at server start) are informed by command-line options.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(subscriber_dir);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	consistent_lsn = setup_publisher(dbinfo);
+
+	/* Write the required recovery parameters */
+	setup_recovery(dbinfo, subscriber_dir, consistent_lsn);
+
+	/*
+	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("stopping and starting the subscriber");
+	stop_standby_server(subscriber_dir);
+	start_standby_server(&opt, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the replication start
+	 * point to the LSN reported by setup_publisher().  It also cleans up
+	 * publications created by this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/* Remove primary_slot_name if it exists on primary */
+	drop_primary_replication_slot(dbinfo, primary_slot_name);
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(&opt);
+
+	/*
+	 * In dry run mode, the server is restarted with the provided command-line
+	 * options so validation can be applied in the target server. In order to
+	 * preserve the initial state of the server (running), start it without
+	 * the command-line options.
+	 */
+	if (dry_run)
+		start_standby_server(&opt, false);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..e1294f9307
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,265 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->adjust_conf('postgresql.conf', 'primary_slot_name', undef);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Check some unmet conditions on node P
+$node_p->append_conf('postgresql.conf', q{
+wal_level = replica
+max_replication_slots = 1
+max_wal_senders = 1
+max_worker_processes = 2
+});
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary contains unmet conditions on node P');
+# Restore default settings here but only apply it after testing standby. Some
+# standby settings should not be a lower setting than on the primary.
+$node_p->append_conf('postgresql.conf', q{
+wal_level = logical
+max_replication_slots = 10
+max_wal_senders = 10
+max_worker_processes = 8
+});
+
+# Check some unmet conditions on node S
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 1
+max_logical_replication_workers = 1
+max_worker_processes = 2
+});
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'standby contains unmet conditions on node S');
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 10
+max_logical_replication_workers = 4
+max_worker_processes = 8
+});
+# Restore default settings on both servers
+$node_s->restart;
+$node_p->restart;
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.34.1

#197Euler Taveira
euler@eulerto.com
In reply to: Amit Kapila (#195)
2 attachment(s)
Re: speed up a logical replica setup

On Fri, Mar 15, 2024, at 3:34 AM, Amit Kapila wrote:

Did you consider adding options for publication/subscription/slot
names as mentioned in my previous email? As discussed in a few emails
above, it would be quite confusing for users to identify the logical
replication objects once the standby is converted to subscriber.

Yes. I was wondering to implement after v1 is pushed. I started to write a code
for it but I wasn't sure about the UI. The best approach I came up with was
multiple options in the same order. (I don't provide short options to avoid
possible portability issues with the order.) It means if I have 3 databases and
the following command-line:

pg_createsubscriber ... --database pg1 --database pg2 --database3 --publication
pubx --publication puby --publication pubz

pubx, puby and pubz are created in the database pg1, pg2, and pg3 respectively.

It seems we care only for publications created on the primary. Isn't
it possible that some of the publications have been replicated to
standby by that time, for example, in case failure happens after
creating a few publications? IIUC, we don't care for standby cleanup
after failure because it can't be used for streaming replication
anymore. So, the only choice the user has is to recreate the standby
and start the pg_createsubscriber again. This sounds questionable to
me as to whether users would like this behavior. Does anyone else have
an opinion on this point?

If it happens after creating a publication and before promotion, the cleanup
routine will drop the publications on primary and it will eventually be applied
to the standby via replication later.

I see the below note in the patch:
+    If <application>pg_createsubscriber</application> fails while processing,
+    then the data directory is likely not in a state that can be recovered. It
+    is true if the target server was promoted. In such a case, creating a new
+    standby server is recommended.

By reading this it is not completely clear whether the standby is not
recoverable in case of any error or only an error after the target
server is promoted. If others agree with this behavior then we should
write the detailed reason for this somewhere in the comments as well
unless it is already explained.

I rewrote the sentence to make it clear that only if the server is promoted,
the target server will be in a state that cannot be reused. It provides a
message saying it too.

pg_createsubscriber: target server reached the consistent state
pg_createsubscriber: hint: If pg_createsubscriber fails after this point, you
must recreate the physical replica before continuing.

I'm attaching a new version (v30) that adds:

* 3 new options (--publication, --subscription, --replication-slot) to assign
names to the objects. The --database option used to ignore duplicate names,
however, since these new options rely on the number of database options to
match the number of object name options, it is forbidden from now on. The
duplication is also forbidden for the object names to avoid errors earlier.
* rewrite the paragraph related to unusuable target server after
pg_createsubscriber fails.
* Vignesh reported an issue [1]/messages/by-id/CALDaNm3VMOi0GugGvhk3motghaFRKSWMCSE2t3YX1e+MttToxg@mail.gmail.com related to reaching the recovery stop point
before the consistent state is reached. I proposed a simple patch that fixes
the issue.

[1]: /messages/by-id/CALDaNm3VMOi0GugGvhk3motghaFRKSWMCSE2t3YX1e+MttToxg@mail.gmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v30-0001-pg_createsubscriber-creates-a-new-logical-replic.patch.gzapplication/gzip; name="=?UTF-8?Q?v30-0001-pg=5Fcreatesubscriber-creates-a-new-logical-replic.pa?= =?UTF-8?Q?tch.gz?="Download
v30-0002-Stop-the-target-server-earlier.patch.gzapplication/gzip; name="=?UTF-8?Q?v30-0002-Stop-the-target-server-earlier.patch.gz?="Download
#198Euler Taveira
euler@eulerto.com
In reply to: vignesh C (#196)
Re: speed up a logical replica setup

On Sat, Mar 16, 2024, at 10:31 AM, vignesh C wrote:

I was able to reproduce this random failure and found the following reason:
The Minimum recovery ending location 0/5000000 was more than the
recovery_target_lsn specified is "0/4001198". In few random cases the
standby applies a few more WAL records after the replication slot is
created; this leads to minimum recovery ending location being greater
than the recovery_target_lsn because of which the server will fail
with:
FATAL: requested recovery stop point is before consistent recovery point

Thanks for checking. I proposed an alternative patch for it [1]/messages/by-id/34637e7f-0330-420d-8f45-1d022962d2fe@app.fastmail.com. Can you check
it?

[1]: /messages/by-id/34637e7f-0330-420d-8f45-1d022962d2fe@app.fastmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

#199vignesh C
vignesh21@gmail.com
In reply to: Euler Taveira (#198)
Re: speed up a logical replica setup

On Sat, 16 Mar 2024 at 21:16, Euler Taveira <euler@eulerto.com> wrote:

On Sat, Mar 16, 2024, at 10:31 AM, vignesh C wrote:

I was able to reproduce this random failure and found the following reason:
The Minimum recovery ending location 0/5000000 was more than the
recovery_target_lsn specified is "0/4001198". In few random cases the
standby applies a few more WAL records after the replication slot is
created; this leads to minimum recovery ending location being greater
than the recovery_target_lsn because of which the server will fail
with:
FATAL: requested recovery stop point is before consistent recovery point

Thanks for checking. I proposed an alternative patch for it [1]. Can you check
it?

This approach looks good to me.

Regards,
Vignesh

#200Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#197)
Re: speed up a logical replica setup

On Sat, Mar 16, 2024 at 9:13 PM Euler Taveira <euler@eulerto.com> wrote:

On Fri, Mar 15, 2024, at 3:34 AM, Amit Kapila wrote:

Did you consider adding options for publication/subscription/slot
names as mentioned in my previous email? As discussed in a few emails
above, it would be quite confusing for users to identify the logical
replication objects once the standby is converted to subscriber.

Yes. I was wondering to implement after v1 is pushed. I started to write a code
for it but I wasn't sure about the UI. The best approach I came up with was
multiple options in the same order. (I don't provide short options to avoid
possible portability issues with the order.) It means if I have 3 databases and
the following command-line:

pg_createsubscriber ... --database pg1 --database pg2 --database3 --publication
pubx --publication puby --publication pubz

pubx, puby and pubz are created in the database pg1, pg2, and pg3 respectively.

With this syntax, you want to give the user the option to specify
publication/subscription/slot name for each database? If so, won't it
be too much verbose?

It seems we care only for publications created on the primary. Isn't
it possible that some of the publications have been replicated to
standby by that time, for example, in case failure happens after
creating a few publications? IIUC, we don't care for standby cleanup
after failure because it can't be used for streaming replication
anymore. So, the only choice the user has is to recreate the standby
and start the pg_createsubscriber again. This sounds questionable to
me as to whether users would like this behavior. Does anyone else have
an opinion on this point?

If it happens after creating a publication and before promotion, the cleanup
routine will drop the publications on primary and it will eventually be applied
to the standby via replication later.

Do you mean to say that the next time if user uses
pg_createsubscriber, it will be ensured that all the prior WAL will be
replicated? I think we need to ensure that after the restart of this
tool and before it attempts to create the publications again, the WAL
of the previous drop has to be replayed.

--
With Regards,
Amit Kapila.

#201Amit Kapila
amit.kapila16@gmail.com
In reply to: Amit Kapila (#200)
Re: speed up a logical replica setup

On Mon, Mar 18, 2024 at 9:37 AM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Sat, Mar 16, 2024 at 9:13 PM Euler Taveira <euler@eulerto.com> wrote:

On Fri, Mar 15, 2024, at 3:34 AM, Amit Kapila wrote:

Did you consider adding options for publication/subscription/slot
names as mentioned in my previous email? As discussed in a few emails
above, it would be quite confusing for users to identify the logical
replication objects once the standby is converted to subscriber.

Yes. I was wondering to implement after v1 is pushed. I started to write a code
for it but I wasn't sure about the UI. The best approach I came up with was
multiple options in the same order. (I don't provide short options to avoid
possible portability issues with the order.) It means if I have 3 databases and
the following command-line:

pg_createsubscriber ... --database pg1 --database pg2 --database3 --publication
pubx --publication puby --publication pubz

pubx, puby and pubz are created in the database pg1, pg2, and pg3 respectively.

With this syntax, you want to give the user the option to specify
publication/subscription/slot name for each database? If so, won't it
be too much verbose?

It seems we care only for publications created on the primary. Isn't
it possible that some of the publications have been replicated to
standby by that time, for example, in case failure happens after
creating a few publications? IIUC, we don't care for standby cleanup
after failure because it can't be used for streaming replication
anymore. So, the only choice the user has is to recreate the standby
and start the pg_createsubscriber again. This sounds questionable to
me as to whether users would like this behavior. Does anyone else have
an opinion on this point?

If it happens after creating a publication and before promotion, the cleanup
routine will drop the publications on primary and it will eventually be applied
to the standby via replication later.

Do you mean to say that the next time if user uses
pg_createsubscriber, it will be ensured that all the prior WAL will be
replicated? I think we need to ensure that after the restart of this
tool and before it attempts to create the publications again, the WAL
of the previous drop has to be replayed.

On further thinking, the WAL for such dropped publications should get
replayed eventually before the WAL for new publications (the
publications which will be created after restart) unless the required
WAL is removed on primary due to some reason.

--
With Regards,
Amit Kapila.

#202Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#197)
RE: speed up a logical replica setup

Dear Euler,

Thanks for updating the patch. Here are my comments.
I used Grammarly to proofread sentences.
(The tool strongly recommends to use active voice, but I can ignore for now)

01.

"After a successful run, the state of the target server is analagous to a fresh
logical replication setup."
a/analagous/analogous

02.

"The main difference between the logical replication setup and pg_createsubscriber
is the initial data copy."

Grammarly suggests:
"The initial data copy is the main difference between the logical replication
setup and pg_createsubscriber."

03.

"Only the synchronization phase is done, which ensures each table is brought up
to a synchronized state."

This sentence is not very clear to me. How about:
"pg_createsubscriber does only the synchronization phase, ensuring each table's
replication state is ready."

04.

"The pg_createsubscriber targets large database systems because most of the
execution time is spent making the initial data copy."

Hmm, but initial data sync by logical replication also spends most of time to
make the initial data copy. IIUC bottlenecks are a) this application must stop
and start server several times, and b) only the serial copy works. Can you
clarify them?

05.

It is better to say the internal difference between pg_createsubscriber and the
initial sync by logical replication. For example:
pg_createsubscriber uses a physical replication mechanism to ensure the standby
catches up until a certain point. Then, it converts to the standby to the
subscriber by promoting and creating subscriptions.

06.

"If these are not met an error will be reported."

Grammarly suggests:
"If these are not met, an error will be reported."

07.

"The given target data directory must have the same system identifier than the
source data directory."

Grammarly suggests:
"The given target data directory must have the same system identifier as the
source data directory."

08.

"If a standby server is running on the target data directory or it is a base
backup from the source data directory, system identifiers are the same."

This line is not needed if bullet-style is not used. The line is just a supplement,
not prerequisite.

09.

"The source server must accept connections from the target server. The source server must not be in recovery."

Grammarly suggests:
"The source server must accept connections from the target server and not be in recovery."

10.

"Publications cannot be created in a read-only cluster."

Same as 08, this line is not needed if bullet-style is not used.

11.

"pg_createsubscriber usually starts the target server with different connection
settings during the transformation steps. Hence, connections to target server
might fail."

Grammarly suggests:
"pg_createsubscriber usually starts the target server with different connection
settings during transformation. Hence, connections to the target server might fail."

12.

"During the recovery process,"

Grammarly suggests:
"During recovery,"

13.

"replicated so an error would occur."

Grammarly suggests:
"replicated, so an error would occur."

14.

"It would avoid situations in which WAL files from the source server might be
used by the target server."

Grammarly suggests:
"It would avoid situations in which the target server might use WAL files from
the source server."

15.

"a LSN"

s/a/an

16.

"of write-ahead"

s/of/of the/

17.

"specifies promote"

We can do double-quote for the word promote.

18.

"are also added so it avoids"

Grammarly suggests:
"are added to avoid"

19.

"is accepting read-write transactions"

Grammarly suggests:
"accepts read-write transactions"

20.

New options must be also documented as well. This helps not only users but also
reviewers.
(Sometimes we cannot identify that the implementation is intentinal or not.)

21.

Also, not sure the specification is good. I preferred to specify them by format
string. Because it can reduce the number of arguments and I cannot find use cases
which user want to control the name of objects.

However, your approach has a benefit which users can easily identify the generated
objects by pg_createsubscriber. How do other think?

22.

```
#define BASE_OUTPUT_DIR "pg_createsubscriber_output.d"
```

No one refers the define.

23.

```
} CreateSubscriberOptions;
...
} LogicalRepInfo;
```

Declarations after the "{" are not needed, because we do not do typedef.

22.

While seeing definitions of functions, I found that some pointers are declared
as const, but others are not. E.g., "char *lsn" in setup_recovery() won' be
changed but not the constant. Is it just missing or is there another rule?

23.

```
static int num_dbs = 0;
static int num_pubs = 0;
static int num_subs = 0;
static int num_replslots = 0;
```

I think the name is bit confusing. The number of generating publications/subscriptions/replication slots
are always same as the number of databases. They just indicate the number of
specified.

My idea is num_custom_pubs or something. Thought?

24.

```
/* standby / subscriber data directory */
static char *subscriber_dir = NULL;
```

It is bit strange that only subscriber_dir is a global variable. Caller requires
the CreateSubscriberOptions as an argument, except cleanup_objects_atexit() and
main. So, how about makeing `CreateSubscriberOptions opt` to global one?

25.

```
* Replication slots, publications and subscriptions are created. Depending on
* the step it failed, it should remove the already created objects if it is
* possible (sometimes it won't work due to a connection issue).
```

I think it should be specified here that subscriptions won't be removed with the
reason.

26.

```

/*
* If the server is promoted, there is no way to use the current setup
* again. Warn the user that a new replication setup should be done before
* trying again.
*/
```

Per comment 25, we can add a reference like "See comments atop the function"

27.

usage() was not updated based on recent changes.

28.

```
if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
{
if (dbname)
*dbname = pg_strdup(conn_opt->val);
continue;
}
```

There is a memory-leak if multiple dbname are specified in the conninfo.

29.

```
pg_prng_seed(&prng_state, (uint64) (getpid() ^ time(NULL)));
```

No need to initialize the seed every time. Can you reuse pg_prng_state?

30.

```
if (num_replslots == 0)
dbinfo[i].replslotname = pg_strdup(genname);
```

I think the straightforward way is to use the name of subscription if no name
is specified. This follows the rule for CREATE SUBSCRIPTION.

31.

```
/* Create replication slot on publisher */
if (lsn)
pg_free(lsn);
```

I think allocating/freeing memory is not so efficient.
Can we add a flag to create_logical_replication_slot() for controlling the
returning value (NULL or duplicated string)? We can use the condition (i == num_dbs-1)
as flag.

32.

```
/*
* Close the connection. If exit_on_error is true, it has an undesired
* condition and it should exit immediately.
*/
static void
disconnect_database(PGconn *conn, bool exit_on_error)
```

In case of disconnect_database(), the second argument should have different name.
If it is true, the process exits unconditionally.
Also, comments atop the function must be fixed.

33.

```
wal_level = strdup(PQgetvalue(res, 0, 0));
```

pg_strdup should be used here.

34.

```
{"config-file", required_argument, NULL, 1},
{"publication", required_argument, NULL, 2},
{"replication-slot", required_argument, NULL, 3},
{"subscription", required_argument, NULL, 4},
```

The ordering looks strange for me. According to pg_upgarade and pg_basebackup,
options which do not have short notation are listed behind.

35.

```
opt.sub_port = palloc(16);
```

Per other lines, pg_alloc() should be used.

36.

```
pg_free(opt.sub_port);
```

You said that the leak won't be concerned here. If so, why only 'p' has pg_free()?

37.

```
/* Register a function to clean up objects in case of failure */
atexit(cleanup_objects_atexit);
```

Sorry if we have already discussed. I think the registration can be moved just
before the boot of the standby. Before that, the callback will be no-op.

38.

```
/* Subscriber PID file */
snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);

/*
* If the standby server is running, stop it. Some parameters (that can
* only be set at server start) are informed by command-line options.
*/
if (stat(pidfile, &statbuf) == 0)
```

Hmm. pidfile is used only here, but it is declared in main(). Can it be
separated into another funtion like is_standby_started()?

39.

Or, we may able to introcue "restart_standby_if_needed" or something.

40.

```
* XXX this code was extracted from BootStrapXLOG().
```

So, can we extract the common part to somewhere? Since system identifier is related
with the controldata file, I think it can be located in controldata_util.c.

41.

You said like below in [1]/messages/by-id/40595e73-c7e1-463a-b8be-49792e870007@app.fastmail.com, but I could not find the related fix. Can you clarify?

That's a good point. We should state in the documentation that GUCs specified in
the command-line options are ignored during the execution.

[1]: /messages/by-id/40595e73-c7e1-463a-b8be-49792e870007@app.fastmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

#203vignesh C
vignesh21@gmail.com
In reply to: Euler Taveira (#197)
Re: speed up a logical replica setup

On Sat, 16 Mar 2024 at 21:13, Euler Taveira <euler@eulerto.com> wrote:

On Fri, Mar 15, 2024, at 3:34 AM, Amit Kapila wrote:

Did you consider adding options for publication/subscription/slot
names as mentioned in my previous email? As discussed in a few emails
above, it would be quite confusing for users to identify the logical
replication objects once the standby is converted to subscriber.

Yes. I was wondering to implement after v1 is pushed. I started to write a code
for it but I wasn't sure about the UI. The best approach I came up with was
multiple options in the same order. (I don't provide short options to avoid
possible portability issues with the order.) It means if I have 3 databases and
the following command-line:

pg_createsubscriber ... --database pg1 --database pg2 --database3 --publication
pubx --publication puby --publication pubz

pubx, puby and pubz are created in the database pg1, pg2, and pg3 respectively.

It seems we care only for publications created on the primary. Isn't
it possible that some of the publications have been replicated to
standby by that time, for example, in case failure happens after
creating a few publications? IIUC, we don't care for standby cleanup
after failure because it can't be used for streaming replication
anymore. So, the only choice the user has is to recreate the standby
and start the pg_createsubscriber again. This sounds questionable to
me as to whether users would like this behavior. Does anyone else have
an opinion on this point?

If it happens after creating a publication and before promotion, the cleanup
routine will drop the publications on primary and it will eventually be applied
to the standby via replication later.

I see the below note in the patch:
+    If <application>pg_createsubscriber</application> fails while processing,
+    then the data directory is likely not in a state that can be recovered. It
+    is true if the target server was promoted. In such a case, creating a new
+    standby server is recommended.

By reading this it is not completely clear whether the standby is not
recoverable in case of any error or only an error after the target
server is promoted. If others agree with this behavior then we should
write the detailed reason for this somewhere in the comments as well
unless it is already explained.

I rewrote the sentence to make it clear that only if the server is promoted,
the target server will be in a state that cannot be reused. It provides a
message saying it too.

pg_createsubscriber: target server reached the consistent state
pg_createsubscriber: hint: If pg_createsubscriber fails after this point, you
must recreate the physical replica before continuing.

I'm attaching a new version (v30) that adds:

* 3 new options (--publication, --subscription, --replication-slot) to assign
names to the objects. The --database option used to ignore duplicate names,
however, since these new options rely on the number of database options to
match the number of object name options, it is forbidden from now on. The
duplication is also forbidden for the object names to avoid errors earlier.
* rewrite the paragraph related to unusuable target server after
pg_createsubscriber fails.

1) Maximum size of the object name is 64, we can have a check so that
we don't specify more than the maximum allowed length:
+ case 3:
+ if (!simple_string_list_member(&opt.replslot_names, optarg))
+ {
+ simple_string_list_append(&opt.replslot_names, optarg);
+ num_replslots++;
+ }
+ else
+ {
+ pg_log_error("duplicate replication slot \"%s\"", optarg);
+ exit(1);
+ }
+ break;

If we allow something like this:
./pg_createsubscriber -U postgres -D data_N2/ -P "port=5431
user=postgres" -p 5432 -s /home/vignesh/postgres/inst/bin/ -d db1 -d
db2 -d db3 --replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes1"
--replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes2"
--replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes3"
In this case creation of replication slot will fail:
pg_createsubscriber: error: could not create replication slot
"testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes" on
database "db2": ERROR: replication slot
"testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes"
already exists

2) Similarly here too:
+ case 4:
+ if (!simple_string_list_member(&opt.sub_names, optarg))
+ {
+ simple_string_list_append(&opt.sub_names, optarg);
+ num_subs++;
+ }
+ else
+ {
+ pg_log_error("duplicate subscription \"%s\"", optarg);
+ exit(1);
+ }
+ break;

If we allow something like this:
./pg_createsubscriber -U postgres -D data_N2/ -P "port=5431
user=postgres" -p 5432 -s /home/vignesh/postgres/inst/bin/ -d db1 -d
db2 -d db3 --subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes1
--subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes2
--subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes3

Subscriptions will be created with the same name and later there will
be a problem when setting replication progress as there will be
multiple subscriptions with the same name.

Regards,
Vignesh

#204Peter Eisentraut
peter@eisentraut.org
In reply to: vignesh C (#203)
Re: speed up a logical replica setup

On 18.03.24 08:18, vignesh C wrote:

1) Maximum size of the object name is 64, we can have a check so that
we don't specify more than the maximum allowed length:
+ case 3:
+ if (!simple_string_list_member(&opt.replslot_names, optarg))
+ {
+ simple_string_list_append(&opt.replslot_names, optarg);
+ num_replslots++;
+ }
+ else
+ {
+ pg_log_error("duplicate replication slot \"%s\"", optarg);
+ exit(1);
+ }
+ break;

If we allow something like this:
./pg_createsubscriber -U postgres -D data_N2/ -P "port=5431
user=postgres" -p 5432 -s /home/vignesh/postgres/inst/bin/ -d db1 -d
db2 -d db3 --replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes1"
--replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes2"
--replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes3"
In this case creation of replication slot will fail:
pg_createsubscriber: error: could not create replication slot
"testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes" on
database "db2": ERROR: replication slot
"testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes"
already exists

I think this is fine. The server can check whether the names it is
given are of the right size. We don't need to check it again in the client.

2) Similarly here too:
+ case 4:
+ if (!simple_string_list_member(&opt.sub_names, optarg))
+ {
+ simple_string_list_append(&opt.sub_names, optarg);
+ num_subs++;
+ }
+ else
+ {
+ pg_log_error("duplicate subscription \"%s\"", optarg);
+ exit(1);
+ }
+ break;

If we allow something like this:
./pg_createsubscriber -U postgres -D data_N2/ -P "port=5431
user=postgres" -p 5432 -s /home/vignesh/postgres/inst/bin/ -d db1 -d
db2 -d db3 --subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes1
--subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes2
--subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes3

Subscriptions will be created with the same name and later there will
be a problem when setting replication progress as there will be
multiple subscriptions with the same name.

Could you clarify this problem?

#205Peter Eisentraut
peter@eisentraut.org
In reply to: Hayato Kuroda (Fujitsu) (#202)
1 attachment(s)
Re: speed up a logical replica setup

On 18.03.24 06:43, Hayato Kuroda (Fujitsu) wrote:

02.

"The main difference between the logical replication setup and pg_createsubscriber
is the initial data copy."

Grammarly suggests:
"The initial data copy is the main difference between the logical replication
setup and pg_createsubscriber."

I think that change is worse.

09.

"The source server must accept connections from the target server. The source server must not be in recovery."

Grammarly suggests:
"The source server must accept connections from the target server and not be in recovery."

I think the previous version is better.

17.

"specifies promote"

We can do double-quote for the word promote.

The v30 patch has <literal>promote</literal>, which I think is adequate.

19.

"is accepting read-write transactions"

Grammarly suggests:
"accepts read-write transactions"

I like the first one better.

20.

New options must be also documented as well. This helps not only users but also
reviewers.
(Sometimes we cannot identify that the implementation is intentinal or not.)

Which ones are missing?

21.

Also, not sure the specification is good. I preferred to specify them by format
string. Because it can reduce the number of arguments and I cannot find use cases
which user want to control the name of objects.

However, your approach has a benefit which users can easily identify the generated
objects by pg_createsubscriber. How do other think?

I think listing them explicitly is better for the first version. It's
simpler to implement and more flexible.

22.

```
#define BASE_OUTPUT_DIR "pg_createsubscriber_output.d"
```

No one refers the define.

This is gone in v30.

23.

```
} CreateSubscriberOptions;
...
} LogicalRepInfo;
```

Declarations after the "{" are not needed, because we do not do typedef.

Yeah, this is actually wrong, because as it is written now, it defines
global variables.

22.

While seeing definitions of functions, I found that some pointers are declared
as const, but others are not. E.g., "char *lsn" in setup_recovery() won' be
changed but not the constant. Is it just missing or is there another rule?

Yes, more could be done here. I have attached a patch for this. (This
also requires the just-committed 48018f1d8c.)

24.

```
/* standby / subscriber data directory */
static char *subscriber_dir = NULL;
```

It is bit strange that only subscriber_dir is a global variable. Caller requires
the CreateSubscriberOptions as an argument, except cleanup_objects_atexit() and
main. So, how about makeing `CreateSubscriberOptions opt` to global one?

Fewer global variables seem preferable. Only make global as needed.

30.

```
if (num_replslots == 0)
dbinfo[i].replslotname = pg_strdup(genname);
```

I think the straightforward way is to use the name of subscription if no name
is specified. This follows the rule for CREATE SUBSCRIPTION.

right

31.

```
/* Create replication slot on publisher */
if (lsn)
pg_free(lsn);
```

I think allocating/freeing memory is not so efficient.
Can we add a flag to create_logical_replication_slot() for controlling the
returning value (NULL or duplicated string)? We can use the condition (i == num_dbs-1)
as flag.

Nothing is even using the return value of
create_logical_replication_slot(). I think this can be removed altogether.

34.

```
{"config-file", required_argument, NULL, 1},
{"publication", required_argument, NULL, 2},
{"replication-slot", required_argument, NULL, 3},
{"subscription", required_argument, NULL, 4},
```

The ordering looks strange for me. According to pg_upgarade and pg_basebackup,
options which do not have short notation are listed behind.

35.

```
opt.sub_port = palloc(16);
```

Per other lines, pg_alloc() should be used.

Even better psprintf().

37.

```
/* Register a function to clean up objects in case of failure */
atexit(cleanup_objects_atexit);
```

Sorry if we have already discussed. I think the registration can be moved just
before the boot of the standby. Before that, the callback will be no-op.

But it can also stay where it is. What is the advantage of moving it later?

40.

```
* XXX this code was extracted from BootStrapXLOG().
```

So, can we extract the common part to somewhere? Since system identifier is related
with the controldata file, I think it can be located in controldata_util.c.

Let's leave it as is for this PG release.

Attachments:

0001-fixup-pg_createsubscriber-creates-a-new-logical-repl.patchtext/plain; charset=UTF-8; name=0001-fixup-pg_createsubscriber-creates-a-new-logical-repl.patchDownload
From d951a9f186b2162aa241f7554908b236c718154f Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Mon, 18 Mar 2024 11:54:03 +0100
Subject: [PATCH] fixup! pg_createsubscriber: creates a new logical replica
 from a standby server

Add const decorations.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 54 ++++++++++-----------
 1 file changed, 27 insertions(+), 27 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 6cc1c341214..e24ed7ef506 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -61,46 +61,46 @@ struct LogicalRepInfo
 
 static void cleanup_objects_atexit(void);
 static void usage();
-static char *get_base_conninfo(char *conninfo, char **dbname);
-static char *get_sub_conninfo(struct CreateSubscriberOptions *opt);
+static char *get_base_conninfo(const char *conninfo, char **dbname);
+static char *get_sub_conninfo(const struct CreateSubscriberOptions *opt);
 static char *get_exec_path(const char *argv0, const char *progname);
 static void check_data_directory(const char *datadir);
 static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
-static struct LogicalRepInfo *store_pub_sub_info(struct CreateSubscriberOptions *opt,
+static struct LogicalRepInfo *store_pub_sub_info(const struct CreateSubscriberOptions *opt,
 												 const char *pub_base_conninfo,
 												 const char *sub_base_conninfo);
 static PGconn *connect_database(const char *conninfo, bool exit_on_error);
 static void disconnect_database(PGconn *conn, bool exit_on_error);
 static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
-static void modify_subscriber_sysid(struct CreateSubscriberOptions *opt);
+static void modify_subscriber_sysid(const struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static char *generate_object_name(PGconn *conn);
-static void check_publisher(struct LogicalRepInfo *dbinfo);
+static void check_publisher(const struct LogicalRepInfo *dbinfo);
 static char *setup_publisher(struct LogicalRepInfo *dbinfo);
-static void check_subscriber(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(const struct LogicalRepInfo *dbinfo);
 static void setup_subscriber(struct LogicalRepInfo *dbinfo,
 							 const char *consistent_lsn);
-static void setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir,
-						   char *lsn);
+static void setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir,
+						   const char *lsn);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
-										  char *slotname);
+										  const char *slotname);
 static char *create_logical_replication_slot(PGconn *conn,
 											 struct LogicalRepInfo *dbinfo);
 static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
 								  const char *slot_name);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
-static void start_standby_server(struct CreateSubscriberOptions *opt,
+static void start_standby_server(const struct CreateSubscriberOptions *opt,
 								 bool restricted_access);
 static void stop_standby_server(const char *datadir);
 static void wait_for_end_recovery(const char *conninfo,
-								  struct CreateSubscriberOptions *opt);
+								  const struct CreateSubscriberOptions *opt);
 static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
-static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
-static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
+static void create_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo,
 									 const char *lsn);
-static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void enable_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo);
 
 #define	USEC_PER_SEC	1000000
 #define	WAIT_INTERVAL	1		/* 1 second */
@@ -244,7 +244,7 @@ usage(void)
  * dbname.
  */
 static char *
-get_base_conninfo(char *conninfo, char **dbname)
+get_base_conninfo(const char *conninfo, char **dbname)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQconninfoOption *conn_opts = NULL;
@@ -292,7 +292,7 @@ get_base_conninfo(char *conninfo, char **dbname)
  * since it starts a server with restricted access.
  */
 static char *
-get_sub_conninfo(struct CreateSubscriberOptions *opt)
+get_sub_conninfo(const struct CreateSubscriberOptions *opt)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	char	   *ret;
@@ -411,7 +411,7 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
  * setup_publisher().
  */
 static struct LogicalRepInfo *
-store_pub_sub_info(struct CreateSubscriberOptions *opt, const char *pub_base_conninfo,
+store_pub_sub_info(const struct CreateSubscriberOptions *opt, const char *pub_base_conninfo,
 				   const char *sub_base_conninfo)
 {
 	struct LogicalRepInfo *dbinfo;
@@ -603,7 +603,7 @@ get_standby_sysid(const char *datadir)
  * files from one of the systems might be used in the other one.
  */
 static void
-modify_subscriber_sysid(struct CreateSubscriberOptions *opt)
+modify_subscriber_sysid(const struct CreateSubscriberOptions *opt)
 {
 	ControlFileData *cf;
 	bool		crc_ok;
@@ -791,7 +791,7 @@ server_is_in_recovery(PGconn *conn)
  * XXX Does it not allow a synchronous replica?
  */
 static void
-check_publisher(struct LogicalRepInfo *dbinfo)
+check_publisher(const struct LogicalRepInfo *dbinfo)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -947,7 +947,7 @@ check_publisher(struct LogicalRepInfo *dbinfo)
  * will be broken at the end of this process (due to pg_resetwal).
  */
 static void
-check_subscriber(struct LogicalRepInfo *dbinfo)
+check_subscriber(const struct LogicalRepInfo *dbinfo)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -1124,7 +1124,7 @@ setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
  * Write the required recovery parameters.
  */
 static void
-setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir, char *lsn)
+setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const char *lsn)
 {
 	PGconn	   *conn;
 	PQExpBuffer recoveryconfcontents;
@@ -1184,7 +1184,7 @@ setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir, char *lsn)
  * eventually drops this replication slot later.
  */
 static void
-drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, char *slotname)
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, const char *slotname)
 {
 	PGconn	   *conn;
 
@@ -1322,7 +1322,7 @@ pg_ctl_status(const char *pg_ctl_cmd, int rc)
 }
 
 static void
-start_standby_server(struct CreateSubscriberOptions *opt, bool restricted_access)
+start_standby_server(const struct CreateSubscriberOptions *opt, bool restricted_access)
 {
 	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
 	int			rc;
@@ -1380,7 +1380,7 @@ stop_standby_server(const char *datadir)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo, struct CreateSubscriberOptions *opt)
+wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions *opt)
 {
 	PGconn	   *conn;
 	int			status = POSTMASTER_STILL_STARTING;
@@ -1575,7 +1575,7 @@ drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
  * initial location.
  */
 static void
-create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+create_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
@@ -1620,7 +1620,7 @@ create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
  * and LSN are set to invalid values for printing purposes.
  */
 static void
-set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char *lsn)
+set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, const char *lsn)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
@@ -1702,7 +1702,7 @@ set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char
  * adjusting the initial logical replication location, enable the subscription.
  */
 static void
-enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+enable_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;

base-commit: 48018f1d8c12d42b53c2a855626ee1ceb7f4ca71
prerequisite-patch-id: e4c7223061ca1e12dfa84f4a5ccdc3a9ab0c268a
-- 
2.44.0

#206Peter Eisentraut
peter@eisentraut.org
In reply to: Euler Taveira (#197)
1 attachment(s)
Re: speed up a logical replica setup

On 16.03.24 16:42, Euler Taveira wrote:

I'm attaching a new version (v30) that adds:

I have some review comments and attached a patch with some smaller
fixups (mainly message wording and avoid fixed-size string buffers).

* doc/src/sgml/ref/pg_createsubscriber.sgml

I would remove the "How It Works" section. This is not relevant to
users, and it is very detailed and will require updating whenever the
implementation changes. It could be a source code comment instead.

* src/bin/pg_basebackup/pg_createsubscriber.c

I think the connection string handling is not robust against funny
characters, like spaces, in database names etc.

Most SQL commands need to be amended for proper identifier or string
literal quoting and/or escaping.

In check_subscriber(): All these permissions checks seem problematic
to me. We shouldn't reimplement our own copy of the server's
permission checks. The server can check the permissions. And if the
permission checking in the server ever changes, then we have
inconsistencies to take care of. Also, the error messages "permission
denied" are inappropriate, because we are not doing the actual thing.
Maybe we want to do a dry-run for the benefit of the user, but then we
should do the actual thing, like try to create a replication slot, or
whatever. But I would rather just remove all this, it seems too
problematic.

In main(): The first check if the standby is running is problematic.
I think it would be better to require that the standby is initially
shut down. Consider, the standby might be running under systemd.
This tool will try to stop it, systemd will try to restart it. Let's
avoid these kinds of battles. It's also safer if we don't try to
touch running servers.

The -p option (--subscriber-port) doesn't seem to do anything. In my
testing, it always uses the compiled-in default port.

Printing all the server log lines to the terminal doesn't seem very
user-friendly. Not sure what to do about that, short of keeping a
pg_upgrade-style directory of log files. But it's ugly.

Attachments:

0001-Various-improvements.patchtext/plain; charset=UTF-8; name=0001-Various-improvements.patchDownload
From ec8e6ed6c3325a6f9fde2d1632346e212ade9c9f Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Mon, 18 Mar 2024 14:48:55 +0100
Subject: [PATCH] Various improvements

---
 src/bin/pg_basebackup/Makefile              |  2 +-
 src/bin/pg_basebackup/nls.mk                |  1 +
 src/bin/pg_basebackup/pg_createsubscriber.c | 49 +++++++++------------
 3 files changed, 23 insertions(+), 29 deletions(-)

diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index e9a920dbcda..26c53e473f5 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -49,7 +49,7 @@ all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
-pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+pg_createsubscriber: pg_createsubscriber.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
diff --git a/src/bin/pg_basebackup/nls.mk b/src/bin/pg_basebackup/nls.mk
index fc475003e8e..7870cea71ce 100644
--- a/src/bin/pg_basebackup/nls.mk
+++ b/src/bin/pg_basebackup/nls.mk
@@ -8,6 +8,7 @@ GETTEXT_FILES    = $(FRONTEND_COMMON_GETTEXT_FILES) \
                    bbstreamer_tar.c \
                    bbstreamer_zstd.c \
                    pg_basebackup.c \
+                   pg_createsubscriber.c \
                    pg_receivewal.c \
                    pg_recvlogical.c \
                    receivelog.c \
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index e24ed7ef506..91c3a2f0036 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -43,7 +43,7 @@ struct CreateSubscriberOptions
 	SimpleStringList sub_names; /* list of subscription names */
 	SimpleStringList replslot_names;	/* list of replication slot names */
 	int			recovery_timeout;	/* stop recovery after this time */
-}			CreateSubscriberOptions;
+};
 
 struct LogicalRepInfo
 {
@@ -57,7 +57,7 @@ struct LogicalRepInfo
 
 	bool		made_replslot;	/* replication slot was created */
 	bool		made_publication;	/* publication was created */
-}			LogicalRepInfo;
+};
 
 static void cleanup_objects_atexit(void);
 static void usage();
@@ -155,9 +155,9 @@ cleanup_objects_atexit(void)
 	 */
 	if (recovery_ended)
 	{
-		pg_log_warning("pg_createsubscriber failed after the end of recovery");
-		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.");
-		pg_log_warning_hint("You must recreate the physical replica before continuing.");
+		pg_log_warning("failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.  "
+							"You must recreate the physical replica before continuing.");
 	}
 
 	for (int i = 0; i < num_dbs; i++)
@@ -184,13 +184,13 @@ cleanup_objects_atexit(void)
 				 */
 				if (dbinfo[i].made_publication)
 				{
-					pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
+					pg_log_warning("publication \"%s\" in database \"%s\" on primary might be left behind",
 								   dbinfo[i].pubname, dbinfo[i].dbname);
 					pg_log_warning_hint("Consider dropping this publication before trying again.");
 				}
 				if (dbinfo[i].made_replslot)
 				{
-					pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
+					pg_log_warning("replication slot \"%s\" in database \"%s\" on primary might be left behind",
 								   dbinfo[i].replslotname, dbinfo[i].dbname);
 					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
 				}
@@ -420,7 +420,7 @@ store_pub_sub_info(const struct CreateSubscriberOptions *opt, const char *pub_ba
 	SimpleStringListCell *replslotcell = NULL;
 	int			i = 0;
 
-	dbinfo = (struct LogicalRepInfo *) pg_malloc(num_dbs * sizeof(struct LogicalRepInfo));
+	dbinfo = pg_malloc_array(struct LogicalRepInfo, num_dbs);
 
 	if (num_pubs > 0)
 		pubcell = opt->pub_names.head;
@@ -611,7 +611,7 @@ modify_subscriber_sysid(const struct CreateSubscriberOptions *opt)
 
 	char	   *cmd_str;
 
-	pg_log_info("modifying system identifier from subscriber");
+	pg_log_info("modifying system identifier of subscriber");
 
 	cf = get_controlfile(subscriber_dir, &crc_ok);
 	if (!crc_ok)
@@ -965,7 +965,7 @@ check_subscriber(const struct LogicalRepInfo *dbinfo)
 	/* The target server must be a standby */
 	if (!server_is_in_recovery(conn))
 	{
-		pg_log_error("The target server must be a standby");
+		pg_log_error("target server must be a standby");
 		disconnect_database(conn, true);
 	}
 
@@ -1217,13 +1217,11 @@ create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res = NULL;
-	char		slot_name[NAMEDATALEN];
+	const char *slot_name = dbinfo->replslotname;
 	char	   *lsn = NULL;
 
 	Assert(conn != NULL);
 
-	snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->replslotname);
-
 	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
 				slot_name, dbinfo->dbname);
 
@@ -1451,8 +1449,7 @@ wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions
 		pg_fatal("server did not end recovery");
 
 	pg_log_info("target server reached the consistent state");
-	pg_log_info_hint("If pg_createsubscriber fails after this point, "
-					 "you must recreate the physical replica before continuing.");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, you must recreate the physical replica before continuing.");
 }
 
 /*
@@ -1625,8 +1622,8 @@ set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, cons
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res;
 	Oid			suboid;
-	char		originname[NAMEDATALEN];
-	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+	char	   *originname;
+	char	   *lsnstr;
 
 	Assert(conn != NULL);
 
@@ -1653,13 +1650,12 @@ set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, cons
 	if (dry_run)
 	{
 		suboid = InvalidOid;
-		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
-				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+		lsnstr = psprintf("%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
 	}
 	else
 	{
 		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
-		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+		lsnstr = psprintf("%s", lsn);
 	}
 
 	PQclear(res);
@@ -1668,7 +1664,7 @@ set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, cons
 	 * The origin name is defined as pg_%u. %u is the subscription OID. See
 	 * ApplyWorkerMain().
 	 */
-	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+	originname = psprintf("pg_%u", suboid);
 
 	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
 				originname, lsnstr, dbinfo->dbname);
@@ -1692,6 +1688,8 @@ set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, cons
 		PQclear(res);
 	}
 
+	pg_free(originname);
+	pg_free(lsnstr);
 	destroyPQExpBuffer(str);
 }
 
@@ -1797,13 +1795,9 @@ main(int argc, char **argv)
 	opt.config_file = NULL;
 	opt.pub_conninfo_str = NULL;
 	opt.socket_dir = NULL;
-	opt.sub_port = palloc(16);
-	strcpy(opt.sub_port, DEFAULT_SUB_PORT);
+	opt.sub_port = DEFAULT_SUB_PORT;
 	opt.sub_username = NULL;
-	opt.database_names = (SimpleStringList)
-	{
-		NULL, NULL
-	};
+	opt.database_names = (SimpleStringList){0};
 	opt.recovery_timeout = 0;
 
 	/*
@@ -1847,7 +1841,6 @@ main(int argc, char **argv)
 				dry_run = true;
 				break;
 			case 'p':
-				pg_free(opt.sub_port);
 				opt.sub_port = pg_strdup(optarg);
 				break;
 			case 'P':

base-commit: 48018f1d8c12d42b53c2a855626ee1ceb7f4ca71
prerequisite-patch-id: e4c7223061ca1e12dfa84f4a5ccdc3a9ab0c268a
prerequisite-patch-id: 6cc7583799a18d1119c8ecbb400b4471c6873768
-- 
2.44.0

#207Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Peter Eisentraut (#206)
Re: speed up a logical replica setup

On 2024-Mar-18, Peter Eisentraut wrote:

* src/bin/pg_basebackup/pg_createsubscriber.c

I think the connection string handling is not robust against funny
characters, like spaces, in database names etc.

Maybe it would be easier to deal with this by passing around a struct
with keyword/value pairs that can be given to PQconnectdbParams (and
keeping dbname as a param that's passed separately, so that it can be
added when one is needed), instead of messing with the string conninfos;
then you don't have to worry about quoting.

--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"If you have nothing to say, maybe you need just the right tool to help you
not say it." (New York Times, about Microsoft PowerPoint)

#208Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Peter Eisentraut (#205)
RE: speed up a logical replica setup

Dear Peter,

Thanks for giving comments. I want to reply some of them.

17.

"specifies promote"

We can do double-quote for the word promote.

The v30 patch has <literal>promote</literal>, which I think is adequate.

Opps. Actually I did look v29 patch while firstly reviewing. Sorry for noise.

20.

New options must be also documented as well. This helps not only users but

also

reviewers.
(Sometimes we cannot identify that the implementation is intentinal or not.)

Which ones are missing?

In v29, newly added options (publication/subscription/replication-slot) was not added.
Since they have been added, please ignore.

21.

Also, not sure the specification is good. I preferred to specify them by format
string. Because it can reduce the number of arguments and I cannot find use

cases

which user want to control the name of objects.

However, your approach has a benefit which users can easily identify the

generated

objects by pg_createsubscriber. How do other think?

I think listing them explicitly is better for the first version. It's
simpler to implement and more flexible.

OK.

22.

```
#define BASE_OUTPUT_DIR

"pg_createsubscriber_output.d"

```

No one refers the define.

This is gone in v30.

I wrote due to the above reason. Please ignore...

31.

```
/* Create replication slot on publisher */
if (lsn)
pg_free(lsn);
```

I think allocating/freeing memory is not so efficient.
Can we add a flag to create_logical_replication_slot() for controlling the
returning value (NULL or duplicated string)? We can use the condition (i ==

num_dbs-1)

as flag.

Nothing is even using the return value of
create_logical_replication_slot(). I think this can be removed altogether.

37.

```
/* Register a function to clean up objects in case of failure */
atexit(cleanup_objects_atexit);
```

Sorry if we have already discussed. I think the registration can be moved just
before the boot of the standby. Before that, the callback will be no-op.

But it can also stay where it is. What is the advantage of moving it later?

I thought we could reduce the risk of bugs. Previously some bugs were reported
because the registration is too early. However, this is not a strong opinion.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#209Amit Kapila
amit.kapila16@gmail.com
In reply to: Peter Eisentraut (#206)
Re: speed up a logical replica setup

On Mon, Mar 18, 2024 at 7:22 PM Peter Eisentraut <peter@eisentraut.org> wrote:

In check_subscriber(): All these permissions checks seem problematic
to me. We shouldn't reimplement our own copy of the server's
permission checks. The server can check the permissions. And if the
permission checking in the server ever changes, then we have
inconsistencies to take care of. Also, the error messages "permission
denied" are inappropriate, because we are not doing the actual thing.
Maybe we want to do a dry-run for the benefit of the user, but then we
should do the actual thing, like try to create a replication slot, or
whatever. But I would rather just remove all this, it seems too
problematic.

If we remove all the checks then there is a possibility that we can
fail later while creating the actual subscription. For example, if
there are not sufficient max_replication_slots, then it is bound to
fail in the later steps which would be a costlier affair because by
that time the standby would have been promoted and the user won't have
any way to move forward but to re-create standby and then use this
tool again. I think here the patch tries to mimic pg_upgrade style
checks where we do some pre-checks.

This raises a question in my mind how are we expecting users to know
all these required values and configure it properly before using this
tool? IIUC, we are expecting that the user should figure out the
appropriate values for max_replication_slots,
max_logical_replication_workers, etc. by querying the number of
databases in the primary. Then either stop the standby to change these
parameters or use ALTER SYSTEM depending on the required parameters.
Similarly, there are some config requirements (like max_wal_senders,
max_replication_slots) for the primary which would be difficult for
users to know as these are tools are internal requirements.

The two possibilities that come to my mind are (a) pg_createsubscriber
should have some special mode/option using which the user can find out
all the required config settings, or (b) document how a user can find
the required settings. There are good chances of mistakes with option
(b).

--
With Regards,
Amit Kapila.

#210Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Euler Taveira (#197)
3 attachment(s)
Re: speed up a logical replica setup

Hi,

I'm attaching a new version (v30) that adds:

* 3 new options (--publication, --subscription, --replication-slot) to assign
names to the objects. The --database option used to ignore duplicate names,
however, since these new options rely on the number of database options to
match the number of object name options, it is forbidden from now on. The
duplication is also forbidden for the object names to avoid errors earlier.
* rewrite the paragraph related to unusuable target server after
pg_createsubscriber fails.
* Vignesh reported an issue [1] related to reaching the recovery stop point
before the consistent state is reached. I proposed a simple patch that fixes
the issue.

[1] /messages/by-id/CALDaNm3VMOi0GugGvhk3motghaFRKSWMCSE2t3YX1e+MttToxg@mail.gmail.com

I have added a top-up patch v30-0003. The issue in [1]/messages/by-id/CAHv8Rj+5mzK9Jt+7ECogJzfm5czvDCCd5jO1_rCx0bTEYpBE5g@mail.gmail.com still exists in
the v30 patch. I was not able to come up with an approach to handle it
in the code, so I have added it to the documentation in the warning
section. Thoughts?
I am not changing the version as I have not made any changes in
v30-0001 and v30-0002.

[1]: /messages/by-id/CAHv8Rj+5mzK9Jt+7ECogJzfm5czvDCCd5jO1_rCx0bTEYpBE5g@mail.gmail.com

Thanks and regards,
Shlok Kyal

Attachments:

v30-0001-pg_createsubscriber-creates-a-new-logical-replic.patchapplication/octet-stream; name=v30-0001-pg_createsubscriber-creates-a-new-logical-replic.patchDownload
From 979ac94190035baee454f6667710a8fb67c3d93d Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v30 1/3] pg_createsubscriber: creates a new logical replica
 from a standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber). All tables
in the specified database(s) are included in the logical replication
setup. A pair of publication and subscription objects are created for
each database.

The main advantage of pg_createsubscriber over the common logical
replication setup is the initial data copy. It also reduces the catchup
phase.

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
using FOR ALL TABLES and a replication slot for each specified database.
Write recovery parameters into the target data directory and start the
target server. It specifies the LSN of the last replication slot
(replication start point) up to which the recovery will proceed. Wait
until the target server is promoted. Create one subscription per
specified database (using publication and replication slot created in a
previous step) on the target server. Set the replication progress to the
replication start point for each subscription. Enable the subscription
for each specified database on the target server. And finally, change
the system identifier on the target server.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  525 ++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2131 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  326 +++
 8 files changed, 3012 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..642213b5a4
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,525 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. All tables in the specified
+    database are included in the logical replication setup. A pair of
+    publication and subscription objects are created for each database. It
+    must be run at the target server.
+  </para>
+
+  <para>
+    After a successful run, the state of the target server is analagous to a
+    fresh logical replication setup. The main difference between the logical
+    replication setup and <application>pg_createsubscriber</application>
+    is the initial data copy. Only the synchronization phase is done, which
+    ensures each table is brought up to a synchronized state.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because most of the execution time is spent making the initial data
+   copy. For smaller databases,
+   <link linkend="logical-replication">initial data synchronization</link> is
+   recommended.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--config-file=<replaceable class="parameter">filename</replaceable></option></term>
+      <listitem>
+       <para>
+        Use the specified main server configuration file for the target
+        data directory. The <application>pg_createsubscriber</application>
+        uses internally the <application>pg_ctl</application> command to start
+        and stop the target server. It allows you to specify the actual
+        <filename>postgresql.conf</filename> configuration file if it is stored
+        outside the data directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--publication=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The publication name to set up the logical replication. Multiple
+        publications can be specified by writing multiple
+        <option>--publication</option> switches. The number of publication
+        names must match the number of specified databases, otherwise an error
+        is reported. The order of the multiple publication name switches must
+        match the order of database switches. If this option is not specified,
+        a generated name is assigned to the publication name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--subscription=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The subscription name to set up the logical replication. Multiple
+        subscriptions can be specified by writing multiple
+        <option>--subscription</option> switches. The number of subscription
+        names must match the number of specified databases, otherwise an error
+        is reported. The order of the multiple subscription name switches must
+        match the order of database switches. If this option is not specified,
+        a generated name is assigned to the subscription name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--replication-slot=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The replication slot name to set up the logical replication. Multiple
+        replication slots can be specified by writing multiple
+        <option>--replication-slot</option> switches. The number of replication
+        slot names must match the number of specified databases, otherwise an
+        error is reported. The order of the multiple replication slot name
+        switches must match the order of database switches. If this option is
+        not specified, a generated name is assigned to the replication slot
+        name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   There are some prerequisites for
+   <application>pg_createsubscriber</application> to convert the target server
+   into a logical replica. If these are not met an error will be reported. The
+   source and target servers must have the same major version as the
+   <application>pg_createsubscriber</application>. The given target data
+   directory must have the same system identifier than the source data
+   directory. If a standby server is running on the target data directory or it
+   is a base backup from the source data directory, system identifiers are the
+   same. The given database user for the target data directory must have
+   privileges for creating
+   <link linkend="sql-createsubscription">subscriptions</link> and using
+   <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+  </para>
+
+  <para>
+   The target server must be used as a physical standby. The target server
+   must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   and
+   <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases. The target server must have
+   <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+   configured to a value greater than the number of specified databases. The
+   target server must accept local connections.
+  </para>
+
+  <para>
+   The source server must accept connections from the target server. The
+   source server must not be in recovery. Publications cannot be created in a
+   read-only cluster. The source server must have
+   <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+   <literal>logical</literal>.
+   The source server must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases plus existing replication slots. The source server must have
+   <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases and existing <literal>walsender</literal> processes.
+  </para>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails after the target
+    server was promoted, then the data directory is likely not in a state that
+    can be recovered. In such case, creating a new standby server is
+    recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during the transformation steps.
+    Hence, regular connections to the target server should fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Since DDL commands are not replicated by logical replication, avoid
+    executing DDL commands that change the database schema while running
+    <application>pg_createsubscriber</application>. If the target server has
+    already been converted to logical replica, the DDL commands must not be
+    replicated which might cause an error.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    are removed. The removal might fail if the target server cannot connect to
+    the source server. In such a case, a warning message will inform the
+    objects left. If the target server is running, it will be stopped.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    If the target server is a synchronous replica, transaction commits on the
+    primary might wait for replication while running
+    <application>pg_createsubscriber</application>.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which WAL files from the source server might be used by the target server.
+    If the target server has a standby, replication will break and a fresh
+    standby should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is already running, stop it because some parameters can only
+     be set at server start.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. If any of the prerequisites are not met,
+     <application>pg_createsubscriber</application> will terminate with an
+     error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication is created using
+     <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>.
+     If <option>publication-name</option> option is not specified, it has the
+     following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameter:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     If <option>replication-slot-name</option> is not specified, the
+     replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+     The last replication slot LSN is used as a stopping point in the
+     <xref linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication start point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies a LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are also added so it avoids unexpected behavior during the recovery
+     process such as end of the recovery as soon as a consistent state is
+     reached (WAL should be applied until the replication start location) and
+     multiple recovery targets that can cause a failure. This step finishes
+     once the server ends standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     If <option>subscription-name</option> is not specified, the subscription
+     has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     It does not copy existing data from the source server. It does not create
+     a replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the replication
+     start point before starting the replication.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the replication start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the replication start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the replication start point. This is the exact LSN to be used
+     as a initial replication location for each subscription. The replication
+     origin name is obtained since the subscription was created. The replication
+     origin name and the replication start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the replication start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..e9a920dbcd 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..6cc1c34121
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2131 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/pg_prng.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	"50432"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *config_file;	/* configuration file */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	char	   *sub_port;		/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	SimpleStringList pub_names; /* list of publication names */
+	SimpleStringList sub_names; /* list of subscription names */
+	SimpleStringList replslot_names;	/* list of replication slot names */
+	int			recovery_timeout;	/* stop recovery after this time */
+}			CreateSubscriberOptions;
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name */
+	char	   *replslotname;	/* replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+}			LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_sub_conninfo(struct CreateSubscriberOptions *opt);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(struct CreateSubscriberOptions *opt,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static char *generate_object_name(PGconn *conn);
+static void check_publisher(struct LogicalRepInfo *dbinfo);
+static char *setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static void setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir,
+						   char *lsn);
+static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
+										  char *slotname);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(struct CreateSubscriberOptions *opt,
+								 bool restricted_access);
+static void stop_standby_server(const char *datadir);
+static void wait_for_end_recovery(const char *conninfo,
+								  struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+static int	num_pubs = 0;
+static int	num_subs = 0;
+static int	num_replslots = 0;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+/* standby / subscriber data directory */
+static char *subscriber_dir = NULL;
+
+static bool recovery_ended = false;
+static bool standby_running = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("pg_createsubscriber failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.");
+		pg_log_warning_hint("You must recreate the physical replica before continuing.");
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			PGconn	   *conn;
+
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].replslotname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].replslotname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+
+	if (standby_running)
+		stop_standby_server(subscriber_dir);
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %s)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_("     --config-file=FILENAME          use specified main server configuration\n"
+			 "                                     file when running target cluster\n"));
+	printf(_("     --publication=NAME              publication name\n"));
+	printf(_("     --replication-slot=NAME         replication slot name\n"));
+	printf(_("     --subscription=NAME             subscription name\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Build a subscriber connection string. Only a few parameters are supported
+ * since it starts a server with restricted access.
+ */
+static char *
+get_sub_conninfo(struct CreateSubscriberOptions *opt)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	appendPQExpBuffer(buf, "port=%s", opt->sub_port);
+#if !defined(WIN32)
+	appendPQExpBuffer(buf, " host=%s", opt->socket_dir);
+#endif
+	if (opt->sub_username != NULL)
+		appendPQExpBuffer(buf, " user=%s", opt->sub_username);
+	appendPQExpBuffer(buf, " fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ *
+ * If publication, replication slot and subscription names were specified,
+ * store it here. Otherwise, a generated name will be assigned to the object in
+ * setup_publisher().
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(struct CreateSubscriberOptions *opt, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	SimpleStringListCell *pubcell = NULL;
+	SimpleStringListCell *subcell = NULL;
+	SimpleStringListCell *replslotcell = NULL;
+	int			i = 0;
+
+	dbinfo = (struct LogicalRepInfo *) pg_malloc(num_dbs * sizeof(struct LogicalRepInfo));
+
+	if (num_pubs > 0)
+		pubcell = opt->pub_names.head;
+	if (num_subs > 0)
+		subcell = opt->sub_names.head;
+	if (num_replslots > 0)
+		replslotcell = opt->replslot_names.head;
+
+	for (SimpleStringListCell *cell = opt->database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		if (num_pubs > 0)
+			dbinfo[i].pubname = pubcell->val;
+		else
+			dbinfo[i].pubname = NULL;
+		if (num_replslots > 0)
+			dbinfo[i].replslotname = replslotcell->val;
+		else
+			dbinfo[i].replslotname = NULL;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		if (num_subs > 0)
+			dbinfo[i].subname = subcell->val;
+		else
+			dbinfo[i].subname = NULL;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): publication: %s ; replication slot: %s ; connection string: %s", i,
+					 dbinfo[i].pubname ? dbinfo[i].pubname : "(auto)",
+					 dbinfo[i].replslotname ? dbinfo[i].replslotname : "(auto)",
+					 dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): subscription: %s ; connection string: %s", i,
+					 dbinfo[i].subname ? dbinfo[i].subname : "(auto)",
+					 dbinfo[i].subconninfo);
+
+		if (num_pubs > 0)
+			pubcell = pubcell->next;
+		if (num_subs > 0)
+			subcell = subcell->next;
+		if (num_replslots > 0)
+			replslotcell = replslotcell->next;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   subscriber_dir, DEVNULL);
+
+	pg_log_debug("pg_resetwal command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Generate an object name using a prefix, database oid and a random integer.
+ * It is used in case the user does not specify an object name (publication,
+ * subscription, replication slot).
+ */
+static char *
+generate_object_name(PGconn *conn)
+{
+	PGresult   *res;
+	Oid			oid;
+	uint32		rand;
+	pg_prng_state prng_state;
+	char	   *objname;
+
+	pg_prng_seed(&prng_state, (uint64) (getpid() ^ time(NULL)));
+
+	res = PQexec(conn,
+				 "SELECT oid FROM pg_catalog.pg_database "
+				 "WHERE datname = pg_catalog.current_database()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain database OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	/* Database OID */
+	oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+	PQclear(res);
+
+	/* Random unsigned integer */
+	rand = pg_prng_uint32(&prng_state);
+
+	/*
+	 * Build the object name. The name must not exceed NAMEDATALEN - 1. This
+	 * current schema uses a maximum of 40 characters (20 + 10 + 1 + 8 +
+	 * '\0').
+	 */
+	objname = psprintf("pg_createsubscriber_%u_%x", oid, rand);
+
+	return objname;
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication. Returns the LSN from latest replication slot. It will be the
+ * replication start point that is used to adjust the subscriptions (see
+ * set_replication_progress).
+ */
+static char *
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	char	   *lsn = NULL;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		char	   *genname = NULL;
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		/*
+		 * If an object name was not specified as command-line options, assign
+		 * a generated object name.
+		 */
+		if (num_pubs == 0 || num_subs == 0 || num_replslots == 0)
+			genname = generate_object_name(conn);
+		if (num_pubs == 0)
+			dbinfo[i].pubname = pg_strdup(genname);
+		if (num_subs == 0)
+			dbinfo[i].subname = pg_strdup(genname);
+		if (num_replslots == 0)
+			dbinfo[i].replslotname = pg_strdup(genname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/* Create replication slot on publisher */
+		if (lsn)
+			pg_free(lsn);
+		lsn = create_logical_replication_slot(conn, &dbinfo[i]);
+		if (lsn != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						dbinfo[i].replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+
+	return lsn;
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ *
+ * XXX Does it not allow a synchronous replica?
+ */
+static void
+check_publisher(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	bool		failed = false;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		failed = true;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
+		failed = true;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ *
+ * XXX Does it not allow a time-delayed replica?
+ *
+ * XXX In a cascaded replication scenario (P -> S -> C), if the target server
+ * is S, it cannot detect there is a replica (server C) because server S starts
+ * accepting only local connections and server C cannot connect to it. Hence,
+ * there is not a reliable way to provide a suitable error saying the server C
+ * will be broken at the end of this process (due to pg_resetwal).
+ */
+static void
+check_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+	bool		failed = false;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("The target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		failed = true;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * replication setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Write the required recovery parameters.
+ */
+static void
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir, char *lsn)
+{
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	/*
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection. The primary_conninfo is generated using the
+	 * connection settings.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * The subscriber is not running yet. In dry run mode, the recovery
+	 * parameters *won't* be written. An invalid LSN is used for printing
+	 * purposes. Additional recovery parameters are added here. It avoids
+	 * unexpected behavior such as end of recovery as soon as a consistent
+	 * state is reached (recovery_target) and failure due to multiple recovery
+	 * targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  lsn);
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+}
+
+/*
+ * Drop physical replication slot on primary if the standby was using it. After
+ * the transformation, it has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, char *slotname)
+{
+	PGconn	   *conn;
+
+	/* Replication slot does not exist, do nothing */
+	if (!primary_slot_name)
+		return;
+
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn != NULL)
+	{
+		drop_replication_slot(conn, &dbinfo[0], slotname);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop replication slot \"%s\" on primary",
+					   slotname);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->replslotname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(struct CreateSubscriberOptions *opt, bool restricted_access)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	int			rc;
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, subscriber_dir);
+	if (restricted_access)
+	{
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %s\"", opt->sub_port);
+#if !defined(WIN32)
+
+		/*
+		 * An empty listen_addresses list means the server does not listen on
+		 * any IP interfaces; only Unix-domain sockets can be used to connect
+		 * to the server. Prevent external connections to minimize the chance
+		 * of failure.
+		 */
+		appendPQExpBufferStr(pg_ctl_cmd, " -o \"-c listen_addresses='' -c unix_socket_permissions=0700");
+		if (opt->socket_dir)
+			appendPQExpBuffer(pg_ctl_cmd, " -c unix_socket_directories='%s'",
+							  opt->socket_dir);
+		appendPQExpBufferChar(pg_ctl_cmd, '"');
+#endif
+	}
+	if (opt->config_file != NULL)
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-c config_file=%s\"",
+						  opt->config_file);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	standby_running = true;
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	standby_running = false;
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	10
+
+	pg_log_info("waiting for the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid and
+		 * a random number.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it.  It
+ * is not required to copy data. The subscription will be created but it will
+ * not be enabled now. That's because the replication progress must be set and
+ * the replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, enabled = false, "
+					  "slot_name = '%s', copy_data = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname,
+					  dbinfo->replslotname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the last
+ * replication slot that was created. The goal is to set up the initial
+ * location for the logical replication that is the exact LSN that the
+ * subscriber was promoted. Once the subscription is enabled it will start
+ * streaming from that location onwards.  In dry run mode, the subscription OID
+ * and LSN are set to invalid values for printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"config-file", required_argument, NULL, 1},
+		{"publication", required_argument, NULL, 2},
+		{"replication-slot", required_argument, NULL, 3},
+		{"subscription", required_argument, NULL, 4},
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pub_base_conninfo;
+	char	   *sub_base_conninfo;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	char	   *consistent_lsn;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	subscriber_dir = NULL;
+	opt.config_file = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = palloc(16);
+	strcpy(opt.sub_port, DEFAULT_SUB_PORT);
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:np:P:s:t:U:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				else
+				{
+					pg_log_error("duplicate database \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				pg_free(opt.sub_port);
+				opt.sub_port = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				canonicalize_path(opt.socket_dir);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			case 1:
+				opt.config_file = pg_strdup(optarg);
+				break;
+			case 2:
+				if (!simple_string_list_member(&opt.pub_names, optarg))
+				{
+					simple_string_list_append(&opt.pub_names, optarg);
+					num_pubs++;
+				}
+				else
+				{
+					pg_log_error("duplicate publication \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 3:
+				if (!simple_string_list_member(&opt.replslot_names, optarg))
+				{
+					simple_string_list_append(&opt.replslot_names, optarg);
+					num_replslots++;
+				}
+				else
+				{
+					pg_log_error("duplicate replication slot \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 4:
+				if (!simple_string_list_member(&opt.sub_names, optarg))
+				{
+					simple_string_list_append(&opt.sub_names, optarg);
+					num_subs++;
+				}
+				else
+				{
+					pg_log_error("duplicate subscription \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/* Any non-option arguments? */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* Required arguments */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* If socket directory is not provided, use the current directory */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_sub_conninfo(&opt);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Number of object names must match number of databases */
+	if (num_pubs > 0 && num_pubs != num_dbs)
+	{
+		pg_log_error("wrong number of publication names");
+		pg_log_error_hint("Number of publication names (%d) must match number of database names (%d).",
+						  num_pubs, num_dbs);
+		exit(1);
+	}
+	if (num_subs > 0 && num_subs != num_dbs)
+	{
+		pg_log_error("wrong number of subscription names");
+		pg_log_error_hint("Number of subscription names (%d) must match number of database names (%d).",
+						  num_subs, num_dbs);
+		exit(1);
+	}
+	if (num_replslots > 0 && num_replslots != num_dbs)
+	{
+		pg_log_error("wrong number of replication slot names");
+		pg_log_error_hint("Number of replication slot names (%d) must match number of database names (%d).",
+						  num_replslots, num_dbs);
+		exit(1);
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(&opt, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * If the standby server is running, stop it. Some parameters (that can
+	 * only be set at server start) are informed by command-line options.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(subscriber_dir);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	consistent_lsn = setup_publisher(dbinfo);
+
+	/* Write the required recovery parameters */
+	setup_recovery(dbinfo, subscriber_dir, consistent_lsn);
+
+	/*
+	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("stopping and starting the subscriber");
+	stop_standby_server(subscriber_dir);
+	start_standby_server(&opt, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the replication start
+	 * point to the LSN reported by setup_publisher().  It also cleans up
+	 * publications created by this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/* Remove primary_slot_name if it exists on primary */
+	drop_primary_replication_slot(dbinfo, primary_slot_name);
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(&opt);
+
+	/*
+	 * In dry run mode, the server is restarted with the provided command-line
+	 * options so validation can be applied in the target server. In order to
+	 * preserve the initial state of the server (running), start it without
+	 * the command-line options.
+	 */
+	if (dry_run)
+		start_standby_server(&opt, false);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..ecf503a7d0
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,326 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--database', 'pg1',
+		'--database', 'pg1'
+	],
+	'duplicate database name');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'duplicate publication name');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of publication names');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo2',
+		'--subscription', 'bar1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of subscription names');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo2',
+		'--subscription', 'bar1',
+		'--subscription', 'bar2',
+		'--replication-slot', 'baz1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of replication slot names');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->adjust_conf('postgresql.conf', 'primary_slot_name', undef);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Check some unmet conditions on node P
+$node_p->append_conf('postgresql.conf', q{
+wal_level = replica
+max_replication_slots = 1
+max_wal_senders = 1
+max_worker_processes = 2
+});
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary contains unmet conditions on node P');
+# Restore default settings here but only apply it after testing standby. Some
+# standby settings should not be a lower setting than on the primary.
+$node_p->append_conf('postgresql.conf', q{
+wal_level = logical
+max_replication_slots = 10
+max_wal_senders = 10
+max_worker_processes = 8
+});
+
+# Check some unmet conditions on node S
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 1
+max_logical_replication_workers = 1
+max_worker_processes = 2
+});
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'standby contains unmet conditions on node S');
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 10
+max_logical_replication_workers = 4
+max_worker_processes = 8
+});
+# Restore default settings on both servers
+$node_s->restart;
+$node_p->restart;
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--publication', 'pub1',
+		'--publication', 'pub2',
+		'--subscription', 'sub1',
+		'--subscription', 'sub2',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--replication-slot', 'replslot1'
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.34.1

v30-0002-Stop-the-target-server-earlier.patchapplication/octet-stream; name=v30-0002-Stop-the-target-server-earlier.patchDownload
From b974cba7919678a8c11d2f1f89ee0f2d82f0b4e9 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Sat, 16 Mar 2024 11:58:11 -0300
Subject: [PATCH v30 2/3] Stop the target server earlier

Since the recovery process requires that it reaches a consistent state
before considering the recovery stop point, stop the server before
creating the replication slots since the last replication slot is its
recovery stop point.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 6cc1c34121..34ec7c8505 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -2074,6 +2074,16 @@ main(int argc, char **argv)
 	 */
 	check_publisher(dbinfo);
 
+	/*
+	 * Stop the target server. The recovery process requires that the server
+	 * reaches a consistent state before targeting the recovery stop point.
+	 * Make sure a consistent state is reached (stop the target server
+	 * guarantees it) *before* creating the replication slots in
+	 * setup_publisher().
+	 */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(subscriber_dir);
+
 	/*
 	 * Create the required objects for each database on publisher. This step
 	 * is here mainly because if we stop the standby we cannot verify if the
@@ -2086,11 +2096,10 @@ main(int argc, char **argv)
 	setup_recovery(dbinfo, subscriber_dir, consistent_lsn);
 
 	/*
-	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * Start subscriber so the recovery parameters will take effect. Wait
 	 * until accepting connections.
 	 */
-	pg_log_info("stopping and starting the subscriber");
-	stop_standby_server(subscriber_dir);
+	pg_log_info("starting the subscriber");
 	start_standby_server(&opt, true);
 
 	/* Waiting the subscriber to be promoted */
-- 
2.34.1

v30-0003-Document-a-limitation-of-pg_createsubscriber.patchapplication/octet-stream; name=v30-0003-Document-a-limitation-of-pg_createsubscriber.patchDownload
From 86a8718ecfbe3a8397a150ea524a54ebfa3dab70 Mon Sep 17 00:00:00 2001
From: Ubuntu <shlok.kyal@gmail.com>
Date: Tue, 19 Mar 2024 16:35:27 +0530
Subject: [PATCH v30 3/3] Document a limitation of pg_createsubscriber

Document a limitation of pg_createsubscriber
---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 642213b5a4..1afd53e904 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -369,6 +369,18 @@ PostgreSQL documentation
     If the target server has a standby, replication will break and a fresh
     standby should be created.
    </para>
+
+   <para>
+    If the target server is not direct standby of the source server and none of the 
+    parent standby of target server has a replication slot, the replication between 
+    target server and its standby may break and a logical replication between target 
+    server and source server is set up.
+    For example: Node A, B and C are in cascade physical replication without 
+    replication slot. And if pg_createsubscriber is run on Node C as target server and
+    Node A as source server, the execution is successful and the physical replication 
+    between Node B and Node C breaks and a logical replication is setup between Node A 
+    and Node C.
+   </para> 
   </warning>
 
  </refsect1>
-- 
2.34.1

#211Peter Eisentraut
peter@eisentraut.org
In reply to: Amit Kapila (#209)
Re: speed up a logical replica setup

On 19.03.24 08:05, Amit Kapila wrote:

On Mon, Mar 18, 2024 at 7:22 PM Peter Eisentraut <peter@eisentraut.org> wrote:

In check_subscriber(): All these permissions checks seem problematic
to me. We shouldn't reimplement our own copy of the server's
permission checks. The server can check the permissions. And if the
permission checking in the server ever changes, then we have
inconsistencies to take care of. Also, the error messages "permission
denied" are inappropriate, because we are not doing the actual thing.
Maybe we want to do a dry-run for the benefit of the user, but then we
should do the actual thing, like try to create a replication slot, or
whatever. But I would rather just remove all this, it seems too
problematic.

If we remove all the checks then there is a possibility that we can
fail later while creating the actual subscription. For example, if
there are not sufficient max_replication_slots, then it is bound to
fail in the later steps which would be a costlier affair because by
that time the standby would have been promoted and the user won't have
any way to move forward but to re-create standby and then use this
tool again. I think here the patch tries to mimic pg_upgrade style
checks where we do some pre-checks.

I think checking for required parameter settings is fine. My concern is
with the code before that, that does
pg_has_role/has_database_privilege/has_function_privilege.

#212Peter Eisentraut
peter@eisentraut.org
In reply to: Shlok Kyal (#210)
Re: speed up a logical replica setup

On 19.03.24 12:26, Shlok Kyal wrote:

I'm attaching a new version (v30) that adds:

* 3 new options (--publication, --subscription, --replication-slot) to assign
names to the objects. The --database option used to ignore duplicate names,
however, since these new options rely on the number of database options to
match the number of object name options, it is forbidden from now on. The
duplication is also forbidden for the object names to avoid errors earlier.
* rewrite the paragraph related to unusuable target server after
pg_createsubscriber fails.
* Vignesh reported an issue [1] related to reaching the recovery stop point
before the consistent state is reached. I proposed a simple patch that fixes
the issue.

[1] /messages/by-id/CALDaNm3VMOi0GugGvhk3motghaFRKSWMCSE2t3YX1e+MttToxg@mail.gmail.com

I have added a top-up patch v30-0003. The issue in [1] still exists in
the v30 patch. I was not able to come up with an approach to handle it
in the code, so I have added it to the documentation in the warning
section. Thoughts?

Seems acceptable to me. pg_createsubscriber will probably always have
some restrictions and unsupported edge cases like that. We can't
support everything, so documenting is ok.

#213vignesh C
vignesh21@gmail.com
In reply to: Peter Eisentraut (#204)
Re: speed up a logical replica setup

On Mon, 18 Mar 2024 at 16:36, Peter Eisentraut <peter@eisentraut.org> wrote:

On 18.03.24 08:18, vignesh C wrote:

1) Maximum size of the object name is 64, we can have a check so that
we don't specify more than the maximum allowed length:
+ case 3:
+ if (!simple_string_list_member(&opt.replslot_names, optarg))
+ {
+ simple_string_list_append(&opt.replslot_names, optarg);
+ num_replslots++;
+ }
+ else
+ {
+ pg_log_error("duplicate replication slot \"%s\"", optarg);
+ exit(1);
+ }
+ break;

If we allow something like this:
./pg_createsubscriber -U postgres -D data_N2/ -P "port=5431
user=postgres" -p 5432 -s /home/vignesh/postgres/inst/bin/ -d db1 -d
db2 -d db3 --replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes1"
--replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes2"
--replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes3"
In this case creation of replication slot will fail:
pg_createsubscriber: error: could not create replication slot
"testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes" on
database "db2": ERROR: replication slot
"testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes"
already exists

I think this is fine. The server can check whether the names it is
given are of the right size. We don't need to check it again in the client.

2) Similarly here too:
+ case 4:
+ if (!simple_string_list_member(&opt.sub_names, optarg))
+ {
+ simple_string_list_append(&opt.sub_names, optarg);
+ num_subs++;
+ }
+ else
+ {
+ pg_log_error("duplicate subscription \"%s\"", optarg);
+ exit(1);
+ }
+ break;

If we allow something like this:
./pg_createsubscriber -U postgres -D data_N2/ -P "port=5431
user=postgres" -p 5432 -s /home/vignesh/postgres/inst/bin/ -d db1 -d
db2 -d db3 --subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes1
--subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes2
--subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes3

Subscriptions will be created with the same name and later there will
be a problem when setting replication progress as there will be
multiple subscriptions with the same name.

Could you clarify this problem?

In this case the subscriptions name specified is more than the allowed
name, the subscription name will be truncated and both the
subscription for db1 and db2 will have same name like below:
db2=# select subname, subdbid from pg_subscription;
subname | subdbid
-----------------------------------------------------------------+---------
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes | 16384
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes | 16385

Now we try to set the replication origin of the subscriptions to a
consistent lsn from the following:
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
const char *lsn)
+{
+       PQExpBuffer str = createPQExpBuffer();
+       PGresult   *res;
+       Oid                     suboid;
+       char            originname[NAMEDATALEN];
+       char            lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+       Assert(conn != NULL);
+
+       appendPQExpBuffer(str,
+                                         "SELECT oid FROM
pg_catalog.pg_subscription "
+                                         "WHERE subname = '%s'",
+                                         dbinfo->subname);
+
+       res = PQexec(conn, str->data);
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               pg_log_error("could not obtain subscription OID: %s",
+                                        PQresultErrorMessage(res));
+               disconnect_database(conn, true);
+       }
+
+       if (PQntuples(res) != 1 && !dry_run)
+       {
+               pg_log_error("could not obtain subscription OID: got
%d rows, expected %d rows",
+                                        PQntuples(res), 1);
+               disconnect_database(conn, true);
+       }

Since the subscription name is truncated, we will have multiple
records returned for the above query which results in failure with:
pg_createsubscriber: error: could not obtain subscription OID: got 2
rows, expected 1 rows
pg_createsubscriber: warning: pg_createsubscriber failed after the end
of recovery
pg_createsubscriber: hint: The target server cannot be used as a
physical replica anymore.
pg_createsubscriber: hint: You must recreate the physical replica
before continuing.

The problem with this failure is that standby has been promoted
already and we will have to re-create the physica replica again.

If you are not planning to have the checks for name length, this could
alternatively be fixed by including database id also while querying
pg_subscription like below in set_replication_progress function:
appendPQExpBuffer(str,
"SELECT oid FROM pg_catalog.pg_subscription \n"
"WHERE subname = '%s' AND subdbid = (SELECT oid FROM
pg_catalog.pg_database WHERE datname = '%s')",
dbinfo->subname,
dbinfo->dbname);

I have verified this fixes the issue.

Regards,
Vignesh

#214Peter Eisentraut
peter@eisentraut.org
In reply to: vignesh C (#213)
Re: speed up a logical replica setup

On 19.03.24 16:24, vignesh C wrote:

The problem with this failure is that standby has been promoted
already and we will have to re-create the physica replica again.

If you are not planning to have the checks for name length, this could
alternatively be fixed by including database id also while querying
pg_subscription like below in set_replication_progress function:
appendPQExpBuffer(str,
"SELECT oid FROM pg_catalog.pg_subscription \n"
"WHERE subname = '%s' AND subdbid = (SELECT oid FROM
pg_catalog.pg_database WHERE datname = '%s')",
dbinfo->subname,
dbinfo->dbname);

Yes, this is more correct anyway, because subscription names are
per-database, not global. So you should be able to make
pg_createsubscriber use the same subscription name for each database.

#215Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Shlok Kyal (#210)
3 attachment(s)
Re: speed up a logical replica setup

Hi,

I'm attaching a new version (v30) that adds:

* 3 new options (--publication, --subscription, --replication-slot) to assign
names to the objects. The --database option used to ignore duplicate names,
however, since these new options rely on the number of database options to
match the number of object name options, it is forbidden from now on. The
duplication is also forbidden for the object names to avoid errors earlier.
* rewrite the paragraph related to unusuable target server after
pg_createsubscriber fails.
* Vignesh reported an issue [1] related to reaching the recovery stop point
before the consistent state is reached. I proposed a simple patch that fixes
the issue.

[1] /messages/by-id/CALDaNm3VMOi0GugGvhk3motghaFRKSWMCSE2t3YX1e+MttToxg@mail.gmail.com

I have added a top-up patch v30-0003. The issue in [1] still exists in
the v30 patch. I was not able to come up with an approach to handle it
in the code, so I have added it to the documentation in the warning
section. Thoughts?
I am not changing the version as I have not made any changes in
v30-0001 and v30-0002.

[1]: /messages/by-id/CAHv8Rj+5mzK9Jt+7ECogJzfm5czvDCCd5jO1_rCx0bTEYpBE5g@mail.gmail.com

There was some whitespace error in the v30-0003 patch. Updated the patch.

Thanks and regards,
Shlok Kyal

Attachments:

v31-0001-pg_createsubscriber-creates-a-new-logical-replic.patchapplication/x-patch; name=v31-0001-pg_createsubscriber-creates-a-new-logical-replic.patchDownload
From d6717bcaf94302c0d41eabd448802b1fae6167d9 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v31 1/3] pg_createsubscriber: creates a new logical replica
 from a standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber). All tables
in the specified database(s) are included in the logical replication
setup. A pair of publication and subscription objects are created for
each database.

The main advantage of pg_createsubscriber over the common logical
replication setup is the initial data copy. It also reduces the catchup
phase.

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
using FOR ALL TABLES and a replication slot for each specified database.
Write recovery parameters into the target data directory and start the
target server. It specifies the LSN of the last replication slot
(replication start point) up to which the recovery will proceed. Wait
until the target server is promoted. Create one subscription per
specified database (using publication and replication slot created in a
previous step) on the target server. Set the replication progress to the
replication start point for each subscription. Enable the subscription
for each specified database on the target server. And finally, change
the system identifier on the target server.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  525 ++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2131 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  326 +++
 8 files changed, 3012 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..642213b5a4
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,525 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. All tables in the specified
+    database are included in the logical replication setup. A pair of
+    publication and subscription objects are created for each database. It
+    must be run at the target server.
+  </para>
+
+  <para>
+    After a successful run, the state of the target server is analagous to a
+    fresh logical replication setup. The main difference between the logical
+    replication setup and <application>pg_createsubscriber</application>
+    is the initial data copy. Only the synchronization phase is done, which
+    ensures each table is brought up to a synchronized state.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because most of the execution time is spent making the initial data
+   copy. For smaller databases,
+   <link linkend="logical-replication">initial data synchronization</link> is
+   recommended.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--config-file=<replaceable class="parameter">filename</replaceable></option></term>
+      <listitem>
+       <para>
+        Use the specified main server configuration file for the target
+        data directory. The <application>pg_createsubscriber</application>
+        uses internally the <application>pg_ctl</application> command to start
+        and stop the target server. It allows you to specify the actual
+        <filename>postgresql.conf</filename> configuration file if it is stored
+        outside the data directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--publication=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The publication name to set up the logical replication. Multiple
+        publications can be specified by writing multiple
+        <option>--publication</option> switches. The number of publication
+        names must match the number of specified databases, otherwise an error
+        is reported. The order of the multiple publication name switches must
+        match the order of database switches. If this option is not specified,
+        a generated name is assigned to the publication name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--subscription=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The subscription name to set up the logical replication. Multiple
+        subscriptions can be specified by writing multiple
+        <option>--subscription</option> switches. The number of subscription
+        names must match the number of specified databases, otherwise an error
+        is reported. The order of the multiple subscription name switches must
+        match the order of database switches. If this option is not specified,
+        a generated name is assigned to the subscription name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--replication-slot=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The replication slot name to set up the logical replication. Multiple
+        replication slots can be specified by writing multiple
+        <option>--replication-slot</option> switches. The number of replication
+        slot names must match the number of specified databases, otherwise an
+        error is reported. The order of the multiple replication slot name
+        switches must match the order of database switches. If this option is
+        not specified, a generated name is assigned to the replication slot
+        name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   There are some prerequisites for
+   <application>pg_createsubscriber</application> to convert the target server
+   into a logical replica. If these are not met an error will be reported. The
+   source and target servers must have the same major version as the
+   <application>pg_createsubscriber</application>. The given target data
+   directory must have the same system identifier than the source data
+   directory. If a standby server is running on the target data directory or it
+   is a base backup from the source data directory, system identifiers are the
+   same. The given database user for the target data directory must have
+   privileges for creating
+   <link linkend="sql-createsubscription">subscriptions</link> and using
+   <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+  </para>
+
+  <para>
+   The target server must be used as a physical standby. The target server
+   must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   and
+   <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases. The target server must have
+   <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+   configured to a value greater than the number of specified databases. The
+   target server must accept local connections.
+  </para>
+
+  <para>
+   The source server must accept connections from the target server. The
+   source server must not be in recovery. Publications cannot be created in a
+   read-only cluster. The source server must have
+   <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+   <literal>logical</literal>.
+   The source server must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases plus existing replication slots. The source server must have
+   <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases and existing <literal>walsender</literal> processes.
+  </para>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails after the target
+    server was promoted, then the data directory is likely not in a state that
+    can be recovered. In such case, creating a new standby server is
+    recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during the transformation steps.
+    Hence, regular connections to the target server should fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Since DDL commands are not replicated by logical replication, avoid
+    executing DDL commands that change the database schema while running
+    <application>pg_createsubscriber</application>. If the target server has
+    already been converted to logical replica, the DDL commands must not be
+    replicated which might cause an error.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    are removed. The removal might fail if the target server cannot connect to
+    the source server. In such a case, a warning message will inform the
+    objects left. If the target server is running, it will be stopped.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    If the target server is a synchronous replica, transaction commits on the
+    primary might wait for replication while running
+    <application>pg_createsubscriber</application>.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which WAL files from the source server might be used by the target server.
+    If the target server has a standby, replication will break and a fresh
+    standby should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is already running, stop it because some parameters can only
+     be set at server start.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. If any of the prerequisites are not met,
+     <application>pg_createsubscriber</application> will terminate with an
+     error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication is created using
+     <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>.
+     If <option>publication-name</option> option is not specified, it has the
+     following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameter:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     If <option>replication-slot-name</option> is not specified, the
+     replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+     The last replication slot LSN is used as a stopping point in the
+     <xref linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication start point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies a LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are also added so it avoids unexpected behavior during the recovery
+     process such as end of the recovery as soon as a consistent state is
+     reached (WAL should be applied until the replication start location) and
+     multiple recovery targets that can cause a failure. This step finishes
+     once the server ends standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     If <option>subscription-name</option> is not specified, the subscription
+     has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     It does not copy existing data from the source server. It does not create
+     a replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the replication
+     start point before starting the replication.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the replication start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the replication start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the replication start point. This is the exact LSN to be used
+     as a initial replication location for each subscription. The replication
+     origin name is obtained since the subscription was created. The replication
+     origin name and the replication start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the replication start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..e9a920dbcd 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..6cc1c34121
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2131 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/pg_prng.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	"50432"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *config_file;	/* configuration file */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	char	   *sub_port;		/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	SimpleStringList pub_names; /* list of publication names */
+	SimpleStringList sub_names; /* list of subscription names */
+	SimpleStringList replslot_names;	/* list of replication slot names */
+	int			recovery_timeout;	/* stop recovery after this time */
+}			CreateSubscriberOptions;
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name */
+	char	   *replslotname;	/* replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+}			LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_sub_conninfo(struct CreateSubscriberOptions *opt);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(struct CreateSubscriberOptions *opt,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static char *generate_object_name(PGconn *conn);
+static void check_publisher(struct LogicalRepInfo *dbinfo);
+static char *setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static void setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir,
+						   char *lsn);
+static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
+										  char *slotname);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(struct CreateSubscriberOptions *opt,
+								 bool restricted_access);
+static void stop_standby_server(const char *datadir);
+static void wait_for_end_recovery(const char *conninfo,
+								  struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+static int	num_pubs = 0;
+static int	num_subs = 0;
+static int	num_replslots = 0;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+/* standby / subscriber data directory */
+static char *subscriber_dir = NULL;
+
+static bool recovery_ended = false;
+static bool standby_running = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("pg_createsubscriber failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.");
+		pg_log_warning_hint("You must recreate the physical replica before continuing.");
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			PGconn	   *conn;
+
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].replslotname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].replslotname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+
+	if (standby_running)
+		stop_standby_server(subscriber_dir);
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %s)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_("     --config-file=FILENAME          use specified main server configuration\n"
+			 "                                     file when running target cluster\n"));
+	printf(_("     --publication=NAME              publication name\n"));
+	printf(_("     --replication-slot=NAME         replication slot name\n"));
+	printf(_("     --subscription=NAME             subscription name\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Build a subscriber connection string. Only a few parameters are supported
+ * since it starts a server with restricted access.
+ */
+static char *
+get_sub_conninfo(struct CreateSubscriberOptions *opt)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	appendPQExpBuffer(buf, "port=%s", opt->sub_port);
+#if !defined(WIN32)
+	appendPQExpBuffer(buf, " host=%s", opt->socket_dir);
+#endif
+	if (opt->sub_username != NULL)
+		appendPQExpBuffer(buf, " user=%s", opt->sub_username);
+	appendPQExpBuffer(buf, " fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ *
+ * If publication, replication slot and subscription names were specified,
+ * store it here. Otherwise, a generated name will be assigned to the object in
+ * setup_publisher().
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(struct CreateSubscriberOptions *opt, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	SimpleStringListCell *pubcell = NULL;
+	SimpleStringListCell *subcell = NULL;
+	SimpleStringListCell *replslotcell = NULL;
+	int			i = 0;
+
+	dbinfo = (struct LogicalRepInfo *) pg_malloc(num_dbs * sizeof(struct LogicalRepInfo));
+
+	if (num_pubs > 0)
+		pubcell = opt->pub_names.head;
+	if (num_subs > 0)
+		subcell = opt->sub_names.head;
+	if (num_replslots > 0)
+		replslotcell = opt->replslot_names.head;
+
+	for (SimpleStringListCell *cell = opt->database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		if (num_pubs > 0)
+			dbinfo[i].pubname = pubcell->val;
+		else
+			dbinfo[i].pubname = NULL;
+		if (num_replslots > 0)
+			dbinfo[i].replslotname = replslotcell->val;
+		else
+			dbinfo[i].replslotname = NULL;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		if (num_subs > 0)
+			dbinfo[i].subname = subcell->val;
+		else
+			dbinfo[i].subname = NULL;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): publication: %s ; replication slot: %s ; connection string: %s", i,
+					 dbinfo[i].pubname ? dbinfo[i].pubname : "(auto)",
+					 dbinfo[i].replslotname ? dbinfo[i].replslotname : "(auto)",
+					 dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): subscription: %s ; connection string: %s", i,
+					 dbinfo[i].subname ? dbinfo[i].subname : "(auto)",
+					 dbinfo[i].subconninfo);
+
+		if (num_pubs > 0)
+			pubcell = pubcell->next;
+		if (num_subs > 0)
+			subcell = subcell->next;
+		if (num_replslots > 0)
+			replslotcell = replslotcell->next;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   subscriber_dir, DEVNULL);
+
+	pg_log_debug("pg_resetwal command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Generate an object name using a prefix, database oid and a random integer.
+ * It is used in case the user does not specify an object name (publication,
+ * subscription, replication slot).
+ */
+static char *
+generate_object_name(PGconn *conn)
+{
+	PGresult   *res;
+	Oid			oid;
+	uint32		rand;
+	pg_prng_state prng_state;
+	char	   *objname;
+
+	pg_prng_seed(&prng_state, (uint64) (getpid() ^ time(NULL)));
+
+	res = PQexec(conn,
+				 "SELECT oid FROM pg_catalog.pg_database "
+				 "WHERE datname = pg_catalog.current_database()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain database OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	/* Database OID */
+	oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+	PQclear(res);
+
+	/* Random unsigned integer */
+	rand = pg_prng_uint32(&prng_state);
+
+	/*
+	 * Build the object name. The name must not exceed NAMEDATALEN - 1. This
+	 * current schema uses a maximum of 40 characters (20 + 10 + 1 + 8 +
+	 * '\0').
+	 */
+	objname = psprintf("pg_createsubscriber_%u_%x", oid, rand);
+
+	return objname;
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication. Returns the LSN from latest replication slot. It will be the
+ * replication start point that is used to adjust the subscriptions (see
+ * set_replication_progress).
+ */
+static char *
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	char	   *lsn = NULL;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		char	   *genname = NULL;
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		/*
+		 * If an object name was not specified as command-line options, assign
+		 * a generated object name.
+		 */
+		if (num_pubs == 0 || num_subs == 0 || num_replslots == 0)
+			genname = generate_object_name(conn);
+		if (num_pubs == 0)
+			dbinfo[i].pubname = pg_strdup(genname);
+		if (num_subs == 0)
+			dbinfo[i].subname = pg_strdup(genname);
+		if (num_replslots == 0)
+			dbinfo[i].replslotname = pg_strdup(genname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/* Create replication slot on publisher */
+		if (lsn)
+			pg_free(lsn);
+		lsn = create_logical_replication_slot(conn, &dbinfo[i]);
+		if (lsn != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						dbinfo[i].replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+
+	return lsn;
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ *
+ * XXX Does it not allow a synchronous replica?
+ */
+static void
+check_publisher(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	bool		failed = false;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		failed = true;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
+		failed = true;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ *
+ * XXX Does it not allow a time-delayed replica?
+ *
+ * XXX In a cascaded replication scenario (P -> S -> C), if the target server
+ * is S, it cannot detect there is a replica (server C) because server S starts
+ * accepting only local connections and server C cannot connect to it. Hence,
+ * there is not a reliable way to provide a suitable error saying the server C
+ * will be broken at the end of this process (due to pg_resetwal).
+ */
+static void
+check_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+	bool		failed = false;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("The target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		failed = true;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * replication setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Write the required recovery parameters.
+ */
+static void
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir, char *lsn)
+{
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	/*
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection. The primary_conninfo is generated using the
+	 * connection settings.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * The subscriber is not running yet. In dry run mode, the recovery
+	 * parameters *won't* be written. An invalid LSN is used for printing
+	 * purposes. Additional recovery parameters are added here. It avoids
+	 * unexpected behavior such as end of recovery as soon as a consistent
+	 * state is reached (recovery_target) and failure due to multiple recovery
+	 * targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  lsn);
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+}
+
+/*
+ * Drop physical replication slot on primary if the standby was using it. After
+ * the transformation, it has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, char *slotname)
+{
+	PGconn	   *conn;
+
+	/* Replication slot does not exist, do nothing */
+	if (!primary_slot_name)
+		return;
+
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn != NULL)
+	{
+		drop_replication_slot(conn, &dbinfo[0], slotname);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop replication slot \"%s\" on primary",
+					   slotname);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->replslotname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(struct CreateSubscriberOptions *opt, bool restricted_access)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	int			rc;
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, subscriber_dir);
+	if (restricted_access)
+	{
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %s\"", opt->sub_port);
+#if !defined(WIN32)
+
+		/*
+		 * An empty listen_addresses list means the server does not listen on
+		 * any IP interfaces; only Unix-domain sockets can be used to connect
+		 * to the server. Prevent external connections to minimize the chance
+		 * of failure.
+		 */
+		appendPQExpBufferStr(pg_ctl_cmd, " -o \"-c listen_addresses='' -c unix_socket_permissions=0700");
+		if (opt->socket_dir)
+			appendPQExpBuffer(pg_ctl_cmd, " -c unix_socket_directories='%s'",
+							  opt->socket_dir);
+		appendPQExpBufferChar(pg_ctl_cmd, '"');
+#endif
+	}
+	if (opt->config_file != NULL)
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-c config_file=%s\"",
+						  opt->config_file);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	standby_running = true;
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	standby_running = false;
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	10
+
+	pg_log_info("waiting for the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid and
+		 * a random number.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it.  It
+ * is not required to copy data. The subscription will be created but it will
+ * not be enabled now. That's because the replication progress must be set and
+ * the replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, enabled = false, "
+					  "slot_name = '%s', copy_data = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname,
+					  dbinfo->replslotname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the last
+ * replication slot that was created. The goal is to set up the initial
+ * location for the logical replication that is the exact LSN that the
+ * subscriber was promoted. Once the subscription is enabled it will start
+ * streaming from that location onwards.  In dry run mode, the subscription OID
+ * and LSN are set to invalid values for printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"config-file", required_argument, NULL, 1},
+		{"publication", required_argument, NULL, 2},
+		{"replication-slot", required_argument, NULL, 3},
+		{"subscription", required_argument, NULL, 4},
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pub_base_conninfo;
+	char	   *sub_base_conninfo;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	char	   *consistent_lsn;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	subscriber_dir = NULL;
+	opt.config_file = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = palloc(16);
+	strcpy(opt.sub_port, DEFAULT_SUB_PORT);
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:np:P:s:t:U:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				else
+				{
+					pg_log_error("duplicate database \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				pg_free(opt.sub_port);
+				opt.sub_port = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				canonicalize_path(opt.socket_dir);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			case 1:
+				opt.config_file = pg_strdup(optarg);
+				break;
+			case 2:
+				if (!simple_string_list_member(&opt.pub_names, optarg))
+				{
+					simple_string_list_append(&opt.pub_names, optarg);
+					num_pubs++;
+				}
+				else
+				{
+					pg_log_error("duplicate publication \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 3:
+				if (!simple_string_list_member(&opt.replslot_names, optarg))
+				{
+					simple_string_list_append(&opt.replslot_names, optarg);
+					num_replslots++;
+				}
+				else
+				{
+					pg_log_error("duplicate replication slot \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 4:
+				if (!simple_string_list_member(&opt.sub_names, optarg))
+				{
+					simple_string_list_append(&opt.sub_names, optarg);
+					num_subs++;
+				}
+				else
+				{
+					pg_log_error("duplicate subscription \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/* Any non-option arguments? */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* Required arguments */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* If socket directory is not provided, use the current directory */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_sub_conninfo(&opt);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Number of object names must match number of databases */
+	if (num_pubs > 0 && num_pubs != num_dbs)
+	{
+		pg_log_error("wrong number of publication names");
+		pg_log_error_hint("Number of publication names (%d) must match number of database names (%d).",
+						  num_pubs, num_dbs);
+		exit(1);
+	}
+	if (num_subs > 0 && num_subs != num_dbs)
+	{
+		pg_log_error("wrong number of subscription names");
+		pg_log_error_hint("Number of subscription names (%d) must match number of database names (%d).",
+						  num_subs, num_dbs);
+		exit(1);
+	}
+	if (num_replslots > 0 && num_replslots != num_dbs)
+	{
+		pg_log_error("wrong number of replication slot names");
+		pg_log_error_hint("Number of replication slot names (%d) must match number of database names (%d).",
+						  num_replslots, num_dbs);
+		exit(1);
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(&opt, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * If the standby server is running, stop it. Some parameters (that can
+	 * only be set at server start) are informed by command-line options.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(subscriber_dir);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	consistent_lsn = setup_publisher(dbinfo);
+
+	/* Write the required recovery parameters */
+	setup_recovery(dbinfo, subscriber_dir, consistent_lsn);
+
+	/*
+	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("stopping and starting the subscriber");
+	stop_standby_server(subscriber_dir);
+	start_standby_server(&opt, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the replication start
+	 * point to the LSN reported by setup_publisher().  It also cleans up
+	 * publications created by this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/* Remove primary_slot_name if it exists on primary */
+	drop_primary_replication_slot(dbinfo, primary_slot_name);
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(&opt);
+
+	/*
+	 * In dry run mode, the server is restarted with the provided command-line
+	 * options so validation can be applied in the target server. In order to
+	 * preserve the initial state of the server (running), start it without
+	 * the command-line options.
+	 */
+	if (dry_run)
+		start_standby_server(&opt, false);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..ecf503a7d0
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,326 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--database', 'pg1',
+		'--database', 'pg1'
+	],
+	'duplicate database name');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'duplicate publication name');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of publication names');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo2',
+		'--subscription', 'bar1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of subscription names');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo2',
+		'--subscription', 'bar1',
+		'--subscription', 'bar2',
+		'--replication-slot', 'baz1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of replication slot names');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->adjust_conf('postgresql.conf', 'primary_slot_name', undef);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Check some unmet conditions on node P
+$node_p->append_conf('postgresql.conf', q{
+wal_level = replica
+max_replication_slots = 1
+max_wal_senders = 1
+max_worker_processes = 2
+});
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary contains unmet conditions on node P');
+# Restore default settings here but only apply it after testing standby. Some
+# standby settings should not be a lower setting than on the primary.
+$node_p->append_conf('postgresql.conf', q{
+wal_level = logical
+max_replication_slots = 10
+max_wal_senders = 10
+max_worker_processes = 8
+});
+
+# Check some unmet conditions on node S
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 1
+max_logical_replication_workers = 1
+max_worker_processes = 2
+});
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'standby contains unmet conditions on node S');
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 10
+max_logical_replication_workers = 4
+max_worker_processes = 8
+});
+# Restore default settings on both servers
+$node_s->restart;
+$node_p->restart;
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--publication', 'pub1',
+		'--publication', 'pub2',
+		'--subscription', 'sub1',
+		'--subscription', 'sub2',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--replication-slot', 'replslot1'
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.34.1

v31-0002-Stop-the-target-server-earlier.patchapplication/x-patch; name=v31-0002-Stop-the-target-server-earlier.patchDownload
From 86495414cbe59e66203ccf311de5af7541aa1be4 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Sat, 16 Mar 2024 11:58:11 -0300
Subject: [PATCH v31 2/3] Stop the target server earlier

Since the recovery process requires that it reaches a consistent state
before considering the recovery stop point, stop the server before
creating the replication slots since the last replication slot is its
recovery stop point.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 6cc1c34121..34ec7c8505 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -2074,6 +2074,16 @@ main(int argc, char **argv)
 	 */
 	check_publisher(dbinfo);
 
+	/*
+	 * Stop the target server. The recovery process requires that the server
+	 * reaches a consistent state before targeting the recovery stop point.
+	 * Make sure a consistent state is reached (stop the target server
+	 * guarantees it) *before* creating the replication slots in
+	 * setup_publisher().
+	 */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(subscriber_dir);
+
 	/*
 	 * Create the required objects for each database on publisher. This step
 	 * is here mainly because if we stop the standby we cannot verify if the
@@ -2086,11 +2096,10 @@ main(int argc, char **argv)
 	setup_recovery(dbinfo, subscriber_dir, consistent_lsn);
 
 	/*
-	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * Start subscriber so the recovery parameters will take effect. Wait
 	 * until accepting connections.
 	 */
-	pg_log_info("stopping and starting the subscriber");
-	stop_standby_server(subscriber_dir);
+	pg_log_info("starting the subscriber");
 	start_standby_server(&opt, true);
 
 	/* Waiting the subscriber to be promoted */
-- 
2.34.1

v31-0003-Document-a-limitation-of-pg_createsubscriber.patchapplication/x-patch; name=v31-0003-Document-a-limitation-of-pg_createsubscriber.patchDownload
From 7d60893f7d94e064b33e66f01e0f9ed44230f38d Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 19 Mar 2024 16:35:27 +0530
Subject: [PATCH v31 3/3] Document a limitation of pg_createsubscriber

Document a limitation of pg_createsubscriber
---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 642213b5a4..7e1c5e25da 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -369,6 +369,18 @@ PostgreSQL documentation
     If the target server has a standby, replication will break and a fresh
     standby should be created.
    </para>
+
+   <para>
+    If the target server is not direct standby of the source server and none of the
+    parent standby of target server has a replication slot, the replication between
+    target server and its standby may break and a logical replication between target
+    server and source server is set up.
+    For example: Node A, B and C are in cascade physical replication without
+    replication slot. And if pg_createsubscriber is run on Node C as target server and
+    Node A as source server, the execution is successful and the physical replication
+    between Node B and Node C breaks and a logical replication is setup between Node A
+    and Node C.
+   </para>
   </warning>
 
  </refsect1>
-- 
2.34.1

#216Shubham Khanna
khannashubham1197@gmail.com
In reply to: vignesh C (#213)
4 attachment(s)
Re: speed up a logical replica setup

On Tue, Mar 19, 2024 at 8:54 PM vignesh C <vignesh21@gmail.com> wrote:

On Mon, 18 Mar 2024 at 16:36, Peter Eisentraut <peter@eisentraut.org> wrote:

On 18.03.24 08:18, vignesh C wrote:

1) Maximum size of the object name is 64, we can have a check so that
we don't specify more than the maximum allowed length:
+ case 3:
+ if (!simple_string_list_member(&opt.replslot_names, optarg))
+ {
+ simple_string_list_append(&opt.replslot_names, optarg);
+ num_replslots++;
+ }
+ else
+ {
+ pg_log_error("duplicate replication slot \"%s\"", optarg);
+ exit(1);
+ }
+ break;

If we allow something like this:
./pg_createsubscriber -U postgres -D data_N2/ -P "port=5431
user=postgres" -p 5432 -s /home/vignesh/postgres/inst/bin/ -d db1 -d
db2 -d db3 --replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes1"
--replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes2"
--replication-slot="testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes3"
In this case creation of replication slot will fail:
pg_createsubscriber: error: could not create replication slot
"testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes" on
database "db2": ERROR: replication slot
"testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes"
already exists

I think this is fine. The server can check whether the names it is
given are of the right size. We don't need to check it again in the client.

2) Similarly here too:
+ case 4:
+ if (!simple_string_list_member(&opt.sub_names, optarg))
+ {
+ simple_string_list_append(&opt.sub_names, optarg);
+ num_subs++;
+ }
+ else
+ {
+ pg_log_error("duplicate subscription \"%s\"", optarg);
+ exit(1);
+ }
+ break;

If we allow something like this:
./pg_createsubscriber -U postgres -D data_N2/ -P "port=5431
user=postgres" -p 5432 -s /home/vignesh/postgres/inst/bin/ -d db1 -d
db2 -d db3 --subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes1
--subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes2
--subscription=testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes3

Subscriptions will be created with the same name and later there will
be a problem when setting replication progress as there will be
multiple subscriptions with the same name.

Could you clarify this problem?

In this case the subscriptions name specified is more than the allowed
name, the subscription name will be truncated and both the
subscription for db1 and db2 will have same name like below:
db2=# select subname, subdbid from pg_subscription;
subname | subdbid
-----------------------------------------------------------------+---------
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes | 16384
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttes | 16385

Now we try to set the replication origin of the subscriptions to a
consistent lsn from the following:
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
const char *lsn)
+{
+       PQExpBuffer str = createPQExpBuffer();
+       PGresult   *res;
+       Oid                     suboid;
+       char            originname[NAMEDATALEN];
+       char            lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+       Assert(conn != NULL);
+
+       appendPQExpBuffer(str,
+                                         "SELECT oid FROM
pg_catalog.pg_subscription "
+                                         "WHERE subname = '%s'",
+                                         dbinfo->subname);
+
+       res = PQexec(conn, str->data);
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               pg_log_error("could not obtain subscription OID: %s",
+                                        PQresultErrorMessage(res));
+               disconnect_database(conn, true);
+       }
+
+       if (PQntuples(res) != 1 && !dry_run)
+       {
+               pg_log_error("could not obtain subscription OID: got
%d rows, expected %d rows",
+                                        PQntuples(res), 1);
+               disconnect_database(conn, true);
+       }

Since the subscription name is truncated, we will have multiple
records returned for the above query which results in failure with:
pg_createsubscriber: error: could not obtain subscription OID: got 2
rows, expected 1 rows
pg_createsubscriber: warning: pg_createsubscriber failed after the end
of recovery
pg_createsubscriber: hint: The target server cannot be used as a
physical replica anymore.
pg_createsubscriber: hint: You must recreate the physical replica
before continuing.

The problem with this failure is that standby has been promoted
already and we will have to re-create the physica replica again.

If you are not planning to have the checks for name length, this could
alternatively be fixed by including database id also while querying
pg_subscription like below in set_replication_progress function:
appendPQExpBuffer(str,
"SELECT oid FROM pg_catalog.pg_subscription \n"
"WHERE subname = '%s' AND subdbid = (SELECT oid FROM
pg_catalog.pg_database WHERE datname = '%s')",
dbinfo->subname,
dbinfo->dbname);

The attached patch has the changes to handle the same.

Thanks and Regards,
Shubham Khanna.

Attachments:

v31-0001-pg_createsubscriber-creates-a-new-logical-replic.patchapplication/octet-stream; name=v31-0001-pg_createsubscriber-creates-a-new-logical-replic.patchDownload
From d6717bcaf94302c0d41eabd448802b1fae6167d9 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v31 1/3] pg_createsubscriber: creates a new logical replica
 from a standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber). All tables
in the specified database(s) are included in the logical replication
setup. A pair of publication and subscription objects are created for
each database.

The main advantage of pg_createsubscriber over the common logical
replication setup is the initial data copy. It also reduces the catchup
phase.

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
using FOR ALL TABLES and a replication slot for each specified database.
Write recovery parameters into the target data directory and start the
target server. It specifies the LSN of the last replication slot
(replication start point) up to which the recovery will proceed. Wait
until the target server is promoted. Create one subscription per
specified database (using publication and replication slot created in a
previous step) on the target server. Set the replication progress to the
replication start point for each subscription. Enable the subscription
for each specified database on the target server. And finally, change
the system identifier on the target server.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  525 ++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2131 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  326 +++
 8 files changed, 3012 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..642213b5a4
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,525 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. All tables in the specified
+    database are included in the logical replication setup. A pair of
+    publication and subscription objects are created for each database. It
+    must be run at the target server.
+  </para>
+
+  <para>
+    After a successful run, the state of the target server is analagous to a
+    fresh logical replication setup. The main difference between the logical
+    replication setup and <application>pg_createsubscriber</application>
+    is the initial data copy. Only the synchronization phase is done, which
+    ensures each table is brought up to a synchronized state.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because most of the execution time is spent making the initial data
+   copy. For smaller databases,
+   <link linkend="logical-replication">initial data synchronization</link> is
+   recommended.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--config-file=<replaceable class="parameter">filename</replaceable></option></term>
+      <listitem>
+       <para>
+        Use the specified main server configuration file for the target
+        data directory. The <application>pg_createsubscriber</application>
+        uses internally the <application>pg_ctl</application> command to start
+        and stop the target server. It allows you to specify the actual
+        <filename>postgresql.conf</filename> configuration file if it is stored
+        outside the data directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--publication=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The publication name to set up the logical replication. Multiple
+        publications can be specified by writing multiple
+        <option>--publication</option> switches. The number of publication
+        names must match the number of specified databases, otherwise an error
+        is reported. The order of the multiple publication name switches must
+        match the order of database switches. If this option is not specified,
+        a generated name is assigned to the publication name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--subscription=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The subscription name to set up the logical replication. Multiple
+        subscriptions can be specified by writing multiple
+        <option>--subscription</option> switches. The number of subscription
+        names must match the number of specified databases, otherwise an error
+        is reported. The order of the multiple subscription name switches must
+        match the order of database switches. If this option is not specified,
+        a generated name is assigned to the subscription name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--replication-slot=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The replication slot name to set up the logical replication. Multiple
+        replication slots can be specified by writing multiple
+        <option>--replication-slot</option> switches. The number of replication
+        slot names must match the number of specified databases, otherwise an
+        error is reported. The order of the multiple replication slot name
+        switches must match the order of database switches. If this option is
+        not specified, a generated name is assigned to the replication slot
+        name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   There are some prerequisites for
+   <application>pg_createsubscriber</application> to convert the target server
+   into a logical replica. If these are not met an error will be reported. The
+   source and target servers must have the same major version as the
+   <application>pg_createsubscriber</application>. The given target data
+   directory must have the same system identifier than the source data
+   directory. If a standby server is running on the target data directory or it
+   is a base backup from the source data directory, system identifiers are the
+   same. The given database user for the target data directory must have
+   privileges for creating
+   <link linkend="sql-createsubscription">subscriptions</link> and using
+   <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+  </para>
+
+  <para>
+   The target server must be used as a physical standby. The target server
+   must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   and
+   <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases. The target server must have
+   <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+   configured to a value greater than the number of specified databases. The
+   target server must accept local connections.
+  </para>
+
+  <para>
+   The source server must accept connections from the target server. The
+   source server must not be in recovery. Publications cannot be created in a
+   read-only cluster. The source server must have
+   <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+   <literal>logical</literal>.
+   The source server must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases plus existing replication slots. The source server must have
+   <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases and existing <literal>walsender</literal> processes.
+  </para>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails after the target
+    server was promoted, then the data directory is likely not in a state that
+    can be recovered. In such case, creating a new standby server is
+    recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during the transformation steps.
+    Hence, regular connections to the target server should fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Since DDL commands are not replicated by logical replication, avoid
+    executing DDL commands that change the database schema while running
+    <application>pg_createsubscriber</application>. If the target server has
+    already been converted to logical replica, the DDL commands must not be
+    replicated which might cause an error.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    are removed. The removal might fail if the target server cannot connect to
+    the source server. In such a case, a warning message will inform the
+    objects left. If the target server is running, it will be stopped.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    If the target server is a synchronous replica, transaction commits on the
+    primary might wait for replication while running
+    <application>pg_createsubscriber</application>.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which WAL files from the source server might be used by the target server.
+    If the target server has a standby, replication will break and a fresh
+    standby should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is already running, stop it because some parameters can only
+     be set at server start.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. If any of the prerequisites are not met,
+     <application>pg_createsubscriber</application> will terminate with an
+     error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication is created using
+     <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>.
+     If <option>publication-name</option> option is not specified, it has the
+     following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameter:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     If <option>replication-slot-name</option> is not specified, the
+     replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+     The last replication slot LSN is used as a stopping point in the
+     <xref linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication start point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies a LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are also added so it avoids unexpected behavior during the recovery
+     process such as end of the recovery as soon as a consistent state is
+     reached (WAL should be applied until the replication start location) and
+     multiple recovery targets that can cause a failure. This step finishes
+     once the server ends standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     If <option>subscription-name</option> is not specified, the subscription
+     has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     It does not copy existing data from the source server. It does not create
+     a replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the replication
+     start point before starting the replication.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the replication start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the replication start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the replication start point. This is the exact LSN to be used
+     as a initial replication location for each subscription. The replication
+     origin name is obtained since the subscription was created. The replication
+     origin name and the replication start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the replication start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..e9a920dbcd 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: $(WIN32RES) pg_createsubscriber.o | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..6cc1c34121
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2131 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/pg_prng.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	"50432"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *config_file;	/* configuration file */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	char	   *sub_port;		/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	SimpleStringList pub_names; /* list of publication names */
+	SimpleStringList sub_names; /* list of subscription names */
+	SimpleStringList replslot_names;	/* list of replication slot names */
+	int			recovery_timeout;	/* stop recovery after this time */
+}			CreateSubscriberOptions;
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name */
+	char	   *replslotname;	/* replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+}			LogicalRepInfo;
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(char *conninfo, char **dbname);
+static char *get_sub_conninfo(struct CreateSubscriberOptions *opt);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(struct CreateSubscriberOptions *opt,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static char *generate_object_name(PGconn *conn);
+static void check_publisher(struct LogicalRepInfo *dbinfo);
+static char *setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static void setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir,
+						   char *lsn);
+static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
+										  char *slotname);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(struct CreateSubscriberOptions *opt,
+								 bool restricted_access);
+static void stop_standby_server(const char *datadir);
+static void wait_for_end_recovery(const char *conninfo,
+								  struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;
+static int	num_pubs = 0;
+static int	num_subs = 0;
+static int	num_replslots = 0;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+/* standby / subscriber data directory */
+static char *subscriber_dir = NULL;
+
+static bool recovery_ended = false;
+static bool standby_running = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Replication slots, publications and subscriptions are created. Depending on
+ * the step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("pg_createsubscriber failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.");
+		pg_log_warning_hint("You must recreate the physical replica before continuing.");
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			PGconn	   *conn;
+
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].replslotname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("There might be a publication \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("There might be a replication slot \"%s\" in database \"%s\" on primary",
+								   dbinfo[i].replslotname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+
+	if (standby_running)
+		stop_standby_server(subscriber_dir);
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %s)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_("     --config-file=FILENAME          use specified main server configuration\n"
+			 "                                     file when running target cluster\n"));
+	printf(_("     --publication=NAME              publication name\n"));
+	printf(_("     --replication-slot=NAME         replication slot name\n"));
+	printf(_("     --subscription=NAME             subscription name\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Build a subscriber connection string. Only a few parameters are supported
+ * since it starts a server with restricted access.
+ */
+static char *
+get_sub_conninfo(struct CreateSubscriberOptions *opt)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	appendPQExpBuffer(buf, "port=%s", opt->sub_port);
+#if !defined(WIN32)
+	appendPQExpBuffer(buf, " host=%s", opt->socket_dir);
+#endif
+	if (opt->sub_username != NULL)
+		appendPQExpBuffer(buf, " user=%s", opt->sub_username);
+	appendPQExpBuffer(buf, " fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ *
+ * If publication, replication slot and subscription names were specified,
+ * store it here. Otherwise, a generated name will be assigned to the object in
+ * setup_publisher().
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(struct CreateSubscriberOptions *opt, const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	SimpleStringListCell *pubcell = NULL;
+	SimpleStringListCell *subcell = NULL;
+	SimpleStringListCell *replslotcell = NULL;
+	int			i = 0;
+
+	dbinfo = (struct LogicalRepInfo *) pg_malloc(num_dbs * sizeof(struct LogicalRepInfo));
+
+	if (num_pubs > 0)
+		pubcell = opt->pub_names.head;
+	if (num_subs > 0)
+		subcell = opt->sub_names.head;
+	if (num_replslots > 0)
+		replslotcell = opt->replslot_names.head;
+
+	for (SimpleStringListCell *cell = opt->database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		if (num_pubs > 0)
+			dbinfo[i].pubname = pubcell->val;
+		else
+			dbinfo[i].pubname = NULL;
+		if (num_replslots > 0)
+			dbinfo[i].replslotname = replslotcell->val;
+		else
+			dbinfo[i].replslotname = NULL;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		if (num_subs > 0)
+			dbinfo[i].subname = subcell->val;
+		else
+			dbinfo[i].subname = NULL;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): publication: %s ; replication slot: %s ; connection string: %s", i,
+					 dbinfo[i].pubname ? dbinfo[i].pubname : "(auto)",
+					 dbinfo[i].replslotname ? dbinfo[i].replslotname : "(auto)",
+					 dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): subscription: %s ; connection string: %s", i,
+					 dbinfo[i].subname ? dbinfo[i].subname : "(auto)",
+					 dbinfo[i].subconninfo);
+
+		if (num_pubs > 0)
+			pubcell = pubcell->next;
+		if (num_subs > 0)
+			subcell = subcell->next;
+		if (num_replslots > 0)
+			replslotcell = replslotcell->next;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier from subscriber");
+
+	cf = get_controlfile(subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   subscriber_dir, DEVNULL);
+
+	pg_log_debug("pg_resetwal command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Generate an object name using a prefix, database oid and a random integer.
+ * It is used in case the user does not specify an object name (publication,
+ * subscription, replication slot).
+ */
+static char *
+generate_object_name(PGconn *conn)
+{
+	PGresult   *res;
+	Oid			oid;
+	uint32		rand;
+	pg_prng_state prng_state;
+	char	   *objname;
+
+	pg_prng_seed(&prng_state, (uint64) (getpid() ^ time(NULL)));
+
+	res = PQexec(conn,
+				 "SELECT oid FROM pg_catalog.pg_database "
+				 "WHERE datname = pg_catalog.current_database()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain database OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	/* Database OID */
+	oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+	PQclear(res);
+
+	/* Random unsigned integer */
+	rand = pg_prng_uint32(&prng_state);
+
+	/*
+	 * Build the object name. The name must not exceed NAMEDATALEN - 1. This
+	 * current schema uses a maximum of 40 characters (20 + 10 + 1 + 8 +
+	 * '\0').
+	 */
+	objname = psprintf("pg_createsubscriber_%u_%x", oid, rand);
+
+	return objname;
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication. Returns the LSN from latest replication slot. It will be the
+ * replication start point that is used to adjust the subscriptions (see
+ * set_replication_progress).
+ */
+static char *
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	char	   *lsn = NULL;
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		char	   *genname = NULL;
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		/*
+		 * If an object name was not specified as command-line options, assign
+		 * a generated object name.
+		 */
+		if (num_pubs == 0 || num_subs == 0 || num_replslots == 0)
+			genname = generate_object_name(conn);
+		if (num_pubs == 0)
+			dbinfo[i].pubname = pg_strdup(genname);
+		if (num_subs == 0)
+			dbinfo[i].subname = pg_strdup(genname);
+		if (num_replslots == 0)
+			dbinfo[i].replslotname = pg_strdup(genname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/* Create replication slot on publisher */
+		if (lsn)
+			pg_free(lsn);
+		lsn = create_logical_replication_slot(conn, &dbinfo[i]);
+		if (lsn != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						dbinfo[i].replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+
+	return lsn;
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ *
+ * XXX Does it not allow a synchronous replica?
+ */
+static void
+check_publisher(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	bool		failed = false;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		failed = true;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
+		failed = true;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ *
+ * XXX Does it not allow a time-delayed replica?
+ *
+ * XXX In a cascaded replication scenario (P -> S -> C), if the target server
+ * is S, it cannot detect there is a replica (server C) because server S starts
+ * accepting only local connections and server C cannot connect to it. Hence,
+ * there is not a reliable way to provide a suitable error saying the server C
+ * will be broken at the end of this process (due to pg_resetwal).
+ */
+static void
+check_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	PQExpBuffer str = createPQExpBuffer();
+	bool		failed = false;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("The target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*
+	 * Subscriptions can only be created by roles that have the privileges of
+	 * pg_create_subscription role and CREATE privileges on the specified
+	 * database.
+	 */
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_has_role(current_user, %u, 'MEMBER'), "
+					  "pg_catalog.has_database_privilege(current_user, '%s', 'CREATE'), "
+					  "pg_catalog.has_function_privilege(current_user, 'pg_catalog.pg_replication_origin_advance(text, pg_lsn)', 'EXECUTE')",
+					  ROLE_PG_CREATE_SUBSCRIPTION, dbinfo[0].dbname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	res = PQexec(conn, str->data);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain access privilege information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (strcmp(PQgetvalue(res, 0, 0), "t") != 0)
+	{
+		pg_log_error("permission denied to create subscription");
+		pg_log_error_hint("Only roles with privileges of the \"%s\" role may create subscriptions.",
+						  "pg_create_subscription");
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 1), "t") != 0)
+	{
+		pg_log_error("permission denied for database %s", dbinfo[0].dbname);
+		failed = true;
+	}
+	if (strcmp(PQgetvalue(res, 0, 2), "t") != 0)
+	{
+		pg_log_error("permission denied for function \"%s\"",
+					 "pg_catalog.pg_replication_origin_advance(text, pg_lsn)");
+		failed = true;
+	}
+
+	destroyPQExpBuffer(str);
+	PQclear(res);
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * replication setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Write the required recovery parameters.
+ */
+static void
+setup_recovery(struct LogicalRepInfo *dbinfo, char *datadir, char *lsn)
+{
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	/*
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection. The primary_conninfo is generated using the
+	 * connection settings.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * The subscriber is not running yet. In dry run mode, the recovery
+	 * parameters *won't* be written. An invalid LSN is used for printing
+	 * purposes. Additional recovery parameters are added here. It avoids
+	 * unexpected behavior such as end of recovery as soon as a consistent
+	 * state is reached (recovery_target) and failure due to multiple recovery
+	 * targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  lsn);
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+}
+
+/*
+ * Drop physical replication slot on primary if the standby was using it. After
+ * the transformation, it has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, char *slotname)
+{
+	PGconn	   *conn;
+
+	/* Replication slot does not exist, do nothing */
+	if (!primary_slot_name)
+		return;
+
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn != NULL)
+	{
+		drop_replication_slot(conn, &dbinfo[0], slotname);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop replication slot \"%s\" on primary",
+					   slotname);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	char		slot_name[NAMEDATALEN];
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	snprintf(slot_name, NAMEDATALEN, "%s", dbinfo->replslotname);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(struct CreateSubscriberOptions *opt, bool restricted_access)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	int			rc;
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, subscriber_dir);
+	if (restricted_access)
+	{
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %s\"", opt->sub_port);
+#if !defined(WIN32)
+
+		/*
+		 * An empty listen_addresses list means the server does not listen on
+		 * any IP interfaces; only Unix-domain sockets can be used to connect
+		 * to the server. Prevent external connections to minimize the chance
+		 * of failure.
+		 */
+		appendPQExpBufferStr(pg_ctl_cmd, " -o \"-c listen_addresses='' -c unix_socket_permissions=0700");
+		if (opt->socket_dir)
+			appendPQExpBuffer(pg_ctl_cmd, " -c unix_socket_directories='%s'",
+							  opt->socket_dir);
+		appendPQExpBufferChar(pg_ctl_cmd, '"');
+#endif
+	}
+	if (opt->config_file != NULL)
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-c config_file=%s\"",
+						  opt->config_file);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	standby_running = true;
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	standby_running = false;
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	10
+
+	pg_log_info("waiting for the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, "
+					 "you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = '%s'",
+					  dbinfo->pubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid and
+		 * a random number.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", dbinfo->pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it.  It
+ * is not required to copy data. The subscription will be created but it will
+ * not be enabled now. That's because the replication progress must be set and
+ * the replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, enabled = false, "
+					  "slot_name = '%s', copy_data = false)",
+					  dbinfo->subname, dbinfo->pubconninfo, dbinfo->pubname,
+					  dbinfo->replslotname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the last
+ * replication slot that was created. The goal is to set up the initial
+ * location for the logical replication that is the exact LSN that the
+ * subscriber was promoted. Once the subscription is enabled it will start
+ * streaming from that location onwards.  In dry run mode, the subscription OID
+ * and LSN are set to invalid values for printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char		originname[NAMEDATALEN];
+	char		lsnstr[17 + 1]; /* MAXPG_LSNLEN = 17 */
+
+	Assert(conn != NULL);
+
+	appendPQExpBuffer(str,
+					  "SELECT oid FROM pg_catalog.pg_subscription "
+					  "WHERE subname = '%s'",
+					  dbinfo->subname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		snprintf(lsnstr, sizeof(lsnstr), "%X/%X",
+				 LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		snprintf(lsnstr, sizeof(lsnstr), "%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	snprintf(originname, sizeof(originname), "pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", dbinfo->subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"config-file", required_argument, NULL, 1},
+		{"publication", required_argument, NULL, 2},
+		{"replication-slot", required_argument, NULL, 3},
+		{"subscription", required_argument, NULL, 4},
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pub_base_conninfo;
+	char	   *sub_base_conninfo;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	char	   *consistent_lsn;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	subscriber_dir = NULL;
+	opt.config_file = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = palloc(16);
+	strcpy(opt.sub_port, DEFAULT_SUB_PORT);
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList)
+	{
+		NULL, NULL
+	};
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:np:P:s:t:U:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				else
+				{
+					pg_log_error("duplicate database \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				pg_free(opt.sub_port);
+				opt.sub_port = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				canonicalize_path(opt.socket_dir);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			case 1:
+				opt.config_file = pg_strdup(optarg);
+				break;
+			case 2:
+				if (!simple_string_list_member(&opt.pub_names, optarg))
+				{
+					simple_string_list_append(&opt.pub_names, optarg);
+					num_pubs++;
+				}
+				else
+				{
+					pg_log_error("duplicate publication \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 3:
+				if (!simple_string_list_member(&opt.replslot_names, optarg))
+				{
+					simple_string_list_append(&opt.replslot_names, optarg);
+					num_replslots++;
+				}
+				else
+				{
+					pg_log_error("duplicate replication slot \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 4:
+				if (!simple_string_list_member(&opt.sub_names, optarg))
+				{
+					simple_string_list_append(&opt.sub_names, optarg);
+					num_subs++;
+				}
+				else
+				{
+					pg_log_error("duplicate subscription \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/* Any non-option arguments? */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* Required arguments */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* If socket directory is not provided, use the current directory */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_sub_conninfo(&opt);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Number of object names must match number of databases */
+	if (num_pubs > 0 && num_pubs != num_dbs)
+	{
+		pg_log_error("wrong number of publication names");
+		pg_log_error_hint("Number of publication names (%d) must match number of database names (%d).",
+						  num_pubs, num_dbs);
+		exit(1);
+	}
+	if (num_subs > 0 && num_subs != num_dbs)
+	{
+		pg_log_error("wrong number of subscription names");
+		pg_log_error_hint("Number of subscription names (%d) must match number of database names (%d).",
+						  num_subs, num_dbs);
+		exit(1);
+	}
+	if (num_replslots > 0 && num_replslots != num_dbs)
+	{
+		pg_log_error("wrong number of replication slot names");
+		pg_log_error_hint("Number of replication slot names (%d) must match number of database names (%d).",
+						  num_replslots, num_dbs);
+		exit(1);
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(&opt, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * If the standby server is running, stop it. Some parameters (that can
+	 * only be set at server start) are informed by command-line options.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+
+		pg_log_info("standby is up and running");
+		pg_log_info("stopping the server to start the transformation steps");
+		stop_standby_server(subscriber_dir);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	consistent_lsn = setup_publisher(dbinfo);
+
+	/* Write the required recovery parameters */
+	setup_recovery(dbinfo, subscriber_dir, consistent_lsn);
+
+	/*
+	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("stopping and starting the subscriber");
+	stop_standby_server(subscriber_dir);
+	start_standby_server(&opt, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the replication start
+	 * point to the LSN reported by setup_publisher().  It also cleans up
+	 * publications created by this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/* Remove primary_slot_name if it exists on primary */
+	drop_primary_replication_slot(dbinfo, primary_slot_name);
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(&opt);
+
+	/*
+	 * In dry run mode, the server is restarted with the provided command-line
+	 * options so validation can be applied in the target server. In order to
+	 * preserve the initial state of the server (running), start it without
+	 * the command-line options.
+	 */
+	if (dry_run)
+		start_standby_server(&opt, false);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..ecf503a7d0
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,326 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--database', 'pg1',
+		'--database', 'pg1'
+	],
+	'duplicate database name');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'duplicate publication name');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of publication names');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo2',
+		'--subscription', 'bar1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of subscription names');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo2',
+		'--subscription', 'bar1',
+		'--subscription', 'bar2',
+		'--replication-slot', 'baz1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of replication slot names');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+$node_f->start;
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->adjust_conf('postgresql.conf', 'primary_slot_name', undef);
+$node_c->set_standby_mode();
+$node_c->start;
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Check some unmet conditions on node P
+$node_p->append_conf('postgresql.conf', q{
+wal_level = replica
+max_replication_slots = 1
+max_wal_senders = 1
+max_worker_processes = 2
+});
+$node_p->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary contains unmet conditions on node P');
+# Restore default settings here but only apply it after testing standby. Some
+# standby settings should not be a lower setting than on the primary.
+$node_p->append_conf('postgresql.conf', q{
+wal_level = logical
+max_replication_slots = 10
+max_wal_senders = 10
+max_worker_processes = 8
+});
+
+# Check some unmet conditions on node S
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 1
+max_logical_replication_workers = 1
+max_worker_processes = 2
+});
+$node_s->restart;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'standby contains unmet conditions on node S');
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 10
+max_logical_replication_workers = 4
+max_worker_processes = 8
+});
+# Restore default settings on both servers
+$node_s->restart;
+$node_p->restart;
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--publication', 'pub1',
+		'--publication', 'pub2',
+		'--subscription', 'sub1',
+		'--subscription', 'sub2',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--replication-slot', 'replslot1'
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# PID sets to undefined because subscriber was stopped behind the scenes.
+# Start subscriber
+$node_s->{_pid} = undef;
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.34.1

v31-0002-Stop-the-target-server-earlier.patchapplication/octet-stream; name=v31-0002-Stop-the-target-server-earlier.patchDownload
From 86495414cbe59e66203ccf311de5af7541aa1be4 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Sat, 16 Mar 2024 11:58:11 -0300
Subject: [PATCH v31 2/3] Stop the target server earlier

Since the recovery process requires that it reaches a consistent state
before considering the recovery stop point, stop the server before
creating the replication slots since the last replication slot is its
recovery stop point.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 6cc1c34121..34ec7c8505 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -2074,6 +2074,16 @@ main(int argc, char **argv)
 	 */
 	check_publisher(dbinfo);
 
+	/*
+	 * Stop the target server. The recovery process requires that the server
+	 * reaches a consistent state before targeting the recovery stop point.
+	 * Make sure a consistent state is reached (stop the target server
+	 * guarantees it) *before* creating the replication slots in
+	 * setup_publisher().
+	 */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(subscriber_dir);
+
 	/*
 	 * Create the required objects for each database on publisher. This step
 	 * is here mainly because if we stop the standby we cannot verify if the
@@ -2086,11 +2096,10 @@ main(int argc, char **argv)
 	setup_recovery(dbinfo, subscriber_dir, consistent_lsn);
 
 	/*
-	 * Restart subscriber so the recovery parameters will take effect. Wait
+	 * Start subscriber so the recovery parameters will take effect. Wait
 	 * until accepting connections.
 	 */
-	pg_log_info("stopping and starting the subscriber");
-	stop_standby_server(subscriber_dir);
+	pg_log_info("starting the subscriber");
 	start_standby_server(&opt, true);
 
 	/* Waiting the subscriber to be promoted */
-- 
2.34.1

v31-0003-Document-a-limitation-of-pg_createsubscriber.patchapplication/octet-stream; name=v31-0003-Document-a-limitation-of-pg_createsubscriber.patchDownload
From 7d60893f7d94e064b33e66f01e0f9ed44230f38d Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 19 Mar 2024 16:35:27 +0530
Subject: [PATCH v31 3/3] Document a limitation of pg_createsubscriber

Document a limitation of pg_createsubscriber
---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 642213b5a4..7e1c5e25da 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -369,6 +369,18 @@ PostgreSQL documentation
     If the target server has a standby, replication will break and a fresh
     standby should be created.
    </para>
+
+   <para>
+    If the target server is not direct standby of the source server and none of the
+    parent standby of target server has a replication slot, the replication between
+    target server and its standby may break and a logical replication between target
+    server and source server is set up.
+    For example: Node A, B and C are in cascade physical replication without
+    replication slot. And if pg_createsubscriber is run on Node C as target server and
+    Node A as source server, the execution is successful and the physical replication
+    between Node B and Node C breaks and a logical replication is setup between Node A
+    and Node C.
+   </para>
   </warning>
 
  </refsect1>
-- 
2.34.1

v31-0004-Specify-database-along-with-subscription-query.patchapplication/octet-stream; name=v31-0004-Specify-database-along-with-subscription-query.patchDownload
From 7b2099f0d0a6a1ce7493448f40eba99506b662cd Mon Sep 17 00:00:00 2001
From: Shubham Khanna <khannashubham1197@gmail.com>
Date: Wed, 20 Mar 2024 09:38:42 +0530
Subject: [PATCH v31 4/4] Specify database along with subscription query.

Specify database along with subscription query.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 34ec7c8505..975d081cae 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1631,9 +1631,12 @@ set_replication_progress(PGconn *conn, struct LogicalRepInfo *dbinfo, const char
 	Assert(conn != NULL);
 
 	appendPQExpBuffer(str,
-					  "SELECT oid FROM pg_catalog.pg_subscription "
-					  "WHERE subname = '%s'",
-					  dbinfo->subname);
+					  "SELECT oid FROM pg_catalog.pg_subscription s \n"
+					  "WHERE subname = '%s' \n"
+					  "AND s.subdbid = (SELECT oid FROM pg_catalog.pg_database\n"
+					  "WHERE datname = '%s')",
+					  dbinfo->subname,
+					  dbinfo->dbname);
 
 	res = PQexec(conn, str->data);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
-- 
2.34.1

#217Euler Taveira
euler@eulerto.com
In reply to: Peter Eisentraut (#206)
2 attachment(s)
Re: speed up a logical replica setup

On Mon, Mar 18, 2024, at 10:52 AM, Peter Eisentraut wrote:

On 16.03.24 16:42, Euler Taveira wrote:

I'm attaching a new version (v30) that adds:

I have some review comments and attached a patch with some smaller
fixups (mainly message wording and avoid fixed-size string buffers).

Thanks for your review. I'm attaching a new version (v32) that includes your
fixups, merges the v30-0002 into the main patch [1]/messages/by-id/34637e7f-0330-420d-8f45-1d022962d2fe@app.fastmail.com, addresses Hayato's review[2]/messages/by-id/TYCPR01MB12077E2ACB82680CD7BAA22F9F52D2@TYCPR01MB12077.jpnprd01.prod.outlook.com,
your reviews [3]/messages/by-id/085203c4-580d-4c50-90e3-e47249e14585@eisentraut.org[4]/messages/by-id/7a970912-0b77-4942-84f7-2c9ca0bc05a5@eisentraut.org, and fixes the query for set_replication_progress() [5]/messages/by-id/CALDaNm053p5Drh9+=VvkH0Zf_3xoLB+xw8RsNZjEwhc5xc_W=Q@mail.gmail.com.

* doc/src/sgml/ref/pg_createsubscriber.sgml

I would remove the "How It Works" section. This is not relevant to
users, and it is very detailed and will require updating whenever the
implementation changes. It could be a source code comment instead.

It uses the same structure as pg_rewind that also describes how it works
internally. I included a separate patch that completely removes the section.

* src/bin/pg_basebackup/pg_createsubscriber.c

I think the connection string handling is not robust against funny
characters, like spaces, in database names etc.

get_base_conninfo() uses PQconninfoParse to parse the connection string. I
expect PQconnectdb to provide a suitable error message in this case. Even if it
builds keywords and values arrays, it is also susceptible to the same issue, no?

Most SQL commands need to be amended for proper identifier or string
literal quoting and/or escaping.

I completely forgot about this detail when I added the new options in v30. It is
fixed now. I also changed the tests to exercise it.

In check_subscriber(): All these permissions checks seem problematic
to me. We shouldn't reimplement our own copy of the server's
permission checks. The server can check the permissions. And if the
permission checking in the server ever changes, then we have
inconsistencies to take care of. Also, the error messages "permission
denied" are inappropriate, because we are not doing the actual thing.
Maybe we want to do a dry-run for the benefit of the user, but then we
should do the actual thing, like try to create a replication slot, or
whatever. But I would rather just remove all this, it seems too
problematic.

The main goal of the check_* functions are to minimize error during execution.
I removed the permission checks. The GUC checks were kept.

In main(): The first check if the standby is running is problematic.
I think it would be better to require that the standby is initially
shut down. Consider, the standby might be running under systemd.
This tool will try to stop it, systemd will try to restart it. Let's
avoid these kinds of battles. It's also safer if we don't try to
touch running servers.

That's a good point. I hadn't found an excuse to simplify this but you provided
one. :) There was a worry about ignoring some command-line options that changes
GUCs if the server was started. There was also an ugly case for dry run mode
that has to start the server (if it was running) at the end. Both cases are no
longer issues. The current code provides a suitable error if the target server
is running.

The -p option (--subscriber-port) doesn't seem to do anything. In my
testing, it always uses the compiled-in default port.

It works for me. See this snippet from the regression tests. The port (50945) is
used by pg_ctl.

# Running: pg_createsubscriber --verbose --verbose --pgdata /c/pg_createsubscriber/src/bin/pg_basebackup/tmp_check/t_040_pg_createsubscriber_node_s_data/pgdata --publisher-server port=50943 host=/tmp/qpngb0bPKo dbname='pg1' --socket-directory /tmp/qpngb0bPKo --subscriber-port 50945 --database pg1 --database pg2
pg_createsubscriber: validating connection string on publisher
.
.
pg_createsubscriber: pg_ctl command is: "/c/pg_createsubscriber/tmp_install/c/pg_createsubscriber/bin/pg_ctl" start -D "/c/pg_createsubscriber/src/bin/pg_basebackup/tmp_check/t_040_pg_createsubscriber_node_s_data/pgdata" -s -o "-p 50945" -o "-c listen_addresses='' -c unix_socket_permissions=0700 -c unix_socket_directories='/tmp/qpngb0bPKo'"
2024-03-20 18:15:24.517 -03 [105195] LOG: starting PostgreSQL 17devel on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
2024-03-20 18:15:24.517 -03 [105195] LOG: listening on Unix socket "/tmp/qpngb0bPKo/.s.PGSQL.50945"

Printing all the server log lines to the terminal doesn't seem very
user-friendly. Not sure what to do about that, short of keeping a
pg_upgrade-style directory of log files. But it's ugly.

I removed the previous implementation that creates a new directory and stores
the log file there. I don't like the pg_upgrade-style directory because (a) it
stores part of the server log files in another place and (b) it is another
directory to ignore if your tool handles the data directory (like a backup
tool). My last test said it prints 35 server log lines. I expect that the user
redirects the output to a file so he/she can inspect it later if required.

[1]: /messages/by-id/34637e7f-0330-420d-8f45-1d022962d2fe@app.fastmail.com
[2]: /messages/by-id/TYCPR01MB12077E2ACB82680CD7BAA22F9F52D2@TYCPR01MB12077.jpnprd01.prod.outlook.com
[3]: /messages/by-id/085203c4-580d-4c50-90e3-e47249e14585@eisentraut.org
[4]: /messages/by-id/7a970912-0b77-4942-84f7-2c9ca0bc05a5@eisentraut.org
[5]: /messages/by-id/CALDaNm053p5Drh9+=VvkH0Zf_3xoLB+xw8RsNZjEwhc5xc_W=Q@mail.gmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v32-0001-pg_createsubscriber-creates-a-new-logical-replic.patch.gzapplication/gzip; name="=?UTF-8?Q?v32-0001-pg=5Fcreatesubscriber-creates-a-new-logical-replic.pa?= =?UTF-8?Q?tch.gz?="Download
���ev32-0001-pg_createsubscriber-creates-a-new-logical-replic.patch�=ks����3��rb�4�����k-o�)��ci��n�T3��/�������@��y��]�+�%h4���&��,����������������G�g����==����ppp��������
O�`pA�c������`o��g��}�A���_{���5�s��Y �?�yIt��vs~�0��������!]�_
�38:���_��/����{�#{8<`����tz�e@�b,�,����'�e1da2
<7dOC���&�[�������	�=���y���9s���|�Y�fS��Af01K���a�8�	��8��G��I�y\O�M�q���hz���}�����d�	$
"�^0	��|����
��q�^X��B��m5��#x^���n��d��R�G������,�$��&I���7+��u:w�Z��������;���
��9�v+(;��pd��qA�lW�I����5n(��_x\����H;�Lbt�D�����"�_�3��Qx0YL�0����@���+D*�w����G ��f��d@(�oOA�,�v
��~�����awW����%j��"Lr�/#�6��|�`+0�C:�������:Iy��D��G/O`$�q$��H��_-&�ys�32�v���Hvv+On�
{8�<���/q}@��,�8�{����"���E�00Jr��D�We3-m���JZ���Af-�(��� )l��{���#��P��4)����,S�-���X�Yuo�%2��+��$�Qh���������9l)b��������q:�O������(�g|�p�lO�����_C��2�E�
�w|p���Os*(l����*K��q�JH���}*�~o
�b'`�6������F���<g�������4&�/^0E/�����������hZ��a�^����m���M������f�3���&�������������=x�|r����Qj����l8��/��7����Q������9s�+�b�b�m�?�#�����|<<��&�c~rxvvr��D�[�R��z����s08���}�����7I����`���������j
���~��a��x������;@@JEC�������� !�w���emr<	���U&�;��`f�%!y�������oO%�������&���������p��E�.����Es����;=m�|.4�-��P1T&�h���A�Dy�M�����}���!�Z$��C�tl�E���5�|��� ��'"o|���9g2��w���e������N:��Bb#�)�U���c������-����%B��`	��@��J0�<�C~9�%u(��0�Wg����0.����o~��E���x�\^@�|A8�1��~�P����1�/�����D�KPZ:r�gsa�2�m�����13�(��<NR�A/��3zO ����e�R�	������"�z��e�j��g��l�fO���7KD�����;H'��hm�H.u������Zh�G���sG}BE�f��#y���2��K���S������c_���P�L�ko�,'��m�$�����zT�A�����$��5/3�Q���]j�6�F�%	I+�*���Z��^�����h����>$�m��Z���Y��Fn��f�]�4���XS�
�+��S<iO�]���)��h<%�	���et(k6M��p�9����5�<���L*$��e?v'q8�����,K��w�lcm�+k�E��� i��
h�t��R�k�����RF a6�w��B���&p@�{n!p�����hKF��� �����R��K��C���,��������q����0��Vb�p-� l�#Q`)��-�QR�Hf�$iX�D*(���g �� JX�����"[������u��d7@�J<�x�V��b��!r��)"�4C�At������x���X\��� Y��Q�^y�"�1��4�I_eg��_��cr%��[]�F�K��$a�<�%$�8�4�S
���Z���,@b�X�5x��SX����������B��u��� �r�*n�].�����T�|R!9S��.�����P/���OE��_qc��X����|P�o�Y@�HOk���xro9���_Ae���Q���M�x���� �#e���.�'u=�,�����8?Q r�y��Zlb�)[�\l��V�l�@����N��B�8C��OhI����
D		sm��}.r���4M�|;56�A �$u���R� 9��9>i�z�J�
D�Y#����L>q������k�S�0��e�G�)=$��
��[3=p�q�������U<oM���K��l�7[l=��Bl*�%y�W"'����
!��3� ��B�`�~vra��\�kx�-U2�>��)�����F�a$#w�����%w!�t�V=����/uU�+�����:1�U\��4�}���g���I��|n��z���� *���N�Z����@�����t�Nus�t�k��&[<,�����_W�d2�>��<���m.��4P�s�m�*��Q���&-Iy&�/��7�����������
��JAvS����$:�Aa����
f�������En"�������
�tJTw��Tm�U�B��C�=7���$�B�TX��$�DBs��w�����:!��a�	�,)L�P�J�,�mY��#L<3[�G8�������Z�����<�Y�I���ZBZf��4���f`1���+��k���z�X*��dyU�D��m�/�&������%��2��tA��2���s�C"����6��,2;:i�j[����9V{��}unW��%\�_�K��hb YPt��������62�S[�
z%,��j�����`��~,�e	�]<����1�@\`��fa+��d����"P��iB��38�p�J���;U�U�~����w-�bS�����x� D0��_�C,��M��:��Ku�h{���l/�6���]i%����&��v�g�mKl�G}v�n4�n/�uP��y�*�e��m7J���%�n����+��t�P��[����"��2�&����lu�?�]b��0��
��,ydC%'��3���W=F�Rl�����������Y�ncs������5~_N6���$=�Un|��sHW�]�����*��H&�n�P���F�B�T�[��*������A����	x�2@��K�����<3�A��h!"�_�d!o�Mi��[��$SNmY�����A��-�2K�&�
>�����Y<��H�tI�NS��;�VH���S�-P	�t{����$bZ:���$�A���S��M���������j����z�Z{e[K�H�+�|$x����E�J*��oZxN�>5������R~
�+{�����Z���kz.X����yL�O �������jd��� ������#A@-�^��L����BJ��1Z(�5%�}@%Pms��}�z������i��lA3�BT?.\&���$mXy0�R��=
��1�]��o���X�"F�G������5�
��a��R���z�v�L��-���a!��2���`+���K7����<�>UL#���l\$*�����%xt��cd@�Q>�[q+�F:8v'9o�}]��%�qK9S�|����
?�T���S#���O)F$�bli����������j�o������d��&9���>�]D� ����y��^����1scQ�}����H�����~���Q�#Vm��tR���du�4��B!`����~�M)(��f��$��l���_���5s������[t��A��a�H��>�V	����^�
�������g�^�����K:^E��;���ot6'��Z[)Y�h�zteo����W�W���2��-UG&�@�������v�mDW'56���an������6$�k;�u�
����
X�D6#E�T��uI�����R�
�R����w���n�������[���+be(u�����/@#�O��]���!��%^cc�\e�@�e�'�R����QN���O�,�R��#����X��`*�6��������V��Q�������zG����T�������"�	��1�qV�?aXo���%��n�b��rW-��@��h��M*�JZ_��_�����mH���u�
?�a����c�VA��:�TqQ������k�v�k�WzU�u�����K�z|Rk�`�X����Z����4�����G$�G�������"���3"jw���F�H0<���F���t
/�@�-�ZZ�gt��]Z��:���@���������(��]b��j9 (�����Ru��.�o�
�9=����=�rH�����"%]�osHD7����j��������Z�0
���G~���oe��&Z$��y�sl��(��)�5\�)�Ys:P��@������\�zC�������kT5��.=�>�ZtB�Npf�X��VG���f������2�c���X����FW��HI� jh%�G��K0����W���A������J��5r4�`�E�g���F����}�lT@Tu�gvB���"/����~��D1�.�R(H� �#	�8��<���i�%��"��_���H�%�
HcZs���aZ�8�\�Iq�5���4e�D����/�"+����/U�*R~wm��i7 -=�s��A��.���D	��h�K�4;J��6�j�U�RJ��M| ��k��?�Y�x����AG�`����I��gx�W�u�#��+���la�]���������>S�O����C�'�k$*�~P�L(GyD"����X��GU�2{f��V�e��e�N��>h�������AE�tA��u�MI)���K��� �z�0�R��
������cD�uY�u}���E�b��}�m�Nl�X��`b����+��@�s��������J����5�g�[�5�uq�p��+YDI���N�xaNhO��h����Xy��2��6��:������e8����\�k6ZA���U_�O�)}6�����w9���i�W������t��p�'	E���=����\gIZ��m���|�o�O�50\�3RRK"��>�Q�c���]�aF����W9.N��-h	�z��>8����aCK�^7A�� ���/�Ay%M�	l6"�N��r�+�w���Hc���w��';���d��P��A�}l%�tk��:�E�� �"\]kz��
��+������h.�tk�0]J����,�~��`�'�o����l��eI.����:�Vf�6'�H?�0�^���5��+"��v��T��C��t�1Y C�_���c��Q`S�,����z�~h�p��"��.�U2}�����Kz��7�M�s_�93���j�O�n\PnkH�*
����$fD#P#����5�(�/_���'6��� N��-�b�5�"�SoL?���a�B�=��A��
V�a��f�]bl�����-�o9gW�4��.d]��S�j'��l��DE���V5W�����j�D��m�sa���,���JZo����{~49q��I�7���?;�,�h���~q�=��X=;�+V��b�����t�f�2/�n\����TDE$�sm�wD�?5�*5�������T�$�wU"��y�r�"�����l����^ox������\	J�t�0$��{Dw�Q�C;�mw�QUq <���A_`���My�"<�-f��(0�^�wp���������*� ���Q/����>���������W����x,�( �{�K���K�X��`�eX�r�F���	
���T!\���=�I��oP"��CYT~��%�'^@�/`�/v��A0o~�����~�{s]�T?������{x�$������L�[��m�������~><��v{������&�w�����JH~e/Z`#2�H���<���2sQ�}����1�(�.�X�/~��]�����������_��������"��������w��>�����{������6pu�Y
�m����,Z��&�*x�����'������;<DfZ��@F�}B��A1g�m��_D����tY9��oF;�r����( MAj2�����?�U���B�V��u�V�[��"-�o
�B!��d~Qz/��r��[a����(�<9�'~rr:�������c>\+`�a-���8T��3�e����2f��|��1��}Yy���a�^���_U�������i�[��:�E?���Q�#�H�g2����{��p�Y��^B��I�-3��~�����{�8���z%�H�����zW�����#M
9�j�$�!|(6�	������'UdG�y;��5��8��pS�V5��x��t�m���N
���&?U������j#:+X75yup�n��Y�t"`$x�nS���J��[8����n	}�����<��3\5������Q/k$�<����\��H!�I��AGSD���Qx���0�[�E�����!�no}��-��~%�Nmmo�0
/N�/b���k�CU3�8a����%�b��(-�a���?�����	��Rv]�kC�O��������o���V7�|��]�:�y�A���������������g)�{����W��Gg�G{����7�G���p��������/k���Zm�[EJ��B�(8�����f	�Q�%0!>�����^�����?^t������/VQ���">X�����}��C}����YSf��)��;�L���������F�*q�����I���NL^c�z��`K��:�m��|V�?t������U>��DA�E�yY�����Gh��|~�N�������L1�;��3�^�{i��Q-�Z�#�:/����R�����q6��
���~�	O��D��I=2�����V�����?Q��u�)*�&�R���S������?����_������/������
e6�6�O��t�0�Yw���7I����+�+����2y�n,)n��5���0]����
��?����M����w�,�)�"����JR<�$o��O����}QK�Bg���c��7*�,w�5S��{��|���6o�j�y�i��H6
z�����T��Rk�%�q��k������z�����e���Yc�?�hz��N���,z��o&.s�F����l��g�����L�O���
��q��G��!a��������9z����FN����e�Sa���0�L���_�qm��M����,��s�0q_;�kkW�^����N�����j�������	����|��N�J�O
M?��5����o:������o(PCz -����k>l���Q(��Sd>aL��B�*w���d@����H�G�rL���)=d��y���Oath���-��lO4�pc��S����~��W(��?�N4��5���j�'��C^&l\jk��Q�R���aJ��Y�(RK�����$��L��{����	��WD�2��S-����a5-�9�����l����{����n�KR�C��%��dC4���%��aq
�������8�	(s{;Ol��t��������$���8c�N�����b�7L���
i$��CU�a���l%4�w�<*��Y���C_��n����]���x�������M�
�6AD�t��NH������+�����*��n.�A�/�<E]`���-n�j��o-c��;V9�8��,��f�p�#0����!�����S�=,�	�o������i
�m�V�}��up������:\�&�w[���54#�Ni>�|x�%�������;�C7�Z��h8�re���s���A	�M`����s�
����]���^�@�B��" �� pJ������J�1&���#d
�lu��l��4#��Q�=��Z��iYM�CA+
Q��y)a�By����4�2������7�Y>LY�=9n��i��������5���g����uzvp��X�XVL�te*8u��~uh�h�	'�,&��O4z��]����]N7���7�z�?�X�>���Q�U�4	�%�=,TY��K�J�Q�������%)F���s�K��e�I���<�e��P��)������4��������6�I��P�QZ�K����2M�z��:%W0>^����"]p�
��t>t��~�p�s�����`'kB���<L���	����P]}����A�$�s�[7��H��^O4�lO��������B�/�����R0���|����D!���&.W�xf*�j�<�vC"�L�
>_a����V)�K��xnAwf7X�q3I*�-��m4�Hs����0Ty��3�W�?�Y�j�Fq�D����yu���
��A�D���jt��3	�5D���@�1#`���-kk�<7�����R�5������WLoi0m�����F���olO*�[��UB�
�UZg���^�g��
�v�[3�=����y��KWB�M%�j��:t�,�b���Pz0��"�LQ����M��\;j�h��������+��+�fU����.�W9�u�gp3{O�H<����G�`���=�E��T#F�9�	��kl_*��eQ�lW
������������&J��6v�����.N�Q���%j�8�<mWLP��6�9�=�o�FY�~cL�|QKk���
��-��?V���h�W�]�-�_$	���	Z��onn�2(?E(�Bj��W�c�^�7R�]`f�BM4�������^���wp�-x�u*���nGG����g�+�HM����S�_o���(�1k]���/�
�L
�y�O&4�;��
+,���d�������i�aa@���b�>��B�zt�)U(l�z�
���\���l�5��Ck�Gej���H!/[(L�gEo ��+Y�;�yp/��3���%��i��pNu�m�Rf(�8o�lu���5���p
9<`QKb90�rMaV;f�h���������l���(��8�C(�Ct�^3l7���SJ@���>�}��A�B(�Ik�?[����o_��GP�K����u����������L��:�~��w��l&�$��CF�>RmI'J���=���������X�:F�$�*�op�o���7	��
{�J��l�#�#��P����-
^��VIsh*V �E���Z��k�N����5kx�^d.]SH���*%�{~eN��2�@��b4�x�2K���n��&
�d�*�^�����?%:|1�L�\`t0�P��X��q;k������i�r����\�hH\�������b��������i�L�Dd��U�A�S���}V�,����'���j��o(M�������Z�@��Z��jc�X<���\T�/;{��I�7r�^�m��3����_�����g�i�U��9t���B�FR����q�[�7�(��2V���Z	��>r��	yx�H��	��/
�����fr�3�i�=~W�y�L���H�d.Le��N�'I5��D
��P��tH�m�����a�"���v�6_��g��Z
s��`��f����Nz
�0b��%��t��(m&�H~8e��Fe`fc������}j)xY0�]1a��RrK���Ip�����%i�N�N��"����b�T�#�sQ���d�	��gb���?��M�`�`�-���<�M���SdchZ�������	�o@GM�)}95;�J>v�������G*i]^�����\.E�(��&�a2�=���`&*v��l�<�9x���W�T+�B�X��2p�Ceq�u����Df����
0��X��|����>8>"<�C�1_a�n�M����'���PF@�^J��h�5Y���f�H,XAV�O���]�e�]�E-�����>�zu�
i��
<TM��M�~�g�����.�w���V�<in3 ���F�U�����:6�����V3�fL{u�a��b4C\+AP�`Unjf��V�
��������Dx09�LO0�������F'����Pij�<��~�4I<ZRD��5}8 �]���n3�����(�'���$�So
7|W
t�������l�qk��]�2mJ6������l�������T�E�����L�
D�(�%�����pc�@�9����t@r�����zJ9����)_�5�>�|#S�����q���Gx�����������eJ6����Q�^�N3�Q)�t��Fe7�!�i�B�C!�j���e��9��n����&�X#�c��A��b$��a�j�c�����W-b
�[
��%m'h�X��.;�*��9D����Kq���Ar���c	�2g�SF��n��V�0
*%��O�]����w�r`��V���^	W��
�����9;�%�q�Z����%�F?�B�s?E���)}��R�]R��%d���?�2���ehE?�J��F�������e('�{������J|���;{�'�i�?|���	�=!��h����6��a�N]o�/�����s-� �K��ig2�������A�M�+���n)$ �p�ML6�L��u������r�o�k�V���Q Fq�3��"��	6��H{����a�i���I���0��&�(���9u�06��&~k�R��I	�,��L��<����2�����J>bf/a#
g�V��#������U��6(8� ���p���x�xA?;h��_��h���������Y�s�3/?��]���V�K�V^<-X-�w�B�A����T�P��������S�� �<+<A�q�v�o�"~}��iR�uf�Q���!��e_��n���&�W�(�~���b�yd��2+��i��qG���F�6���K{D�y@���-�p?k��>��':�L�E������=�e�
�� �g�$��k
��a!��u��������=1����2�Mx��K|4!�dfhsg/���w^�pw���.�eHRz@����Kz��������Y���4�z����R�7l��a������wz�n,�ZX����.	���dQ�I��!�H�_��]�om�dx{��Nw�������m�`��~��8y}��N����_s�����;��
w�G��{�c�iK����$O4���`w02^���5`����6����h'�z�d��,���P�BP����\�NT����K���9�
>�:ccT��2�X����F���� ���l�5e]�A�+��5
4�E�T��ld;Hg�J��8QOZ&j�SEW���?�#��j�W��o����H6���PD���������1�|��\�Ul���/�y�-f�2����?�Wz�dt�>�
���l$��L�X�|����h��'���F��d��xA�V=�_E��K�`0�����.�sm6��W����a	�G�q�v�����)]AB�+PE�jT�5n���l�,��=�a������=��9�cq�:��]^�0���a����Z�;���_��~�$ ��������*�6�l��W�����GB>K�I.�'���d6���
w/�/
$gUt�&�%��L��Q��������<����k�2&����c�Z��4�c.��:�p�����z�:$`�\���-w��R�5{%osw���@t�E���G�E�����l�
�'�p��g~������&6��
P�*��#��i���?����$3%���w����h���3����5ib%��n�U6��unj�L?6,���e����:����c�g�z��w����^�,v��~������K���d���W�����F��Z��s�^x������b��dbkK�Q����KCVs���^X����g�@h7p����;(z������]��T��[.����3|~'����5i��p�8	�DS����^��VJ��
��&:���H&�����z9�3��W@���b&A�Kl!���b�H�/����Pj./-�`��\v�x�2���H�����V��pH]�^�;JUU������(���_
���0&���@�������M21y���-��VL�w�?���v~���Dl8��%.e��T�r�$����!��	"�&{�ds#%��M�Z}���g�Q�m�����I`��}�%/0�z*�&��Q�:�����QM0������f��"��!�������`�>��fW�|<��K��60���d��?��?%��q��-��{b���@#�7�{\]�g`�%�4O*bzq��>�Hf��`��l�������(4V�#��j���f"�����#�uz�V,�\���X��)`��jI���A>�I/�l�@����2K5���q�5z����������y�D���r��b�$�Z���w�����/Lk������}
w1Z�����85d��P��x����t�@q����o-�(�Y�-�VU�8\���~�l_����l��P��L��a�\�tx��?�oF}���X~��j�7J��0s����E���2��b)�;X��
�(�L���JWAv�1L������m�Q0+�����3�+�x�t�t��z�H�L�J�����"�0B���%���hwf4�uU��{���,�������i6���z7�b�+��#�,	����.�O���lCW0��������PuY�t���z��I��=a�
��6�7�����-%���"�Z����$K9��	~���g����%�/����|6z�x�I�Vf�<h)��\��p��-Q�BB�}�I1JE��N~���q�%�[�'u�7���d4�������v�6�@W1J�zN�j���1��`�~�u���e'Z�>.9�)��F����~k��P�b��sVl��e_`�U��Y����SlQ���~��n�v���,�����{]�?re��,6{#����)N���~����j�����+��[tX�*��l^��c�*VY�{�6u��,��c�pU���i�7J�+�g�m �]<���=`�ZN���D�n�����Vb����x/b��xn��
�%� }�����
��MY�=�`����m���\�V��rj��,�)����q�"� {�e�0�U���z����@z��`�)�����������l�2���u����l��T
�Dz��B�o�P�l�%��/�dlx�R��?b0~0��<�XuX��P���2�A��g���AKn����^cC�!�DN�~vm>���/��ks_F������J�!�%�.���k����?�����^�9�;�K�R���8�>u�Z� �w��[���aA��|(�^oI�������f�vn���m�����Kr��i���4�
�x^��=#���!���������\.��l2�Yn�2b:"��0���Dm��F��������LUrK�9�8i������s*�M}� (l[X�a#�&��m��g�����LE��fI�h/�����jE�S\����x�t��b�cR��fmER*i*
�!j��*��.���V�{��\s�^�������B|���U��U�kq�(E��'�wn9�0�w��f`>�������c_W+M������E���.��o�����G��r~�I<��Z�NQ��5�Ms|��?.�L��<L�	�� 2\��TuN8��nS�o�:�l������&��=�H�����s&��&jl���a��@�j�������;���*��e���KP��:�@V|�9����3���,&�I���,�O��py���X�=x��S�`�&������-�v�'����SN��@���7����U��C�<e!��7I�Y�0�S�I������>��e}|�wn�9A#x��8�`
D���x��&����Nh����+���B�����i��A�7����{��
��T�������\�oZ����n`���TY�)�[�Vmi]Z�Z%�����w	;o/����C���4C�4BJtp�����`��JET�([�[�5(p�Zt|�����[�Y����F{0~Q}�GN��<VP�l{�������m�6z�\��E�������V��t��d-� ��o�G��:�L��Z�Bm�2�0�������������,�1��*��\�e�U$wo��;�+l�������G���H(�� �n\m�xB+Jr8L���$���������Y`S.���bC{6���i�+P�{I��+BEUigZe!q�!� �G��Q1c����jv��q_�uq]rb��MUz����eB�s2�ni�fS�����X�T�bwG����y��e�T�(��x��<�]��OU�G��v[����&���q'kg����q�1������r<�����H�$�%7�>�����
4�-aI�/}��M�S�A�([O�9Y�h���{��B��r�^�&����Z�����g/���8�)�,�j4�a������E��&-��N���1W������:[f�g����'?D+x��7NtD&�&CO�����U�����E��Cp���LZ�)�#�0��b\�I���J�X�X��y!:��y�t������L%�Y�ih

��e�cD[���}���rQG���^2 �Y��L ��T�9V����2��D�e�"���=���fETQ6�O����'b
�`�N��]�E"��HMq1p
L��-�iU�J������6r��Bq�)�d��@�^�
0;2����������r�����~ }��&Hx�0Pri�_{�A����D�(�
����G~����pW�W��oZgi��u�V���'�Ih���y��}p����+&e����Fo�}�bC�Y.'���v�@>�yHRvl,g���?v��6�#��R�b���H���V�:,����B��)s��-C��1��Pv`	����L*�(&�u������%.�L.�S�8Jy�~�+05�9���Y���pj~�d@��v��u
��F�r]�}pla�0����^	�����$'����$��b���L��8Y;	��k~�
���������M@�����
7�����qOJ8�8c.�o������d
�3	F���F�X�MyA��lk)[��NE�OW�	.�yey��UbG���s�u�t��9
*?23C/s�\�)�9�����ZpB�R5�� �Kg���&j������G�V���B'�h����jq�Upl��U��-��d�3^W������xavXkL�r#:�+a&��m�z4'���Q+>�w�������b������v��$�-��x{������������e\�Yr�
�R���STp�z��}��Xz�4'�e#s�^#J��F����k��A�2����M��*�O1!_�bS(�������g�{�m���������_6���!��Y��m� ��N���>x}�:��[�[%@��t��2��<���U�[���0��������7��2`w�J;��]S���^�\���v��uR�������2����r��f�����t�0�@<������w�������R]��|�s�x��0gC�
?���Ox�^B!���D}�`��8�Fc��)!��|d�c_q�oa]�����$���F�r�����&V����Pet[�6����|��WM�Ia�BK�j�R��8�2�7��9�A��Dl-W�n]��Z�$�Oo�i<�N�7aC"~4�#�x�Y)��'#�cx��PR��EV��m�o��O����'Sy�}���$
 �u�R�����yL�e�|HT����+�b6�\v��L�����E���v�[����j�`E�T��������o��*��J,<P�I�����0@�H�����z�j����6��uIY�Q�_�0�e��s��^���"y�P�7��Jt�h���XRgH��w8sx�i���N�c�1iB��X6  y��E)^,���]��ER��B:>0�Per���J�z6sNe�y�9	[�|��s�m�-����)����%��N�`k���EN��i(��C�a~Sxx���t����D��)����MK������7�u�c��N���������"�)����x]���
��a���>{�j����g������k�;51E�%���Gd�v��8A�a�6e�L�� �R��k~���I��@�:;�sr�^��*�q�A�8������ ^��@��[%'��g��G��\|���5��r_��q��b�#(�}�4k<����
�P�N��K@_���:k��9_�u�m������~k�oF"����;Z����=u�.�'a�0
�W]<p��"��2h�|#��@����������D����l��t�8Y}�s����T���9J�5�������A������kR�'�E	�l^+>Il�����Eq���*����v�I��m�+�)
}�!
�>~��&8Hq(��C�%��d���|[P��v��m����,
{��
��	uw�RX���d�����[y�k���7�b,B��r�v�$
���Y6N����@�|�������#��?��y��m���'�^���
��n<O��J��B<%S�9/S���b+���so
�������x����X�.��I�cyt�����f4�8��Jqz���p���,�?�����8%+>r��KH��\�f���Y�\l�������%���a�>\�������@K�ng�����J/�BiJKd�Z�9k��"��>, ����I���r
�7Ifa��/i;�>v�u�`�.���2��Hu�������B��d��:��eQ��L��!*j�3�#W	�Yd�5t����������d�������(P|%�<ehW�@�a\��y�G������_`zLl�n�J�r4"�F)�:�8����.\r��J�'��7
"�I�E��D��ee"��x_)���n������������$�]�8��M-nT�����T '+��8�kr�����n��bm�W��I��09k����.{�%-��6��y�:�[������[���i��w����C����6��h?�(��f��i�r���U�$d.0D#n�*���N���<�=�!��������Tuy��������]/�����9	��`G:80���-�u~c�d���������]��m�e�d���(��8���!����:�	����TD|{15�X���hA�K�?����?��#���u8���e����|�2s��!|��=+Ny19dI�GW�����f��;Ur�����6<���"}������H;7��c���o3I�&��g-���7����,�&��z1W�8����Ex��m��q/,
�2I!P�Axc����h����d�5U��b6�������=��Bm���PM�)��MP���o��s)�(���+�~��?cN�2M����%�j�'.{�Z�+�D���y������k����x������V,����xP��,�.�����L��!��o�3[3��<����T�0#^�h������.��7�h���j����	���F�M��P{�,G����F��Q�:��lA� ���2���;u�q���IA����U�0���Y	EV1���3�!"~����X���ND�:�bd�������F�Ck
k ��40<��s4��Lz9��������Z�4���]L�%�����{1�.��$j7�4��T������:m�	R�-�n��z������"���^����u�y��z����A���nI�o�YR��C*��9�7�4���GeU�z	�W����%��0���8W�u������MW��W);R���A�����4�/����J���E����I�1��5�o�_�tV��i�]�F_|R��� ��jw������4���/����l3y03MxP���J��x<�yGy\��������u��*�O:�%����4�d_���Y�g��<d,g��|h�H����T������<�������*�Is��y�k1���K�=B��D��E,�o����W������;�%yYN�R�oQ�u\H�ST�XUI��z���������3�(����k�e8�rg�l/��aI}���i[~[�>����7
���#��1)�7���H������FO��>&o������|�6D4y�|�K!�M��8na��T��?�O����bfO9X�����/����7�s���p�M8~h=�
m;'�61�c.����IV�dg���N�`]�4a7s����q�j����n����@IP��|k{�`�#:���}��0V�����e6�4��6�P�&�2����w��k��v��M�����8����Q�H�m�|����g���}���}�uKZ���3�O�A@�{�'n�U�)�#�!+|d�|��������\�x
��i~�����a�?��������`�]/9��rm��������������_�k��SJ +PC:���{�H����j7�S��N��N��W��s'O'f�1�z�������x��]�����l�����7���h��J]=<H���I%�%����l��������W
.�3z
��@�-�<�8�"�?:����6���yt��^��)������
n���mx�K����s��K�N���x��S�o�X*��O��)3c�k�~!�}��������������L71�3�m^/� oQk�����=���! {�o3�.8*sT����N��8�9��Q��H��S5y{��^�B����{������(r�EBA���p#�^^-���4%�Q��sg�`������-��AnB#�g4�&��^RUsE��jb����h�q�S��`*'����j]����)� 5�Z��{O���OO��O�O�>��B5��H����N�`�]���?�C�u�uAc=�-O"W �@�D�����U�~{�o��D��0O���Ak �^��LzK��
��3�e��0>�f��P���i9�*��7f�lL����>`���E��?2
�+��(54C�0�����M��8����D�P����L4�Z~����CM�P*E��L�m�q�d.������Sv�,e��?�����VS���eg��������]-������=-�k���>^aC�%�N�RDo�ir��)-Yv�Mz��&�w��b�5���&��X}G$�V��<��:��+La������j��,�I�Mnh>	���O�
��U��&�?���?[��l
o�e6ECa��w���a/��te}FX�`�F�I�Y-��v���d��T*u��y���<�mW�sM��Q����\���!�������$V������j�^9cx���:����)���}�Fb�L-S�T����rj��W�Q�����5����u��G���Sm����9���y�an�����1N:���gB�����x!��+|�N�I�\�l��������?���y�`�m�����1Y!�w
��l�u��P��"c�O�	���2������uS������O�u�9���,�/Q����Q���M��@���$��t�����u��<\�����v7�������s������D�}�'�!��$��~�:*DFP�U�RV]HPSn��=���j��8+PQ�������������E�8�����Y�������O��+~:����5�7G ��H�m8�dil��R��
6��] ��&7�n�J�+
�1��YCjU�/���
a�b�����D�_�%L(mexu�tw�5	U&�\���2:�o�`�iin�RW�3���q#��{��W��A�� ������y9-;*������g�*���~M���E����[-����^u����/�l+i���W������&�{�q#�J�����R��s���)�R�&�p\5�H)�E*E��"�Se���$1���`�.��45/���^�}L���3%���su*���Di7M��'�S$Lv��b��W��P�������'����I ��1�UB�\�z�-~��7�/�L����w�([���
H����������������f���{io0p'���2�%�	\�<I3�3�O<�f/A��u):��J&�,:�0(�w����Cq;f�~�x&����x�{�1�~6�
�+����b!��FJ�n�0S��D������+�L]�`,I���q�R6D
�7P��J���� 4������&�X.�u��1H�CtK,+���.���{���@8��$*:�j�Ls��+x7,H��A�D�Y�b�e�%�l��D�jBcy�2^S��_�r��\��,YT��0�l������1r)�m�n����\C��_$c�����0��h����^.��R� ����AJ6�6����U�2E4b����[�F�a�J$1?���}:igo6�*`��Al���}��[+��V#7�Gp����@�/��/��S-_��Sy-z� �����7�:�Dd��������`��Ou��G�(:�H�xg=�
����/���X���
����5������SU�����<�=���K�W�Du��Mi�������t
��l�C�yJx��I9��>��e.KW���g fp���.M��C���dnq	<?CH�����z�%Q�������iluy�l���I)�����fE"��>3(���M��7�q���Li|&�|�c���bP�=�L�e���k�
Y���2NP�{:$�'C��q
���+����(s�
/YAQm�d�������L�O��3:�_�p���]\�~�vu�&�����K��=]�r���fK�w�J.����������������*
W�=)5���o�V���"����z}J��k���������.6G&#�1��1�J�'�p��r8��sG�A��:�6�����)�H�RC*r���;�By\�AD��P5�S�H�2����3�����B�#�PD������lS���RP���F=�YjE�PxI9>�<�����7f_�_�I�����Q��v��(�t�n=�������/������'[i�����[tZf�,q`|j�����'O���(��67w�<����?>�Cg��l&�z��G���������s��jl%���;�&������.N�j�z��������^F��:����h���50Lc�)����|*%�:!���l��nx�����"���U��u�<�T�����n2O��`O���<W��[ ��|��������~:W)�X��/�jd�(��i(��K�v9�a\��N�W7�H�pP�/��S�"��3��I��,��o��R*��O����2���)�����?%Q����l��>�������i�dD����*��b��_������x�x�
��dK�����L+35���v��v97>\���Z�S�
.F����[nvb{���a��.4����������M���3���*�\�������g/�
�q�;�����5�RAUEV�P����O&~���/�0��b�����J��(A��<u	E��%�������^Q����l��������c�W#�=�%�E�bU�N�l�h�)Y:P���
1O��D���}{������)�T`�������t>B�,�'X3�h�D�b�����fX���n���������W���H���x(���4M�!k��>�er*n��9#U-�nU-;�����w.�������1/����H��=`Z_���	\�g��w����W0_��*�(ql2=l'�����^���rp��?=K����������Z��a��z��Nl�t���
�b��#i�:4��NE ��M�buF;��9�x�����<�/b\0(b'��P����v� _�s��9cr�rj*�e<��2P/�����xA����W��:���B�"T����Y�x�A���
����"d��a�p���IK^��h(��5�y
�I�����^�/XQ��_��/�	�����[?i�c��>y��p��5������xW�x��s��;p����q<���r��h����Dqz
��N�� �c�~w��r_����\-J'Q|vm��Q��Jw���E�*d1�@�T�q���������+X����5:F��*�n��qM��#f����tnR�a��.	)��lxEl���D^���&�1��+Vp�1\0y�0E���s���6�}r,�4�R��TlpJ�����[&�:2�V�|4��dJ�+���T:_��{Bi����cr�����)��
�]T���������3��16��
���BT����8�t��0I\�)�����7o���J�A��(vn����=��Y���d�.���k��\�R���_#������\�������>Y��sn�H���I�Q�t�P��uF�����@���j���r�S�;F�\U��0uR���0��,�
H��-�hMW�����g�uq��j4�^P����U%}�rZ����'�Pd�Tlq6��R�V��y���6��4����Q'���eE��	����n�@$N�U�J���d#B,0���*Z:���Q{�eC���c	\R�=wj�P�Q�1��Y{X�I��������*�u�S<.j�j[Bn������])��uUy%b"\�����v���$*���!���m�*��J4`�Q��@����y*`�������\�N�����_�ZpU���Y�"o���Mk��1	�L�,]�_�P��������7��S^��PRc����{������$)l�Q���>�=E��
�'���s�]���E.�1Ocd$�pr�%V��x;��T#��P��H������^���{Q�������b3��Ae�*K�M}"�d4H�(�2
�(y�n��(����|�����T��E�h���gw�D�(*3�:���z����_\�X����Y*�?�&��d}g��������y:���
v32-0002-Remove-How-it-Works-section.patch.gzapplication/gzip; name="=?UTF-8?Q?v32-0002-Remove-How-it-Works-section.patch.gz?="Download
#218Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#202)
Re: speed up a logical replica setup

On Mon, Mar 18, 2024, at 2:43 AM, Hayato Kuroda (Fujitsu) wrote:

Thanks for updating the patch. Here are my comments.
I used Grammarly to proofread sentences.
(The tool strongly recommends to use active voice, but I can ignore for now)

Thanks for another review. I posted a new patch (v32) that hopefully addresses
these points.

01.

"After a successful run, the state of the target server is analagous to a fresh
logical replication setup."
a/analagous/analogous

Fixed.

02.

"The main difference between the logical replication setup and pg_createsubscriber
is the initial data copy."

Grammarly suggests:
"The initial data copy is the main difference between the logical replication
setup and pg_createsubscriber."

Not fixed.

03.

"Only the synchronization phase is done, which ensures each table is brought up
to a synchronized state."

This sentence is not very clear to me. How about:
"pg_createsubscriber does only the synchronization phase, ensuring each table's
replication state is ready."

I avoided pg_createsubscriber at the beginning because it is already
used in the previous sentence. I kept the last part of the sentence
because it is similar to one in the logical replication [1]https://www.postgresql.org/docs/current/logical-replication-architecture.html#LOGICAL-REPLICATION-SNAPSHOT.

04.

"The pg_createsubscriber targets large database systems because most of the
execution time is spent making the initial data copy."

Hmm, but initial data sync by logical replication also spends most of time to
make the initial data copy. IIUC bottlenecks are a) this application must stop
and start server several times, and b) only the serial copy works. Can you
clarify them?

Reading the sentence again, it is not clear. When I said "most of
execution time" I was referring to the actual logical replication setup.

05.

It is better to say the internal difference between pg_createsubscriber and the
initial sync by logical replication. For example:
pg_createsubscriber uses a physical replication mechanism to ensure the standby
catches up until a certain point. Then, it converts to the standby to the
subscriber by promoting and creating subscriptions.

Isn't it better to leave these details to "How It Works"?

06.

"If these are not met an error will be reported."

Grammarly suggests:
"If these are not met, an error will be reported."

Fixed.

07.

"The given target data directory must have the same system identifier than the
source data directory."

Grammarly suggests:
"The given target data directory must have the same system identifier as the
source data directory."

Fixed.

08.

"If a standby server is running on the target data directory or it is a base
backup from the source data directory, system identifiers are the same."

This line is not needed if bullet-style is not used. The line is just a supplement,
not prerequisite.

Fixed.

09.

"The source server must accept connections from the target server. The source server must not be in recovery."

Grammarly suggests:
"The source server must accept connections from the target server and not be in recovery."

Not fixed.

10.

"Publications cannot be created in a read-only cluster."

Same as 08, this line is not needed if bullet-style is not used.

Fixed.

11.

"pg_createsubscriber usually starts the target server with different connection
settings during the transformation steps. Hence, connections to target server
might fail."

Grammarly suggests:
"pg_createsubscriber usually starts the target server with different connection
settings during transformation. Hence, connections to the target server might fail."

Fixed.

12.

"During the recovery process,"

Grammarly suggests:
"During recovery,"

Not fixed. Our documentation uses "recovery process".

13.

"replicated so an error would occur."

Grammarly suggests:
"replicated, so an error would occur."

I didn't find this one. Maybe you checked a previous version.

14.

"It would avoid situations in which WAL files from the source server might be
used by the target server."

Grammarly suggests:
"It would avoid situations in which the target server might use WAL files from
the source server."

Fixed.

15.

"a LSN"

s/a/an

Fixed.

16.

"of write-ahead"

s/of/of the/

Fixed.

17.

"specifies promote"

We can do double-quote for the word promote.

Why? It is referring to recovery_target_action. If you check this GUC,
you will notice that it also uses literal tag.

18.

"are also added so it avoids"

Grammarly suggests:
"are added to avoid"

Fixed.

19.

"is accepting read-write transactions"

Grammarly suggests:
"accepts read-write transactions"

Not fixed.

20.

New options must be also documented as well. This helps not only users but also
reviewers.
(Sometimes we cannot identify that the implementation is intentinal or not.)

I don't know what are you referring to? If the new options are
--publication, --subscription and --replication-slot, they are
documented. Are you checking the latest patch?

21.

Also, not sure the specification is good. I preferred to specify them by format
string. Because it can reduce the number of arguments and I cannot find use cases
which user want to control the name of objects.

However, your approach has a benefit which users can easily identify the generated
objects by pg_createsubscriber. How do other think?

I prefer explicit options. We can always expand it later if people think it is a
good idea to provide a format string.

22.

```
#define BASE_OUTPUT_DIR "pg_createsubscriber_output.d"
```

No one refers the define.

It was removed in v30.

23.

```
} CreateSubscriberOptions;
...
} LogicalRepInfo;
```

Declarations after the "{" are not needed, because we do not do typedef.

It is a leftover when I removed the typedef.

22.

While seeing definitions of functions, I found that some pointers are declared
as const, but others are not. E.g., "char *lsn" in setup_recovery() won' be
changed but not the constant. Is it just missing or is there another rule?

It slipped my mind. Peter's fixups improves it.

23.

```
static int num_dbs = 0;
static int num_pubs = 0;
static int num_subs = 0;
static int num_replslots = 0;
```

I think the name is bit confusing. The number of generating publications/subscriptions/replication slots
are always same as the number of databases. They just indicate the number of
specified.

My idea is num_custom_pubs or something. Thought?

What does "custom" add to make the name clear? I added comments saying
so.

24.

```
/* standby / subscriber data directory */
static char *subscriber_dir = NULL;
```

It is bit strange that only subscriber_dir is a global variable. Caller requires
the CreateSubscriberOptions as an argument, except cleanup_objects_atexit() and
main. So, how about makeing `CreateSubscriberOptions opt` to global one?

I avoided turning all the options global variables. Since the cleanup routine
required the target data directory to be a global variable, I just did it and
left the others alone.

25.

```
* Replication slots, publications and subscriptions are created. Depending on
* the step it failed, it should remove the already created objects if it is
* possible (sometimes it won't work due to a connection issue).
```

I think it should be specified here that subscriptions won't be removed with the
reason.

I rephrased this comment.

26.

```

/*
* If the server is promoted, there is no way to use the current setup
* again. Warn the user that a new replication setup should be done before
* trying again.
*/
```

Per comment 25, we can add a reference like "See comments atop the function"

It is a few lines above. I don't think you have to point it out. If you
are unsure about this decision, you should check the whole function.

27.

usage() was not updated based on recent changes.

Check v30.

28.

```
if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
{
if (dbname)
*dbname = pg_strdup(conn_opt->val);
continue;
}
```

There is a memory-leak if multiple dbname are specified in the conninfo.

It is not a worrying or critical memory leak.

29.

```
pg_prng_seed(&prng_state, (uint64) (getpid() ^ time(NULL)));
```

No need to initialize the seed every time. Can you reuse pg_prng_state?

Sure.

30.

```
if (num_replslots == 0)
dbinfo[i].replslotname = pg_strdup(genname);
```

I think the straightforward way is to use the name of subscription if no name
is specified. This follows the rule for CREATE SUBSCRIPTION.

Agreed.

31.

```
/* Create replication slot on publisher */
if (lsn)
pg_free(lsn);
```

I think allocating/freeing memory is not so efficient.
Can we add a flag to create_logical_replication_slot() for controlling the
returning value (NULL or duplicated string)? We can use the condition (i == num_dbs-1)
as flag.

It is not. This code path is not critical. You are suggesting to add
complexity here. Efficiency is a good goal but in this case it only adds
complexity with small return.

32.

```
/*
* Close the connection. If exit_on_error is true, it has an undesired
* condition and it should exit immediately.
*/
static void
disconnect_database(PGconn *conn, bool exit_on_error)
```

In case of disconnect_database(), the second argument should have different name.
If it is true, the process exits unconditionally.
Also, comments atop the function must be fixed.

I choose a short name. The comment seems ok to me.

33.

```
wal_level = strdup(PQgetvalue(res, 0, 0));
```

pg_strdup should be used here.

Fixed.

34.

```
{"config-file", required_argument, NULL, 1},
{"publication", required_argument, NULL, 2},
{"replication-slot", required_argument, NULL, 3},
{"subscription", required_argument, NULL, 4},
```

The ordering looks strange for me. According to pg_upgarade and pg_basebackup,
options which do not have short notation are listed behind.

Fixed.

35.

```
opt.sub_port = palloc(16);
```

Per other lines, pg_alloc() should be used.

I think you meant pg_malloc. Fixed.

36.

```
pg_free(opt.sub_port);
```

You said that the leak won't be concerned here. If so, why only 'p' has pg_free()?

Fixed.

37.

```
/* Register a function to clean up objects in case of failure */
atexit(cleanup_objects_atexit);
```

Sorry if we have already discussed. I think the registration can be moved just
before the boot of the standby. Before that, the callback will be no-op.

The main reason is to catch future cases added *before* the point you
want to move this call that requires a cleanup. As you said it is a
no-op. My preference for atexit() calls is to add it as earlier as
possible to avoid leaving cases that it should trigger.

38.

```
/* Subscriber PID file */
snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);

/*
* If the standby server is running, stop it. Some parameters (that can
* only be set at server start) are informed by command-line options.
*/
if (stat(pidfile, &statbuf) == 0)
```

Hmm. pidfile is used only here, but it is declared in main(). Can it be
separated into another funtion like is_standby_started()?

It is so small that I didn't bother adding a new function for it.

39.

Or, we may able to introcue "restart_standby_if_needed" or something.

40.

```
* XXX this code was extracted from BootStrapXLOG().
```

So, can we extract the common part to somewhere? Since system identifier is related
with the controldata file, I think it can be located in controldata_util.c.

I added this comment here as a reference from where I extracted the
code. The referred function is from backend. Feel free to propose a
separate patch for it.

41.

You said like below in [1], but I could not find the related fix. Can you clarify?

That's a good point. We should state in the documentation that GUCs specified in
the command-line options are ignored during the execution.

I added a sentence for it. See "How It Works".

[1]: https://www.postgresql.org/docs/current/logical-replication-architecture.html#LOGICAL-REPLICATION-SNAPSHOT

--
Euler Taveira
EDB https://www.enterprisedb.com/

#219Euler Taveira
euler@eulerto.com
In reply to: Peter Eisentraut (#212)
Re: speed up a logical replica setup

On Tue, Mar 19, 2024, at 8:57 AM, Peter Eisentraut wrote:

On 19.03.24 12:26, Shlok Kyal wrote:

I have added a top-up patch v30-0003. The issue in [1] still exists in
the v30 patch. I was not able to come up with an approach to handle it
in the code, so I have added it to the documentation in the warning
section. Thoughts?

Seems acceptable to me. pg_createsubscriber will probably always have
some restrictions and unsupported edge cases like that. We can't
support everything, so documenting is ok.

Shlok, I'm not sure we should add a sentence about a pilot error. I added a
comment in check_subscriber that describes this situation. I think the comment
is sufficient to understand the limitation and, if it is possible in the future,
a check might be added for it. I didn't include v31-0004.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#220Euler Taveira
euler@eulerto.com
In reply to: Shubham Khanna (#216)
Re: speed up a logical replica setup

On Wed, Mar 20, 2024, at 7:16 AM, Shubham Khanna wrote:

On Tue, Mar 19, 2024 at 8:54 PM vignesh C <vignesh21@gmail.com> wrote:

If you are not planning to have the checks for name length, this could
alternatively be fixed by including database id also while querying
pg_subscription like below in set_replication_progress function:
appendPQExpBuffer(str,
"SELECT oid FROM pg_catalog.pg_subscription \n"
"WHERE subname = '%s' AND subdbid = (SELECT oid FROM
pg_catalog.pg_database WHERE datname = '%s')",
dbinfo->subname,
dbinfo->dbname);

The attached patch has the changes to handle the same.

I included a different query that does the same. See v32.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#221Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Euler Taveira (#217)
3 attachment(s)
Re: speed up a logical replica setup

Hi,

There is a compilation error while building postgres with the patch
due to a recent commit. I have attached a top-up patch v32-0003 to
resolve this compilation error.
I have not updated the version of the patch as I have not made any
change in v32-0001 and v32-0002 patch.

Thanks and regards,
Shlok Kyal

Attachments:

v32-0003-Fix-compilation-error.patchapplication/x-patch; name=v32-0003-Fix-compilation-error.patchDownload
From 164b956524ce2b468eab8c7bbb5d0748288c028c Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 21 Mar 2024 15:04:58 +0530
Subject: [PATCH v32 3/3] Fix compilation error

Fix compilation error due to a recent commit
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index f146fcb5ed..3a8240ea78 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1109,7 +1109,7 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 * state is reached (recovery_target) and failure due to multiple recovery
 	 * targets (name, time, xid, LSN).
 	 */
-	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);
 	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
 	appendPQExpBuffer(recoveryconfcontents,
 					  "recovery_target_timeline = 'latest'\n");
-- 
2.34.1

v32-0002-Remove-How-it-Works-section.patchapplication/x-patch; name=v32-0002-Remove-How-it-Works-section.patchDownload
From 22b5a065f7116bb3f9ba44500eb1a1948e523410 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Thu, 21 Mar 2024 00:16:21 -0300
Subject: [PATCH v32 2/3] Remove How it Works section

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 128 ----------------------
 1 file changed, 128 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index f0cfed8c47..fc890492a8 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -373,134 +373,6 @@ PostgreSQL documentation
 
  </refsect1>
 
- <refsect1>
-  <title>How It Works</title>
-
-  <para>
-    The basic idea is to have a replication start point from the source server
-    and set up a logical replication to start from this point:
-  </para>
-
-  <procedure>
-   <step>
-    <para>
-     Start the target server with the specified command-line options. If the
-     target server is running, <application>pg_createsubscriber</application>
-     will terminate with an error.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Check if the target server can be converted. There are also a few checks
-     on the source server. If any of the prerequisites are not met,
-     <application>pg_createsubscriber</application> will terminate with an
-     error.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Create a publication and replication slot for each specified database on
-     the source server. Each publication is created using
-     <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>.
-     If <option>publication-name</option> option is not specified, it has the
-     following name pattern:
-     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameter:
-     database <parameter>oid</parameter>, random <parameter>int</parameter>).
-     If <option>replication-slot-name</option> is not specified, the
-     replication slot has the following name pattern:
-     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
-     database <parameter>oid</parameter>, random <parameter>int</parameter>).
-     These replication slots will be used by the subscriptions in a future step.
-     The last replication slot LSN is used as a stopping point in the
-     <xref linkend="guc-recovery-target-lsn"/> parameter and by the
-     subscriptions as a replication start point. It guarantees that no
-     transaction will be lost.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Write recovery parameters into the target data directory and restart the
-     target server. It specifies an LSN (<xref linkend="guc-recovery-target-lsn"/>)
-     of the write-ahead log location up to which recovery will proceed. It also
-     specifies <literal>promote</literal> as the action that the server should
-     take once the recovery target is reached. Additional
-     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
-     are added to avoid unexpected behavior during the recovery process such as
-     end of the recovery as soon as a consistent state is reached (WAL should
-     be applied until the replication start location) and multiple recovery
-     targets that can cause a failure. This step finishes once the server ends
-     standby mode and is accepting read-write transactions.
-     If <option>--recovery-timeout</option> option is set,
-     <application>pg_createsubscriber</application> terminates if recovery does
-     not end until the given number of seconds.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Create a subscription for each specified database on the target server.
-     If <option>subscription-name</option> is not specified, the subscription
-     has the following name pattern:
-     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
-     database <parameter>oid</parameter>, random <parameter>int</parameter>).
-     It does not copy existing data from the source server. It does not create
-     a replication slot. Instead, it uses the replication slot that was created
-     in a previous step. The subscription is created but it is not enabled yet.
-     The reason is the replication progress must be set to the replication
-     start point before starting the replication.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Drop publications on the target server that were replicated because they
-     were created before the replication start location. It has no use on the
-     subscriber.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Set the replication progress to the replication start point for each
-     subscription. When the target server starts the recovery process, it
-     catches up to the replication start point. This is the exact LSN to be used
-     as a initial replication location for each subscription. The replication
-     origin name is obtained since the subscription was created. The replication
-     origin name and the replication start point are used in
-     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
-     to set up the initial replication location.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Enable the subscription for each specified database on the target server.
-     The subscription starts applying transactions from the replication start
-     point.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     If the standby server was using
-     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
-     it has no use from now on so drop it.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Update the system identifier on the target server. The
-     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
-     The target server is stopped as a <command>pg_resetwal</command> requirement.
-    </para>
-   </step>
-  </procedure>
- </refsect1>
-
  <refsect1>
   <title>Examples</title>
 
-- 
2.34.1

v32-0001-pg_createsubscriber-creates-a-new-logical-replic.patchapplication/x-patch; name=v32-0001-pg_createsubscriber-creates-a-new-logical-replic.patchDownload
From 6df7766c640cdae34bb0524e8b8a43e2aef94b34 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v32 1/3] pg_createsubscriber: creates a new logical replica
 from a standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber). All tables
in the specified database(s) are included in the logical replication
setup. A pair of publication and subscription objects are created for
each database.

The main advantage of pg_createsubscriber over the common logical
replication setup is the initial data copy. It also reduces the catchup
phase.

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
using FOR ALL TABLES and a replication slot for each specified database.
Write recovery parameters into the target data directory and start the
target server. It specifies the LSN of the last replication slot
(replication start point) up to which the recovery will proceed. Wait
until the target server is promoted. Create one subscription per
specified database (using publication and replication slot created in a
previous step) on the target server. Set the replication progress to the
replication start point for each subscription. Enable the subscription
for each specified database on the target server. And finally, change
the system identifier on the target server.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  525 ++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/nls.mk                  |    1 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2118 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  327 +++
 9 files changed, 3001 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f0cfed8c47
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,525 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. All tables in the specified
+    database are included in the logical replication setup. A pair of
+    publication and subscription objects are created for each database. It
+    must be run at the target server.
+  </para>
+
+  <para>
+    After a successful run, the state of the target server is analogous to a
+    fresh logical replication setup. The main difference between the logical
+    replication setup and <application>pg_createsubscriber</application>
+    is the initial data copy. It does only the synchronization phase, which
+    ensures each table is brought up to a synchronized state.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because in logical replication setup, most of the time is spent
+   doing the initial data copy. Furthermore, a side effect of this long time
+   spent synchronizing data is usually a large amount of changes to be applied
+   (that were produced during the initial data copy) which increases even more
+   the time when the logical replica will be available. For smaller databases,
+   <link linkend="logical-replication">initial data synchronization</link> is
+   recommended.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--config-file=<replaceable class="parameter">filename</replaceable></option></term>
+      <listitem>
+       <para>
+        Use the specified main server configuration file for the target
+        data directory. The <application>pg_createsubscriber</application>
+        uses internally the <application>pg_ctl</application> command to start
+        and stop the target server. It allows you to specify the actual
+        <filename>postgresql.conf</filename> configuration file if it is stored
+        outside the data directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--publication=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The publication name to set up the logical replication. Multiple
+        publications can be specified by writing multiple
+        <option>--publication</option> switches. The number of publication
+        names must match the number of specified databases, otherwise an error
+        is reported. The order of the multiple publication name switches must
+        match the order of database switches. If this option is not specified,
+        a generated name is assigned to the publication name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--subscription=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The subscription name to set up the logical replication. Multiple
+        subscriptions can be specified by writing multiple
+        <option>--subscription</option> switches. The number of subscription
+        names must match the number of specified databases, otherwise an error
+        is reported. The order of the multiple subscription name switches must
+        match the order of database switches. If this option is not specified,
+        a generated name is assigned to the subscription name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--replication-slot=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The replication slot name to set up the logical replication. Multiple
+        replication slots can be specified by writing multiple
+        <option>--replication-slot</option> switches. The number of replication
+        slot names must match the number of specified databases, otherwise an
+        error is reported. The order of the multiple replication slot name
+        switches must match the order of database switches. If this option is
+        not specified, the subscription name is assigned to the replication
+        slot name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   There are some prerequisites for
+   <application>pg_createsubscriber</application> to convert the target server
+   into a logical replica. If these are not met, an error will be reported. The
+   source and target servers must have the same major version as the
+   <application>pg_createsubscriber</application>. The given target data
+   directory must have the same system identifier as the source data directory.
+   The given database user for the target data directory must have privileges
+   for creating <link linkend="sql-createsubscription">subscriptions</link> and
+   using
+   <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+  </para>
+
+  <para>
+   The target server must be used as a physical standby. The target server
+   must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   and
+   <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases. The target server must have
+   <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+   configured to a value greater than the number of specified databases. The
+   target server must accept local connections.
+  </para>
+
+  <para>
+   The source server must accept connections from the target server. The
+   source server must not be in recovery. The source server must have
+   <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+   <literal>logical</literal>.
+   The source server must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases plus existing replication slots. The source server must have
+   <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases and existing <literal>walsender</literal> processes.
+  </para>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails after the target
+    server was promoted, then the data directory is likely not in a state that
+    can be recovered. In such case, creating a new standby server is
+    recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during transformation. Hence,
+    connections to the target server should fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Since DDL commands are not replicated by logical replication, avoid
+    executing DDL commands that change the database schema while running
+    <application>pg_createsubscriber</application>. If the target server has
+    already been converted to logical replica, the DDL commands must not be
+    replicated which might cause an error.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    are removed. The removal might fail if the target server cannot connect to
+    the source server. In such a case, a warning message will inform the
+    objects left. If the target server is running, it will be stopped.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    If the target server is a synchronous replica, transaction commits on the
+    primary might wait for replication while running
+    <application>pg_createsubscriber</application>.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which the target server might use WAL files from the source server. If the
+    target server has a standby, replication will break and a fresh standby
+    should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is running, <application>pg_createsubscriber</application>
+     will terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. If any of the prerequisites are not met,
+     <application>pg_createsubscriber</application> will terminate with an
+     error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication is created using
+     <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>.
+     If <option>publication-name</option> option is not specified, it has the
+     following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameter:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     If <option>replication-slot-name</option> is not specified, the
+     replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+     The last replication slot LSN is used as a stopping point in the
+     <xref linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication start point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies an LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of the write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are added to avoid unexpected behavior during the recovery process such as
+     end of the recovery as soon as a consistent state is reached (WAL should
+     be applied until the replication start location) and multiple recovery
+     targets that can cause a failure. This step finishes once the server ends
+     standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     If <option>subscription-name</option> is not specified, the subscription
+     has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     It does not copy existing data from the source server. It does not create
+     a replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the replication
+     start point before starting the replication.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the replication start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the replication start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the replication start point. This is the exact LSN to be used
+     as a initial replication location for each subscription. The replication
+     origin name is obtained since the subscription was created. The replication
+     origin name and the replication start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the replication start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..26c53e473f 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: pg_createsubscriber.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/nls.mk b/src/bin/pg_basebackup/nls.mk
index fc475003e8..7870cea71c 100644
--- a/src/bin/pg_basebackup/nls.mk
+++ b/src/bin/pg_basebackup/nls.mk
@@ -8,6 +8,7 @@ GETTEXT_FILES    = $(FRONTEND_COMMON_GETTEXT_FILES) \
                    bbstreamer_tar.c \
                    bbstreamer_zstd.c \
                    pg_basebackup.c \
+                   pg_createsubscriber.c \
                    pg_receivewal.c \
                    pg_recvlogical.c \
                    receivelog.c \
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..f146fcb5ed
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2118 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/pg_prng.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	"50432"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *config_file;	/* configuration file */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	char	   *sub_port;		/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	SimpleStringList pub_names; /* list of publication names */
+	SimpleStringList sub_names; /* list of subscription names */
+	SimpleStringList replslot_names;	/* list of replication slot names */
+	int			recovery_timeout;	/* stop recovery after this time */
+};
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name */
+	char	   *replslotname;	/* replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+};
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(const char *conninfo, char **dbname);
+static char *get_sub_conninfo(const struct CreateSubscriberOptions *opt);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(const struct CreateSubscriberOptions *opt,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static char *generate_object_name(PGconn *conn);
+static void check_publisher(const struct LogicalRepInfo *dbinfo);
+static char *setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(const struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static void setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir,
+						   const char *lsn);
+static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
+										  const char *slotname);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(const struct CreateSubscriberOptions *opt,
+								 bool restricted_access);
+static void stop_standby_server(const char *datadir);
+static void wait_for_end_recovery(const char *conninfo,
+								  const struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;		/* number of specified databases */
+static int	num_pubs = 0;		/* number of specified publications */
+static int	num_subs = 0;		/* number of specified subscriptions */
+static int	num_replslots = 0;	/* number of specified replication slots */
+
+static pg_prng_state prng_state;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+/* standby / subscriber data directory */
+static char *subscriber_dir = NULL;
+
+static bool recovery_ended = false;
+static bool standby_running = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Publications and replication slots are created on primary. Depending on the
+ * step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ * There is no cleanup on the target server. The steps on the target server are
+ * executed *after* promotion, hence, at this point, a failure means recreate
+ * the physical replica and start again.
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.  "
+							"You must recreate the physical replica before continuing.");
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			PGconn	   *conn;
+
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].replslotname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("publication \"%s\" in database \"%s\" on primary might be left behind",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("replication slot \"%s\" in database \"%s\" on primary might be left behind",
+								   dbinfo[i].replslotname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+
+	if (standby_running)
+		stop_standby_server(subscriber_dir);
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %s)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_("     --config-file=FILENAME          use specified main server configuration\n"
+			 "                                     file when running target cluster\n"));
+	printf(_("     --publication=NAME              publication name\n"));
+	printf(_("     --replication-slot=NAME         replication slot name\n"));
+	printf(_("     --subscription=NAME             subscription name\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(const char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Build a subscriber connection string. Only a few parameters are supported
+ * since it starts a server with restricted access.
+ */
+static char *
+get_sub_conninfo(const struct CreateSubscriberOptions *opt)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	appendPQExpBuffer(buf, "port=%s", opt->sub_port);
+#if !defined(WIN32)
+	appendPQExpBuffer(buf, " host=%s", opt->socket_dir);
+#endif
+	if (opt->sub_username != NULL)
+		appendPQExpBuffer(buf, " user=%s", opt->sub_username);
+	appendPQExpBuffer(buf, " fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ *
+ * If publication, replication slot and subscription names were specified,
+ * store it here. Otherwise, a generated name will be assigned to the object in
+ * setup_publisher().
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(const struct CreateSubscriberOptions *opt,
+				   const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	SimpleStringListCell *pubcell = NULL;
+	SimpleStringListCell *subcell = NULL;
+	SimpleStringListCell *replslotcell = NULL;
+	int			i = 0;
+
+	dbinfo = pg_malloc_array(struct LogicalRepInfo, num_dbs);
+
+	if (num_pubs > 0)
+		pubcell = opt->pub_names.head;
+	if (num_subs > 0)
+		subcell = opt->sub_names.head;
+	if (num_replslots > 0)
+		replslotcell = opt->replslot_names.head;
+
+	for (SimpleStringListCell *cell = opt->database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		if (num_pubs > 0)
+			dbinfo[i].pubname = pubcell->val;
+		else
+			dbinfo[i].pubname = NULL;
+		if (num_replslots > 0)
+			dbinfo[i].replslotname = replslotcell->val;
+		else
+			dbinfo[i].replslotname = NULL;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		if (num_subs > 0)
+			dbinfo[i].subname = subcell->val;
+		else
+			dbinfo[i].subname = NULL;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): publication: %s ; replication slot: %s ; connection string: %s", i,
+					 dbinfo[i].pubname ? dbinfo[i].pubname : "(auto)",
+					 dbinfo[i].replslotname ? dbinfo[i].replslotname : "(auto)",
+					 dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): subscription: %s ; connection string: %s", i,
+					 dbinfo[i].subname ? dbinfo[i].subname : "(auto)",
+					 dbinfo[i].subconninfo);
+
+		if (num_pubs > 0)
+			pubcell = pubcell->next;
+		if (num_subs > 0)
+			subcell = subcell->next;
+		if (num_replslots > 0)
+			replslotcell = replslotcell->next;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier of subscriber");
+
+	cf = get_controlfile(subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   subscriber_dir, DEVNULL);
+
+	pg_log_debug("pg_resetwal command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Generate an object name using a prefix, database oid and a random integer.
+ * It is used in case the user does not specify an object name (publication,
+ * subscription, replication slot).
+ */
+static char *
+generate_object_name(PGconn *conn)
+{
+	PGresult   *res;
+	Oid			oid;
+	uint32		rand;
+	char	   *objname;
+
+	res = PQexec(conn,
+				 "SELECT oid FROM pg_catalog.pg_database "
+				 "WHERE datname = pg_catalog.current_database()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain database OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	/* Database OID */
+	oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+	PQclear(res);
+
+	/* Random unsigned integer */
+	rand = pg_prng_uint32(&prng_state);
+
+	/*
+	 * Build the object name. The name must not exceed NAMEDATALEN - 1. This
+	 * current schema uses a maximum of 40 characters (20 + 10 + 1 + 8 +
+	 * '\0').
+	 */
+	objname = psprintf("pg_createsubscriber_%u_%x", oid, rand);
+
+	return objname;
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication. Returns the LSN from latest replication slot. It will be the
+ * replication start point that is used to adjust the subscriptions (see
+ * set_replication_progress).
+ */
+static char *
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	char	   *lsn = NULL;
+
+	pg_prng_seed(&prng_state, (uint64) (getpid() ^ time(NULL)));
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		char	   *genname = NULL;
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		/*
+		 * If an object name was not specified as command-line options, assign
+		 * a generated object name. The replication slot has a different rule.
+		 * The subscription name is assigned to the replication slot name if no
+		 * replication slot is specified. It follows the same rule as CREATE
+		 * SUBSCRIPTION.
+		 */
+		if (num_pubs == 0 || num_subs == 0 || num_replslots == 0)
+			genname = generate_object_name(conn);
+		if (num_pubs == 0)
+			dbinfo[i].pubname = pg_strdup(genname);
+		if (num_subs == 0)
+			dbinfo[i].subname = pg_strdup(genname);
+		if (num_replslots == 0)
+			dbinfo[i].replslotname = pg_strdup(dbinfo[i].subname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/* Create replication slot on publisher */
+		if (lsn)
+			pg_free(lsn);
+		lsn = create_logical_replication_slot(conn, &dbinfo[i]);
+		if (lsn != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						dbinfo[i].replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+
+	return lsn;
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ *
+ * XXX Does it not allow a synchronous replica?
+ */
+static void
+check_publisher(const struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	bool		failed = false;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = pg_strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		failed = true;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
+		failed = true;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ *
+ * XXX Does it not allow a time-delayed replica?
+ *
+ * XXX In a cascaded replication scenario (P -> S -> C), if the target server
+ * is S, it cannot detect there is a replica (server C) because server S starts
+ * accepting only local connections and server C cannot connect to it. Hence,
+ * there is not a reliable way to provide a suitable error saying the server C
+ * will be broken at the end of this process (due to pg_resetwal).
+ */
+static void
+check_subscriber(const struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	bool		failed = false;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * replication setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Write the required recovery parameters.
+ */
+static void
+setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const char *lsn)
+{
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	/*
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection. The primary_conninfo is generated using the
+	 * connection settings.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * The subscriber is not running yet. In dry run mode, the recovery
+	 * parameters *won't* be written. An invalid LSN is used for printing
+	 * purposes. Additional recovery parameters are added here. It avoids
+	 * unexpected behavior such as end of recovery as soon as a consistent
+	 * state is reached (recovery_target) and failure due to multiple recovery
+	 * targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  lsn);
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+}
+
+/*
+ * Drop physical replication slot on primary if the standby was using it. After
+ * the transformation, it has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, const char *slotname)
+{
+	PGconn	   *conn;
+
+	/* Replication slot does not exist, do nothing */
+	if (!primary_slot_name)
+		return;
+
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn != NULL)
+	{
+		drop_replication_slot(conn, &dbinfo[0], slotname);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop replication slot \"%s\" on primary",
+					   slotname);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	const char *slot_name = dbinfo->replslotname;
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(const struct CreateSubscriberOptions *opt, bool restricted_access)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	int			rc;
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, subscriber_dir);
+	if (restricted_access)
+	{
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %s\"", opt->sub_port);
+#if !defined(WIN32)
+
+		/*
+		 * An empty listen_addresses list means the server does not listen on
+		 * any IP interfaces; only Unix-domain sockets can be used to connect
+		 * to the server. Prevent external connections to minimize the chance
+		 * of failure.
+		 */
+		appendPQExpBufferStr(pg_ctl_cmd, " -o \"-c listen_addresses='' -c unix_socket_permissions=0700");
+		if (opt->socket_dir)
+			appendPQExpBuffer(pg_ctl_cmd, " -c unix_socket_directories='%s'",
+							  opt->socket_dir);
+		appendPQExpBufferChar(pg_ctl_cmd, '"');
+#endif
+	}
+	if (opt->config_file != NULL)
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-c config_file=%s\"",
+						  opt->config_file);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	standby_running = true;
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	standby_running = false;
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	10
+
+	pg_log_info("waiting for the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *ipubname;
+	char	   *spubname;
+
+	Assert(conn != NULL);
+
+	ipubname = PQescapeIdentifier(conn, dbinfo->pubname, strlen(dbinfo->pubname));
+	spubname = PQescapeLiteral(conn, dbinfo->pubname, strlen(dbinfo->pubname));
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = %s",
+					  spubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid and
+		 * a random number.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  ipubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	pg_free(ipubname);
+	pg_free(spubname);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *pubname;
+
+	Assert(conn != NULL);
+
+	pubname = PQescapeIdentifier(conn, dbinfo->pubname, strlen(dbinfo->pubname));
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	pg_free(pubname);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it.  It
+ * is not required to copy data. The subscription will be created but it will
+ * not be enabled now. That's because the replication progress must be set and
+ * the replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *pubname;
+	char	   *subname;
+
+	Assert(conn != NULL);
+
+	pubname = PQescapeIdentifier(conn, dbinfo->pubname, strlen(dbinfo->pubname));
+	subname = PQescapeIdentifier(conn, dbinfo->subname, strlen(dbinfo->subname));
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, enabled = false, "
+					  "slot_name = '%s', copy_data = false)",
+					  subname, dbinfo->pubconninfo, pubname,
+					  dbinfo->replslotname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	pg_free(pubname);
+	pg_free(subname);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the last
+ * replication slot that was created. The goal is to set up the initial
+ * location for the logical replication that is the exact LSN that the
+ * subscriber was promoted. Once the subscription is enabled it will start
+ * streaming from that location onwards.  In dry run mode, the subscription OID
+ * and LSN are set to invalid values for printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char	   *subname;
+	char	   *dbname;
+	char	   *originname;
+	char	   *lsnstr;
+
+	Assert(conn != NULL);
+
+	subname = PQescapeLiteral(conn, dbinfo->subname, strlen(dbinfo->subname));
+	dbname = PQescapeLiteral(conn, dbinfo->dbname, strlen(dbinfo->dbname));
+
+	appendPQExpBuffer(str,
+					  "SELECT s.oid FROM pg_catalog.pg_subscription s "
+					  "INNER JOIN pg_catalog.pg_database d ON (s.subdbid = d.oid) "
+					  "WHERE s.subname = %s AND d.datname = %s",
+					  subname, dbname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		lsnstr = psprintf("%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		lsnstr = psprintf("%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	originname = psprintf("pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	pg_free(subname);
+	pg_free(dbname);
+	pg_free(originname);
+	pg_free(lsnstr);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *subname;
+
+	Assert(conn != NULL);
+
+	subname = PQescapeIdentifier(conn, dbinfo->subname, strlen(dbinfo->subname));
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	pg_free(subname);
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{"config-file", required_argument, NULL, 1},
+		{"publication", required_argument, NULL, 2},
+		{"replication-slot", required_argument, NULL, 3},
+		{"subscription", required_argument, NULL, 4},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pub_base_conninfo;
+	char	   *sub_base_conninfo;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	char	   *consistent_lsn;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	subscriber_dir = NULL;
+	opt.config_file = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList){0};
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:np:P:s:t:U:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				else
+				{
+					pg_log_error("duplicate database \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				opt.sub_port = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				canonicalize_path(opt.socket_dir);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			case 1:
+				opt.config_file = pg_strdup(optarg);
+				break;
+			case 2:
+				if (!simple_string_list_member(&opt.pub_names, optarg))
+				{
+					simple_string_list_append(&opt.pub_names, optarg);
+					num_pubs++;
+				}
+				else
+				{
+					pg_log_error("duplicate publication \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 3:
+				if (!simple_string_list_member(&opt.replslot_names, optarg))
+				{
+					simple_string_list_append(&opt.replslot_names, optarg);
+					num_replslots++;
+				}
+				else
+				{
+					pg_log_error("duplicate replication slot \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 4:
+				if (!simple_string_list_member(&opt.sub_names, optarg))
+				{
+					simple_string_list_append(&opt.sub_names, optarg);
+					num_subs++;
+				}
+				else
+				{
+					pg_log_error("duplicate subscription \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/* Any non-option arguments? */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* Required arguments */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* If socket directory is not provided, use the current directory */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_sub_conninfo(&opt);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Number of object names must match number of databases */
+	if (num_pubs > 0 && num_pubs != num_dbs)
+	{
+		pg_log_error("wrong number of publication names");
+		pg_log_error_hint("Number of publication names (%d) must match number of database names (%d).",
+						  num_pubs, num_dbs);
+		exit(1);
+	}
+	if (num_subs > 0 && num_subs != num_dbs)
+	{
+		pg_log_error("wrong number of subscription names");
+		pg_log_error_hint("Number of subscription names (%d) must match number of database names (%d).",
+						  num_subs, num_dbs);
+		exit(1);
+	}
+	if (num_replslots > 0 && num_replslots != num_dbs)
+	{
+		pg_log_error("wrong number of replication slot names");
+		pg_log_error_hint("Number of replication slot names (%d) must match number of database names (%d).",
+						  num_replslots, num_dbs);
+		exit(1);
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(&opt, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * The standby server must not be running. If the server is started under
+	 * service manager and pg_createsubscriber stops it, the service manager
+	 * might react to this action and start the server again. Therefore, refuse
+	 * to proceed if the server is running to avoid possible failures.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		pg_log_error("standby is up and running");
+		pg_log_error_hint("Stop the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Stop the target server. The recovery process requires that the server
+	 * reaches a consistent state before targeting the recovery stop point.
+	 * Make sure a consistent state is reached (stop the target server
+	 * guarantees it) *before* creating the replication slots in
+	 * setup_publisher().
+	 */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(subscriber_dir);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	consistent_lsn = setup_publisher(dbinfo);
+
+	/* Write the required recovery parameters */
+	setup_recovery(dbinfo, subscriber_dir, consistent_lsn);
+
+	/*
+	 * Start subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	start_standby_server(&opt, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the replication start
+	 * point to the LSN reported by setup_publisher().  It also cleans up
+	 * publications created by this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/* Remove primary_slot_name if it exists on primary */
+	drop_primary_replication_slot(dbinfo, primary_slot_name);
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(&opt);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..243153645a
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,327 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--database', 'pg1',
+		'--database', 'pg1'
+	],
+	'duplicate database name');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'duplicate publication name');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of publication names');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo2',
+		'--subscription', 'bar1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of subscription names');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo2',
+		'--subscription', 'bar1',
+		'--subscription', 'bar2',
+		'--replication-slot', 'baz1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of replication slot names');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->adjust_conf('postgresql.conf', 'primary_slot_name', undef);
+$node_c->set_standby_mode();
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Check some unmet conditions on node P
+$node_p->append_conf('postgresql.conf', q{
+wal_level = replica
+max_replication_slots = 1
+max_wal_senders = 1
+max_worker_processes = 2
+});
+$node_p->restart;
+$node_s->stop;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary contains unmet conditions on node P');
+# Restore default settings here but only apply it after testing standby. Some
+# standby settings should not be a lower setting than on the primary.
+$node_p->append_conf('postgresql.conf', q{
+wal_level = logical
+max_replication_slots = 10
+max_wal_senders = 10
+max_worker_processes = 8
+});
+
+# Check some unmet conditions on node S
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 1
+max_logical_replication_workers = 1
+max_worker_processes = 2
+});
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'standby contains unmet conditions on node S');
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 10
+max_logical_replication_workers = 4
+max_worker_processes = 8
+});
+# Restore default settings on both servers
+$node_p->restart;
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--publication', 'pub1',
+		'--publication', 'pub2',
+		'--subscription', 'sub1',
+		'--subscription', 'sub2',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+$node_s->start;
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+$node_s->stop;
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--replication-slot', 'replslot1'
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--publication', 'pub1',
+		'--publication', 'Pub2',
+		'--replication-slot', 'replslot1',
+		'--replication-slot', 'replslot2',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# Start subscriber
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.34.1

#222vignesh C
vignesh21@gmail.com
In reply to: Euler Taveira (#217)
Re: speed up a logical replica setup

On Thu, 21 Mar 2024 at 09:50, Euler Taveira <euler@eulerto.com> wrote:

On Mon, Mar 18, 2024, at 10:52 AM, Peter Eisentraut wrote:

On 16.03.24 16:42, Euler Taveira wrote:

I'm attaching a new version (v30) that adds:

I have some review comments and attached a patch with some smaller
fixups (mainly message wording and avoid fixed-size string buffers).

Thanks for your review. I'm attaching a new version (v32) that includes your
fixups, merges the v30-0002 into the main patch [1], addresses Hayato's review[2],
your reviews [3][4], and fixes the query for set_replication_progress() [5].

* doc/src/sgml/ref/pg_createsubscriber.sgml

I would remove the "How It Works" section. This is not relevant to
users, and it is very detailed and will require updating whenever the
implementation changes. It could be a source code comment instead.

It uses the same structure as pg_rewind that also describes how it works
internally. I included a separate patch that completely removes the section.

* src/bin/pg_basebackup/pg_createsubscriber.c

I think the connection string handling is not robust against funny
characters, like spaces, in database names etc.

get_base_conninfo() uses PQconninfoParse to parse the connection string. I
expect PQconnectdb to provide a suitable error message in this case. Even if it
builds keywords and values arrays, it is also susceptible to the same issue, no?

Most SQL commands need to be amended for proper identifier or string
literal quoting and/or escaping.

I completely forgot about this detail when I added the new options in v30. It is
fixed now. I also changed the tests to exercise it.

In check_subscriber(): All these permissions checks seem problematic
to me. We shouldn't reimplement our own copy of the server's
permission checks. The server can check the permissions. And if the
permission checking in the server ever changes, then we have
inconsistencies to take care of. Also, the error messages "permission
denied" are inappropriate, because we are not doing the actual thing.
Maybe we want to do a dry-run for the benefit of the user, but then we
should do the actual thing, like try to create a replication slot, or
whatever. But I would rather just remove all this, it seems too
problematic.

The main goal of the check_* functions are to minimize error during execution.
I removed the permission checks. The GUC checks were kept.

In main(): The first check if the standby is running is problematic.
I think it would be better to require that the standby is initially
shut down. Consider, the standby might be running under systemd.
This tool will try to stop it, systemd will try to restart it. Let's
avoid these kinds of battles. It's also safer if we don't try to
touch running servers.

That's a good point. I hadn't found an excuse to simplify this but you provided
one. :) There was a worry about ignoring some command-line options that changes
GUCs if the server was started. There was also an ugly case for dry run mode
that has to start the server (if it was running) at the end. Both cases are no
longer issues. The current code provides a suitable error if the target server
is running.

The -p option (--subscriber-port) doesn't seem to do anything. In my
testing, it always uses the compiled-in default port.

It works for me. See this snippet from the regression tests. The port (50945) is
used by pg_ctl.

# Running: pg_createsubscriber --verbose --verbose --pgdata /c/pg_createsubscriber/src/bin/pg_basebackup/tmp_check/t_040_pg_createsubscriber_node_s_data/pgdata --publisher-server port=50943 host=/tmp/qpngb0bPKo dbname='pg1' --socket-directory /tmp/qpngb0bPKo --subscriber-port 50945 --database pg1 --database pg2
pg_createsubscriber: validating connection string on publisher
.
.
pg_createsubscriber: pg_ctl command is: "/c/pg_createsubscriber/tmp_install/c/pg_createsubscriber/bin/pg_ctl" start -D "/c/pg_createsubscriber/src/bin/pg_basebackup/tmp_check/t_040_pg_createsubscriber_node_s_data/pgdata" -s -o "-p 50945" -o "-c listen_addresses='' -c unix_socket_permissions=0700 -c unix_socket_directories='/tmp/qpngb0bPKo'"
2024-03-20 18:15:24.517 -03 [105195] LOG: starting PostgreSQL 17devel on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
2024-03-20 18:15:24.517 -03 [105195] LOG: listening on Unix socket "/tmp/qpngb0bPKo/.s.PGSQL.50945"

Printing all the server log lines to the terminal doesn't seem very
user-friendly. Not sure what to do about that, short of keeping a
pg_upgrade-style directory of log files. But it's ugly.

I removed the previous implementation that creates a new directory and stores
the log file there. I don't like the pg_upgrade-style directory because (a) it
stores part of the server log files in another place and (b) it is another
directory to ignore if your tool handles the data directory (like a backup
tool). My last test said it prints 35 server log lines. I expect that the user
redirects the output to a file so he/she can inspect it later if required.

Here are a few suggestions:
1) I felt displaying the server log to the console is not a good idea,
I prefer this to be logged. There were similar comments from
Kuroda-san at [1]/messages/by-id/TY3PR01MB9889593399165B9A04106741F5662@TY3PR01MB9889.jpnprd01.prod.outlook.com, Peter at [2]/messages/by-id/7a970912-0b77-4942-84f7-2c9ca0bc05a5@eisentraut.org. The number of lines will increase
based on the log level set. If you don't want to use pg_upgrade style,
how about exposing the log file option and logging it to the specified
log file.

2) Currently for publication, replication-slot and subscription, we
will have to specify these options based on the number of databases.
Here if we have 100 databases we will have to specify these options
100 times, it might not be user friendly. How about something like
what Tomas had proposed at [3] and  Amit proposed at [4]. It will be
better if the user has to just specify publication, replication slot
and subscription options only one time.
+       /* Number of object names must match number of databases */
+       if (num_pubs > 0 && num_pubs != num_dbs)
+       {
+               pg_log_error("wrong number of publication names");
+               pg_log_error_hint("Number of publication names (%d)
must match number of database names (%d).",
+                                                 num_pubs, num_dbs);
+               exit(1);
+       }

3) Can we have an option similar to dry-run which will display the
configurations required in the primary and standby node something
like:
pg_createsubscriber -D data_N2/ -P "port=5431 user=postgres" -p 9999
-s /home/vignesh/postgres/inst/bin/ -U postgres -d db1 -d db2
--suggest-config
Suggested optimal configurations in the primary:
--------------------------------------
wallevel = logical
max_replication_slots = 3
max_wal_senders = 3
...
Suggested optimal configurations in the standby:
--------------------------------------
max_replication_slots = 3
max_wal_senders = 3
...

[1]: /messages/by-id/TY3PR01MB9889593399165B9A04106741F5662@TY3PR01MB9889.jpnprd01.prod.outlook.com
[2]: /messages/by-id/7a970912-0b77-4942-84f7-2c9ca0bc05a5@eisentraut.org
[3]: /messages/by-id/6423dfeb-a729-45d3-b71e-7bf1b3adb0c9@enterprisedb.com
[4]: /messages/by-id/CAA4eK1+L_J4GYES6g19xqfpEVD3K2NPCAeu3tzrJfZgu_Fk9Tw@mail.gmail.com

Regards,
Vignesh

#223Peter Eisentraut
peter@eisentraut.org
In reply to: vignesh C (#222)
Re: speed up a logical replica setup

On 21.03.24 12:35, vignesh C wrote:

Here are a few suggestions:
1) I felt displaying the server log to the console is not a good idea,
I prefer this to be logged. There were similar comments from
Kuroda-san at [1], Peter at [2]. The number of lines will increase
based on the log level set. If you don't want to use pg_upgrade style,
how about exposing the log file option and logging it to the specified
log file.

Let's leave that for the next version. We need to wrap things up for
this release.

2) Currently for publication, replication-slot and subscription, we
will have to specify these options based on the number of databases.
Here if we have 100 databases we will have to specify these options
100 times, it might not be user friendly. How about something like
what Tomas had proposed at [3] and Amit proposed at [4]. It will be
better if the user has to just specify publication, replication slot
and subscription options only one time.

Same. Designing, implementing, discussing, and testing this cannot be
done in the time remaining.

+       /* Number of object names must match number of databases */
+       if (num_pubs > 0 && num_pubs != num_dbs)
+       {
+               pg_log_error("wrong number of publication names");
+               pg_log_error_hint("Number of publication names (%d)
must match number of database names (%d).",
+                                                 num_pubs, num_dbs);
+               exit(1);
+       }

3) Can we have an option similar to dry-run which will display the
configurations required in the primary and standby node something
like:
pg_createsubscriber -D data_N2/ -P "port=5431 user=postgres" -p 9999
-s /home/vignesh/postgres/inst/bin/ -U postgres -d db1 -d db2
--suggest-config
Suggested optimal configurations in the primary:
--------------------------------------
wallevel = logical
max_replication_slots = 3
max_wal_senders = 3
...
Suggested optimal configurations in the standby:
--------------------------------------
max_replication_slots = 3
max_wal_senders = 3
...

How would this be different from what --dry-run does now?

#224vignesh C
vignesh21@gmail.com
In reply to: Peter Eisentraut (#223)
Re: speed up a logical replica setup

On Thu, 21 Mar 2024 at 18:02, Peter Eisentraut <peter@eisentraut.org> wrote:

On 21.03.24 12:35, vignesh C wrote:

Here are a few suggestions:
1) I felt displaying the server log to the console is not a good idea,
I prefer this to be logged. There were similar comments from
Kuroda-san at [1], Peter at [2]. The number of lines will increase
based on the log level set. If you don't want to use pg_upgrade style,
how about exposing the log file option and logging it to the specified
log file.

Let's leave that for the next version. We need to wrap things up for
this release.

Ok, This can be done as an improvement.

2) Currently for publication, replication-slot and subscription, we
will have to specify these options based on the number of databases.
Here if we have 100 databases we will have to specify these options
100 times, it might not be user friendly. How about something like
what Tomas had proposed at [3] and Amit proposed at [4]. It will be
better if the user has to just specify publication, replication slot
and subscription options only one time.

Same. Designing, implementing, discussing, and testing this cannot be
done in the time remaining.

If we commit this we might not be able to change the way the option
behaves once the customers starts using it. How about removing these
options in the first version and adding it in the next version after
more discussion.

3) Can we have an option similar to dry-run which will display the
configurations required in the primary and standby node something
like:
pg_createsubscriber -D data_N2/ -P "port=5431 user=postgres" -p 9999
-s /home/vignesh/postgres/inst/bin/ -U postgres -d db1 -d db2
--suggest-config
Suggested optimal configurations in the primary:
--------------------------------------
wallevel = logical
max_replication_slots = 3
max_wal_senders = 3
...
Suggested optimal configurations in the standby:
--------------------------------------
max_replication_slots = 3
max_wal_senders = 3
...

How would this be different from what --dry-run does now?

Currently dry-run will do the check and might fail on identifying a
few failures like after checking subscriber configurations. Then the
user will have to correct the configuration and re-run then fix the
next set of failures. Whereas the suggest-config will display all the
optimal configuration for both the primary and the standby in a single
shot. This is not a must in the first version, it can be done as a
subsequent enhancement.

Regards,
Vignesh

#225Euler Taveira
euler@eulerto.com
In reply to: vignesh C (#224)
Re: speed up a logical replica setup

On Thu, Mar 21, 2024, at 10:33 AM, vignesh C wrote:

If we commit this we might not be able to change the way the option
behaves once the customers starts using it. How about removing these
options in the first version and adding it in the next version after
more discussion.

We don't need to redesign this one if we want to add a format string in a next
version. A long time ago, pg_dump started to accept pattern for tables without
breaking or deprecating the -t option. If you have 100 databases and you don't
want to specify the options or use a script to generate it for you, you also
have the option to let pg_createsubscriber generate the object names for you.
Per my experience, it will be a rare case.

Currently dry-run will do the check and might fail on identifying a
few failures like after checking subscriber configurations. Then the
user will have to correct the configuration and re-run then fix the
next set of failures. Whereas the suggest-config will display all the
optimal configuration for both the primary and the standby in a single
shot. This is not a must in the first version, it can be done as a
subsequent enhancement.

Do you meant publisher, right? Per order, check_subscriber is done before
check_publisher and it checks all settings on the subscriber before exiting. In
v30, I changed the way it provides the required settings. In a previous version,
it fails when it found a wrong setting; the current version, check all settings
from that server before providing a suitable error.

pg_createsubscriber: checking settings on publisher
pg_createsubscriber: primary has replication slot "physical_slot"
pg_createsubscriber: error: publisher requires wal_level >= logical
pg_createsubscriber: error: publisher requires 2 replication slots, but only 0 remain
pg_createsubscriber: hint: Consider increasing max_replication_slots to at least 3.
pg_createsubscriber: error: publisher requires 2 wal sender processes, but only 0 remain
pg_createsubscriber: hint: Consider increasing max_wal_senders to at least 3.

If you have such an error, you will fix them all and rerun using dry run mode
again to verify everything is ok. I don't have a strong preference about it. It
can be changed easily (unifying the check functions or providing a return for
each of the check functions).

--
Euler Taveira
EDB https://www.enterprisedb.com/

#226Euler Taveira
euler@eulerto.com
In reply to: Shlok Kyal (#221)
3 attachment(s)
Re: speed up a logical replica setup

On Thu, Mar 21, 2024, at 6:49 AM, Shlok Kyal wrote:

There is a compilation error while building postgres with the patch
due to a recent commit. I have attached a top-up patch v32-0003 to
resolve this compilation error.
I have not updated the version of the patch as I have not made any
change in v32-0001 and v32-0002 patch.

I'm attaching a new version (v33) to incorporate this fix (v32-0003) into the
main patch (v32-0001). This version also includes 2 new tests:

- refuse to run if the standby server is running
- refuse to run if the standby was promoted e.g. it is not in recovery

The first one exercises a recent change (standby should be stopped) and the
second one covers an important requirement.

Based on the discussion [1]/messages/by-id/CALDaNm1Dg5tDRmaabk+ZND4WF17NrNq52WZxCE+90-PGz5trQQ@mail.gmail.com about the check functions, Vignesh suggested that it
should check both server before exiting. v33-0003 implements it. I don't have a
strong preference; feel free to apply it.

[1]: /messages/by-id/CALDaNm1Dg5tDRmaabk+ZND4WF17NrNq52WZxCE+90-PGz5trQQ@mail.gmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v33-0001-pg_createsubscriber-creates-a-new-logical-replic.patch.gzapplication/gzip; name="=?UTF-8?Q?v33-0001-pg=5Fcreatesubscriber-creates-a-new-logical-replic.pa?= =?UTF-8?Q?tch.gz?="Download
����ev33-0001-pg_createsubscriber-creates-a-new-logical-replic.patch�=ks����3��rb�4�����k-o�)��ci��n�T3��/�������@��y��]�+�%h4���&��,��d0�c�O�S�tx~6<>���������wr����d8�OI�ny���l0������`���\��E�3v�>� s�����\����9��,���$��\�9�@�]v��Z����
�.�/��
��b�/���?�_����=�����tz�e@�b,�,����'�e1da2
<7dOC���&�[�������	�=���y���9s���|�Y�fS��Af01K���a�8�	��8��G��I�y\O�M�q���hz���}�����d�	$
"�^0	��|����
��q�^X��B��m5��#x^���n��d��R�G������,�$��&I���7+��u:w�Z��������;���
��9�v+(;��pd��qA�lW�I����5n(��_x\����H;�Lbt�D�����"�_�3��Qx0YL�0����@���+D*�w����G ��f��d@(�oOA�,�v
��~�����awW����%j��"Lr�/#�6��|�`+0�C:�������:Iy��D��G/O`$�q$��H��_-&�ys�32�v���Hvv+On�
{8�<���/q}@��,�8�{����"���E�00Jr��D�We3-m���JZ���Af-�(��� )l��{���#��P��4)����,S�-���X�Yuo�%2��+��$�Qh���������9l)b��������q:�O������(�g|�p�lO�����_C��2�E�
�w|p���Os*(l����*K��q�JH���}*�~o
�b'`�6������F����,�q|���������E�6�`a=�w0�M+:�����>�����i�X������sF�W��w�����D�T����3��������R+%>g�����h}�h��6���"��L&�q@���_���m����G�����Y�79������S�$j���:�����~����q����?��I�x�<[v�dv�HVS��������������9R*��������Ol	i�#\v.k��I0�d�2�9��3�,	��������}{*������6�/�������0�.�t�4�-��\8��i��s��m�����2YDK��(�? �o��3���d����~�"a]����;`��.:�����������>9x������)08������XDt0 /s���v�4u�i
�N!����x,�F��Dn6�l�l��/j�K�0��7W����A��,�C�v���Q�:�Fn���q]G}��^,
������"8�����0�!��������~�G�E�&�_��B��c�<�;��alk�F�4���E!'�q��@�z����x����(��r(�H����F�\���h?/�T��<�dS5{�%`&�Yx B0&���4�A:���Ek�Fr�K�����B+�8:tl�;�*�7{/��[����]��6�?��6����������e���X{�}`9A�m��'��V����
ZE���$i,�y�y������Rc��5j/�HHZ�U��4���]�`���T&Dk�>X��!�lS�`�����]0r��4�8��Z���$���U�@@]���r�I{��B��L1E�)�M -�-�CY��hB�]�{��y���Y���fR!!--��	�;�����<�fY��dk;]Y[��x,
���I�o@+��\�"\�%%�2	���K����`4��R�s�_��.D[2B'6m�>��'X�X@���d$�]�4���g= �	N�3�ka<Y�K	Gn���"&@2$I��'RAi�n>�Q����|��B��T�t�+�� {��W��q�[�Z���7��@�N����+�e����c�0����e�����8��C���A�	V�!N��*;����B6F�+�����`7�\��$	��h/!�������Rp,.��l��f3����C���J�����$g������K���T�p+�2py�5��-����
��2�wA�T����z���~*�<��������<��:�{�*GzZKl��c�{3��
��
�(��m��5vm����\���)#��l�p��8���e)���
��a���@�kF�bO���bK���dsb�/��u����
'BK%�n J H�kK^��s�#]%�i���	���Y%�,���"@��X���I#��TBV` �����,f�����4�E\c��.{<8:<�H�!	|V�������������-����ykz��]��f����b���bS-���9��V� ���������;�����X�sl������Nie��_70#a�����G.�������Y�6�|����^�e�F��p������I����`�>�u0\O���s����W�>Q�Vt��2t���<����;t��+�X\��D�4���`!���f���g �������4�Ul�p�������3n�V�N�:>�6hI�3�|��4���e�%>lh�T
���Nv� �1
+�(e�Tl0�7�g$�8-r���_.$�&V��S��hW��hc���rNE�����x�%��|���P$9$���~��c>����	!D
KL(dIa:�2V��`�o�zGa���*=�1_U�~��F�=�I��L2������2{�����0�X��dF])�^V�+��P�d&���&�$m�~���0yl�4�(!�Y������<0��i�/��-���|`���1H;U�rU7x����������[p���-�����^�7E���(�<�N,������B����n�+a�T�`�-��+�h�c�.K����QD,�����4[�p�$�%(\�4�H*����SV��>��z�2��S��?�k)���&�����!�i��Jb��lBm���]�+�@���
f{�����J+���d7��H��=�l[b�`?���w�!v{���^��TY-�vo�Q:��/�w�]yo����-�������o��6�_N�g�K�y�����Tng�#C*9���AF�%��1Z�bS����8P'������v��}�-����r����&y�q��r����@�z�Z=��dW�h~D2Q.p��Z7�5�����jW�>W].�����7.O�+����^��������*gD��`%��y�mJ�8
���� �rj����$zmY�Y�4�T������`��i<@Z�Kt������
@��Tv��n�Jp������P$���i��%Y0
b�������hR����5�^�Ww� ]�#��+�Z�@�]	��#���X��,�UR�m��s"�����
���kx_��'���j?]�s�*-m�c�}Y���FV�U#�W�5�6]����	�h!��}.�`����R�u��B���)1u�*�j������[�he�dLL�.d
�1��q�2��~%i��������O���P�f���J��{�\��G1*?Zt�g���Al�&
���J���|�[��g��o!�,
��^�!m�[�� �X��������b���d+�"Q1�f�N�(�#�T#������[�� $�0�����;�y���x���o.)��[�������V����L��	O�~J�0"ycK��`����,�W�~����\W%��4���F���"r��l��+g���P���������~�FZU��MW��\���x��j�����|�+%����&p?
c8�&m��nJAy�4��'	�e`$�
��E������/���-
���DjG�9�J�>^���nh�����h?C���^N\���*���|}}��9Q���J�BEK��+{c$ ����D�L��Qwl�:2�ZE.���\��l#�:��y�|
sC���%�U�!)]���T��\�V��$�)
��Z�0�K�m-�$��n��R'���k�t�>f���W�:W�]+C��@�T5�~�}"�Z�
�d.y�����*�" -�>���$ON�r�]�|�/`9����F�$O���S���F�d��`6}�����
]����;�5�e����-d�MHW�	��Z�	�z�l~-Q6v�F'����h�^��G�EmR	��Pb��r������o�@B�l���8P��
6NK�
� /��]����:%��x�uW_��^������3W�T5_�����XC~��*�W���D��������&��<"a>B���D6��A�7`��Q���5��@���q�,�7�FH��k(x�B�hI���B<��M���g��n�"5���6 T>8��GQ����U�	@�������v�|�o0�Y�	H����Y�C��F/)��}�C"���\%T�v�O�F����Q���<��w�}+��4�"���[�cCD��M����L�������p�G~_�L�j�zLg��T\���6t������Jt�3�����:��&�p7�/��>��[~�����O6�rDJ�QC+i>*_]�)�����]����������P�^����IC���(�<e�7"����g�:���x�8�2��y�G �������&��t!�BA��I`�1��q�L/Y8'�N��\/E*-qU@����E
�@�9�*O���V��)&"��$}�Y7�d�x��UY����kStO�i��H�;��6t�T6%J��h-�@K]���Q�d�AVs�B�R*Em�	�]�(w�	����E:
��x\�M���8�K�"��#�T_���e������\�W<��
J�Wu��?�X\#Q������fB9�#y����B>�
��3���B.�%,3w�5�A�W^�g�H*�
n��nJJ�&�6_j��1�S����&lP�U���&\s ���
��;$��.����osvb�8�*���&\D��34�������T�(6���I=�:�������[�_��"J:7Et2�sB{�F���7�������!7����\�-�I��o�B�^�!�
B�E����xbL��9�m����iuTL��:�<_��cg�c>I(B�G���N��u�:K�j?n����s�~��9�������Z��������/��23R�����qqR�lAK �c��I��oZ���	��0i��	�(i�O`�)�w2`��]����VF.��k�>��P6�%c����J�c+��[�@���."<��Z��5Vh�\��\F��Es���[����R���w^`���8)[�N�dk�~((Kr��V~���2+��8yD����h��/����_^��������B^8����������}����: �f����{�C���e��v�p����'��\��W^��(n�&�s�2���g�V��|�w��r[C�Pi�����$13  �q�%G��Fi~����?���qZ�m!s�Y�Y�zc��T|��8���R�W��s|6���c���l��x�9����Fv 3�r�|��W;A�e[u'�(�]_�
��RDt�WC%rl�\�W\gi_��P�z�n�t������ONz���������d�E�6���+�t���]�
�����?�S7�f��x!w�"�N?�"�("a��kK�#*��yW�������Z ���q�����C9NGgc���z�#���'�`X%�JP��+�!a��#���Z��o��S������0���3�m����h1O�@��z��������l�eT)!M���x	5�s���~��/�����b�u��c�GI��^R����^D����,�*�c5���Oh��@X�
��������O��c����������(�<����|���
�y����_`�����������?��w��S'a/^����`�����h�o&�~|�������������7Ax�S%�E�WB�+x��E�����@^P]������o�.�!�Dv���~��L�����wW77��?���W?�Ue�d/_�^����~�a��.((���7��$.�����R�m�_.���`)�����4�W����T�<9������!2���2�����9�
h�|�"�-�������}3���a��OGi
R�a-4�
������5���6��
�����i1~k��
�P�� ���{�7��K���
���5F9��)?����A��
����Z�
k�o�����-����1��+�.�I��������������:���'�O{�����%�3�eIF�eo��cgd�v10��/��'���F�2f2������S��p2sw��8��������>)�Fsij�AV�%���b���������yR�qt�W���A��&�&z�
�YkUS��l+�2�tj�����e�;��
����Q��~���Z76yup��.�nI��iE�H:�x��j��Z�P�����Db���%�����=y��g�j5���vG�������zC�I�p'��M�����!$��Vv������@������U�i��P�������V�����)�E�����6���b�����l�f[����>��?�??/B'��J�u���:t�~����O2%�|��w�����Vw��`fn�[;�=��������g)�{��\�3�N���������7o��R���$����w�7C����3{l�o)9S
Q���V?��1$��,�	���W����/�t��V�_�����/�Q���">P�b�q���Q���?H��"��SZvw4���wgM�
���O����z0:W�� Nj4ub�����{P���n���������1���
��_sV������U����zR7��~�������MK	��) �;��3-�������X-o���K?j�R�\� w:�&���j���e����xR�|g&}��H����Wp[]d4�
mC~��
�SPzM�K9)�2�G�^��0[���z{x����LO�O�V*���l���x�j��n���7N�����bW�W��B��1������V�H�(�i;���P�hn��
{������nw�2�*Y��
�I�G�������Q�/l��@�L��Q����H��H<K]��1�eB�t~�g^G��{������CP$k=�cr���O9������m�5]�TDe��h� 3FYw |���N>����������Aov�����e��������-����q��(�i6>PhC(x��)��~O�!b�����=��)�����DN������S`���j��Q&�
��m��&nc}l]�9n�����F����N/Ku�x'a��EO1�g�r�~����n�>ll'H9�'UM?��|�?s�N!`�&~���!>�������?6���Q(��Sd6aL+"�U:�W�d���q���������(	R|��s`qm�`t�Fm������=����^D����N�k���W������������X���5�H�T����=T��,�h�b,�6�������1UD���	��1����^t>�~�wF��h�S&�x*w�u9������s�5wv!r(��m|I�z�7������so�1 ,N�X������%g@a8cfo����
�z��`�hsL����3f�$q>GF���~j�EW�#9�����!bF������s��rg���
}�������1�N_�B������{
N�JD�t�s'$K�F������J�m�����7�����*�<]����
�o9�D���E��a��L2m��g��������G����b�������d�4U�^�"�����up������:\�B�w�����4#�NI>S�<�������C�����A�yL$z�2]�����a@DR��b��&��3�
����V:���������8E@h�����	����c�JJ1&�?#GH����L���5C��V�mHY���4����T+	Q��y1a�@y����4�2�u�;E:O��f�tP��{r�>{�j�k�������pk��?[�gG����%�$IW&���W7���Q����P��!�2a��D�W,��-���x����`�W�c���S�	�XuJ�@q[���@�u������y�k�����@���
N~Mz��� 	9����C4s3�3h��yg^������B0�=s�!L�r��r�X�Po�P��Z��W&��1�HN�C���e�����)w�,i��M�d�i\����5'C��@�PW_�0���5���M�����8\��WDM �6��������]����0�P
VL��[/~�Rj=���e�JW�+3�l�
a�H��r�{�/�[z���V	}�9IJ<��=�[����$�W�:�R$�>��ys	����)��i����@5Z��W���_?$�������]�
�'����u_������U}D�B��V3��� w�\_2+;[�B�� ZH{����_���x�9S��N��)���<T��F
+|�
�UR����\�g�3@��f�{����s(W�#�H0�yu�jx�0��&��&@�A����g:��5:���v�hc����Qb�/�G�/�K3�\�IL�����:�3u3{G�H����Gf`���=���TF�)���kl_*��EQ�lW���o�D�e���pK��b`��]V�
�k@�����(���!j�8�<nWLPu�6���=���FY�~cH�|QKkj��g[���2��qg��/���[vz�D
���	X�?���#�2(�Y(�Bj��/����o$��� L!BM$������^���wp��-8�u"�+d��#q�T�3�SWH�����R�� o+��(�1�k]���/�
�t
�Gy�N�7����^�w����g�k3�M���8�0�s�5��gT��G�*`�������v�������z�`��Y4�Z��>(],��2���zV���JV���^���L)�H��J����p�u�M�Rb(�8o�le�������r8AAX��X��\�*�cF��'>ZL�>�h�����)\A����c=�����k�f�ex�	����%���/����v���z?}����~�P(��y��]��n?�>������9c9�����#�P���e8x���G���I���!_�����t����73:��uP�A��A��� $�f7��*jNeS�(��X�e�mV(������C]��/|Tu��\�tz�U��� �U��z��tu!�>e��L ���18
J��qV��pp�<�$�Fr@��?(t��<�&U���10���A�@:|1�t�X`t0�����H��v7k;�����i�r
��-����;��UC
�X��}���mF�g�%"�L��K�O8K`K�Y���*��t&,����!�8Y-�r/+�J%*�j+��bx;���4�50f��	�T��|=<�8�g�?�/~�noF����'O���������BY��7�o��!e��3�9}����V#��r$�YVhd��������������f��L?y�l�\����U8QS8�H�I�'4��Z?�C�lSl=��5�^�:Qg����dY��ant�����W�F[����+����x�!$��>J��1�J�(r�a�����iEr�/L"'��+&dW�AJ��r�$=�n����lQ���S�>��k�D_�q�G����0�DB2N���31�H��P�����7U
]��B=�M���Sh#hR��������	�oGM�)}�r%8���+���0#H�S����uy�o7�����#�����I��h\8�G�)���zd���lf����^�S-�
�c��K����a�(gv������zP3i�U������O��G�'f:�k�"���i�?y}�:�3�!$P��K�>�����������@��"���0�e\��������f��,��Q�o�!M���J���;���orfy�����~������Is��M�o�ZM��k�cS\\���j���^�t�}�M�J�!���Y%���B|<�:�tr0����&'��?�
�{8#����	+s7X�1����$�CKBt`� �[��4�u��k7�GN*
��x�
��D2(��:s�����a]w~%��#��3j���M[��M���1��a�ly��@4(E~$Uihx[�������P��}�A_�>|N+8(,]!��F��b{��F
��c��EM��%�����`m:��b`�#�T�G��Gg���l�<%����)��^�N��dv�sx�����4���!�Q�����2r��zs4[��v�	8���haAoP��I�t\
}l��>����L�'s����mM-Q�f��? ���W�b�#�3pN7�y,!W���a�����y��"L�J	��S}_��{���\�h��$
�W������{p9gG�$6�Zk�1qwd���$=wSDZm��W81,a��������r����#�����������������%(%�{����u�J|�����������kz�;OH�7Z����
�p��.���l���^c���p�%Y��3�tn���
�� �&���|��L8�:$�~&z��W���K{Y�7��[+�w��4��������Q���@{���/��������3fv7'�0���9u
5�bu����L��b�%O��<�����������J>Bf/f#�3]	,��2td��ab�5
�8���1\u�G<s<�����/\�6���f�4����q�c�q����KnW��|�����R�E����l��5�^[,d�qm�\b���^}G�>�&��'�.���
#�����v�J�3-F�J���(?�}�	B�2�E��/bQp��m��J��J�EV���,���p��
�m�Y�����\s�`��� R�~��yH%Ot�3��[�r��<�d��B(J6U�{�s@dh���Va��I���Y��~p��'�#9�;�1�����&��Lmn�%�T����\��?��
w�������g�����b���B^�;����/��
��i��9|y�]^�[�Eo�Y��rZ����$#�j����u����6D2�=�W�j���9�1����6m��������vz���P�k�~ha�x'rZ�n�(�rO`�{m	�`\������F������s�S�&a��<��/To���r����
u(&/I�L��X��[������C��#�36u*��j�Z�5����U�����XS�e/���X!��F�������F�5q�������%�8eP4�������=<^*�{uz����d���p��Z���w=���/u�����>���9�x���t��:�K��Q/��n��C���_�����dk�)�8�z1)F�o����`�j��H6��\��
�f=�_!^�K�h0�����.�sm:d��W�?�	N��.��mI��S����!PA�rT�4n���L�,��=�%��!������c~�:��]Z��a�=���.*�)�Dw�MG�>��@@@c�}�mJm�������~V	�,u&9����&�����i�^4_$gYt�&������Q���������<��K�X%L4}���,�Pi��T&�_X�p��*��z�:D`�\*��-���\�4{%oq����3@p����#����\�R2��A�c���3�]w��Gs���N�����+	�|��=y�A�H4SbM|G��_�F�u�t���_�&�#K��_g��^���M��a���-S�Q����_/>�y��'?��<�.�����T����O�p�p��|�l~z������c��)V��yN���ypR]Bl0��,BlM)�	��6d5��I����K�dy��t�
���I����G���x�1���hN��Y���,��q��v"o�.����8���J0��2����j���)�qh�%��e"�Kj�=�^�a
���4�5�I@�b[�r�:���������(5����gj��K*���Q5�vX��Nc�%+��?�,j����
-E�q�:��K<���@X����x���3�9�<�$����F��
��fC+���?����F[?I��#6,������rr�}9��Ht����������^�ln$d����I�EQ�O�"��7��Mp���4	���ol��&WOa�D�5�TG���b���2�	@���Q�L�H$ P&d�{�]w�a���O���50O7��������f���
����t���7��q���������QuE������<����
�"���	W3�?'����Piy0;D�;R�� (+���/�4��aP[X��g��RGS�����by���|(�^��|
y$6�,���_������>w��G�KvZ���&����#�c��^��K����|AZ�H.�{0$����h�o�/������ [gHA;���{3��=*���9|�kE���o���������wO�[g����e{��cUy��E�	j����Hl����c�){��G\+]#��0���B�� ��B�-��la#���L���JVA��1t������M�V0�����3�)�x�X�,���������Z� �����9D�`t!K�I���h��"��)�L]�Y$�W7����Sod��r7�B�+����	�VV���O���lBW ��=�����~_uY�t���z�I��=�
���&�7/�Xk�����}�@-���d����?C�����`������mq�����8h��Q�����}H��c%K��`m�_��D���(w�F�x	�V�I����{5
��4w�1���K*�e�R��c�Z����"���b��|��#���*.9�+��������������xm������>�
�$`�����8y��G���p����%�w;��K6f�Ppt
)��E�#[���b�g0���������������+�QYZ�{�B
�E��U��e�0����Pe�y�	T[���3?F���E�Z��~�(V��[�i�q�u���W���R�4�'Ru�L��P8h$&����y���s#M��,��Q������o1p�����!<�	��>�������� i��.����l�R�n <�,�2w�S1�X5�]�7l�b���js�]@�;=Kg
@��&�^�aQ{\��]hdZD�Y�����B��K,�]b���aK����� ��B�a>h�C�n��`�x�
��.�A���{
5�4>)�����&�r�����	|���GV�*e���\u��~����/�����}�g�b�/��i9+W���c	5[A��q��n�����w��hz(�������S����bs������K���"O�~��~</J��!GE���Y��y��g6�x:��,�~1jr��U����}�YE�V
���u
���@Q;J���c�r�rNA��/m��%�Rkb�V�|��DE�$*��5KZG{���\}�W+b�p5�b����P���y�I�N��%I)���+X��e���:��.BXe��s�CL{!G�R��q
�V��W.V���N�}|�0���<�D�C���Lu����d��i�>�/g����vag\s��6�l���O�Q����ht����6��<���x3qbpp?)���
B�u\
Ug������%mp��&�����x���kr�����&��28cBKo���l�v�
�O3<�Y�y��SV�f/tnm�z��r����a����I��b�|�����z����i��.O���r����n!������P����is����v�qA	�:�����$k��G��p������ �t�:i7i}L��	�m��|������O���1'�`Os����2���8�p��m�R�de��QZu�`��m^\#�����~/�B5W���*��09Z��������Z^�6����M�U��R��j�����U��K��>�y����B*�=��*9O34K#$AGI��P�s6�*TD%�2���uN������������_�����5�����kr:C����f�iM�����i+�o��r��<f����[YL����IZ
Fg�"
�u8>q��7��eTa0z�~Gg���Z��kq1���y���
-�|YAf��^�;nG��N��b�2;3�Q{�=bJb6���$�������7������{l���_lH����_��8�J|/Q��t!TP�v�*	��
�x&8t���K��U����{�:�����o*�{��w,���apK+7��&�-|����;�<�A\����1�����)�r�wMV>Tx�bt��Il�@�����n8�������v������r4�����H���%;�>�����
4�-aA�/y��M��� ���'���X4�R��\mP��\��	=��v�VjOs�����cX�F�e47j�l����"xh���� �_���+	�o}�M�?��
3n�3������<���:"v����PWc���Y��D�"v�1
���f�'-��J�m���(LC|����X�Gy!:��y�t���������44�������?���+pr����OX�����|/	��$w&%��D�9R�������X�e�"��D=��FfEPQ6�O��vE�O�0Z���:���X���5E��0�� ����r��V��K�|M,�����J��N�`$���;3�}�j-	8�m���=���F6@����������\�$bGA���G�7����������O�������v��^�4�f����XHl�O���V���W6�����"G������
ef����
�2���e��X��-�,�SpZwz6��1D+�����
l-p[0�L}���FSaSnC�e���1�:�R�d�\��� �-��fp1/q���r��B�W�[x�/���2Vf����������oW�X����bdA�%����s�f8�9����uc�G���F��L����	�+q'�t�`e��QQ����d�S�I�5{!����6��)h�<����BY���dt�"2����C���G<��T����Z�b��k�o��5~�K�_Y��e���h��L`a��6���=�F��A�M�g��o��@e��`��$VsN^�FB>�����'���ee|I����Q�����3��*���wv��k���lUzz��cy����a�"�%>H�#V	7���������3���o5��f���G�>��"^�6��X,u�E����z����no������{���w����"K�9��]��u����'����n��wHrT<2�N6\�Xi��b�o\�F
R�g����/�k�^@Z�8��&(����������=��^z\bF����_&
���Y��m!i�F��	�>x}�:t�J[A���	�`��t�SPaHkA���O���xw��F�gg��Sz�bw�J;��]]��R_�RQ"����BB�Lu���u�W�'�;]�_$���}�;��[�+�zr�%4�]��5gu4(�s�_g&�>�����a���Z��VH�|tAk���)�Z������dcD2���>�7��ww�,m�����K�TL���+Dt��2�-o��xE�>l�������R���f9��
'_��p&�4Gj��8�4[���[����0����m2@�g���&dN�������`F�����$��&'��fr�Q�xF�����foDU��H^�nzK�
���S���d��<$���w�$([�����b2�\u����;��Yi3��`��j7����j�~���\�o�:��h�7�������/�(1�@]�:�gw�jE��U���T+U�|����������*��-�����<_� �C�*`c�`�+�y��
�G�obH�&=��a������nn~8.�	&�	Y�b���)/J�byT��j�I�C���|0�G��dI��Z�lf�����q�F�F+�]n���X�b"�����l�2���d��Y�/$���zYN��c���s>�&(u)m-[�
�J^��Q+ru�����KB�T5H!��3���[-Ql54��1s�8�����V[�!i����P��:=;8zm��NMt�b������)L2��
G�B;ER��>���KQ�7)TH[gg�oN��+[���\�����|O���)�8�<{&�<�s�<��p?�=�}�
�n�����Q���g �/4P��u,�^����e�d��J��KpC�g]N�[{���TT��Z�Q�p~���L�EY%L�
�!���7�+|�6���*1��
��*:.��-�&�~��e3E������������V����D0�WA�E��/s�^�^���7��N������Vt��bm����p��������v����q6��d��a�D7�O�8xB���xN<HNb�)+:�{[�-	�E�F�^n"��gB����~A%#�r�0U��V��Zp���6{���K������,'�����@�|�~��k��G����.���m{7=��bWo(-t�y�4��%�*���x��t��H�
���������&��������tp�[������d�l$��)E��i�D��O;���?�B9���1�'��G�(I��C�\���\gT��
3��~����������qu���o*Z�w;���dY�VzVrS\� ���I�����`�-mU��6��W�s@q�hf-������ aWl
����"�^/q��T�_(���,@�K�2~.s�-����\Q����H�Oz ���3����H
Lm:��=YK�,��1O��<�(���;=tC'�I����UM���I��S�^�F����T�+����I�9�(�4irE�L�ZA���� Y�H�4��V�+����<?�T;���C��%��� ���������r����r�&'o_�� �+T|u|�����Ku�e������$����7oZG{KY�cO?g��8-�����]���5������%_������������j��Y����I,�3<0���c}�������G@��/���������8������ �������>�*���^��ZL�`j�l��l��wvZ7Y����b�s��R��R���W��1X�|T\aa����b�'6�i/���n;E���T����V(��M�+���AO5�[��q���AKj>�E�f6�+���[����Z�4:
�A
��>$����f�F~�IrP�|#���ThB�"�����up�	�"�~Fp�O������;�^X�SC����(�m�������5Q��b:d���rz���ObP!+s(H�&�D��:��i�7�m���j��V�d?T��;cJ�R$
������m�%.;�Z�+�@���y������+����x������V,���wxP����,�������>.�o�6[��#<����D7?/^i�������/����h���j����	��F�M��P{�G��������2*��l^��z+ vCQ�h?[����o���B�(��v9RD���b�,"��m�CD��u=����i��|u���l��}5�jV����6h@����hx���rx�ca���g*�b��	������+�'-w"+mPeI�n�i\��*
����E���&p����e��?�;�0�8������uy�Vz����A���nA�o�^R��A*��9Po�i��Ge��z�z�j9�P������C�q.���R���m�;W�lI�xX����~�y`^��'�;�����EUg���tkW��9~I�Yi<H�v�J}�I���8���U^����� �����z�h�������yZ����fsy��TV������hZA��,-=��h��y�kC0G df|����H!c9�l�����(�\�_�x����)���Gp��M��
�����..O|�5��b������GX�^��=-�g{p���<%�oQ�u��	0FU�A���dt3�;����B�S3|��<GQ�,����!,�/���=n��+�'������|�~�,fD���e�,�z�w�&���9$�)\ ��su&���&��_>���������pz�Z��?$����V����
�|M��R��^�s���/����{��:��:~�z8
�
M;+�6!�c&����NY�$g���N�`]��a�3����v�j����nEli`�T��z�5=�`�#<���}�� V���O��U6�4��4�P�&�2����s��j�mw��M����������Q�r����y^��L�g������o����������Hv/�DM�*:�{d�d�����D�m�-�O��W@_������?��G^���|z�����hW�����oZ�O^�����A�KH)�@
�����g ~�VpPOyzx�:}�:=_El��;~:!�����/1)|��.t�k�(r�:=��w��up-�]���hc�B�4���sK��4�x'���o���J�B<��������
a����������9v<-����{9��>��rXO*�U?����c^2G[�5lRw��\�[��B!����r�~jN���mL���C���{��Zo�R���'��g��&�^o�z��x�Z�=d�B����
��t��u��LQB&;5U\�L�P�h���&#.��5�u�U�{e#�'�R��~a� n`��"	-2���M������)�@�.���_*0#��z������<�uh�����	���ZU_Q���;��KT��8�*'��Ij_��j]��j�S�Aj���+t�����;';�N��v���K#�F�:r�M{���~���������D����	4"!����j������j��m�V"@���	���<�kk��g��q�7�����sB(�#����Etm��������.`���!�b��g`W5�C�0��������G��8�"Tia@�3I��#�k�P�*B�0!�����7N�����Qq~���������ZY�-1��Xt�K\X8����������=�i��u���d�
���{�J	�5��=���p��6�����k��rk�{aMDQ���p�-o�����k���!�m����v@�@�fQN�lr��I�g�|"��d.�������:�gkx�X�a�5�a�G�>
{���(+�3�j4
k�����@Kj�I��q�O�RG����P�C�u�.���9���N�V�b/I����L��<Ab�I>������3��(�6�/��B����k$��NW4�M�a;���$�voz��J�^!��XS�o$�E�d��h9�T�Q�{dN�o3�}5
�Y�����'�I�D��YO^N���������2����m�i?Y��Y��^M�8/�m�	�9;�;�#r�0�3�N�mdL����J,��Yq+��50n*�{�3��Q�6�oH���s�(�A�:V�!���$�7x���U����my���_3&���0�F��c��4��9r��9���-�V2	IP��qT�� ����,����J�=���b��8P(��_���������3q8���g��
3NS.���b$�q�knC2��`2�����?6�l��;CR�����?�U#!/)��p��g5�A ���[�����/�jV#n�0y��������5��W��s=G&W����!�W��Qp�W�&�����k�D�����|N1��	��k������NI�Q�>{��aP���p+yP/:���i�����^�\v�����%�n\z�k�M����F����
�9���M�<
��#R�&plM�HA�E"E�"�S%���81�����.�M7�/���^�}H�BE49���S�,_L�����M�����mA��c2��>D9��BW����b��v�H��"!w�y�M�=j����R"ly��[N������H����o�ot�w_z�Bp�]�9���@�L\�y&0KT��y�f�f����N��r���u�9>6�2�YYp!P��z������������9�
�$���*��$g�L���7R�tS��8 *M']��P�����0�xr�G�K�5d�@6N
�N]�G��x�����Tb�(W]�F/5�-��p~6-@�tK:�]2�2�����x�\����� �
��n���"��*b��9�l��D�bBcy2\S��_Ls��T��,T5���L��y�c�Rx2#�����&���*��@�T�-a��h����^6��R� h���J6)���GJ�*O#!�h�qK.�4�N��=%���A�m�L���N�:Xn}5�b� ��
+05�u��sl2,���u�j�T��*U^�^>H�� {����&�b�P�/k48G��S����#E�Io��gP>cX�c0��'��FL������h�FJo�/DQ3�P���$w�/^��LN����k��_���L�C�zJ3x���9��J��e.K����' �Su��E	��z����8�Y\�OR��gw�^zID2�@=�P�0��6�	8�� )&T�����p�:�g����&p��ZV����M���|5���.9D�&;8����(U��-Ee��BUr�%X���y���E��2|�37������u��v'�8*�8J�E�4]>���U����B�����������\������i1Z���D����8Er&�qw4z<�����R�B��dGJ�b���k�����@����^�>�Z<C�T��g+�:�������I���`��\�kr���w`8���	��%&���TjH�A��}����~D4Ia������������E���eu�7�C[��g�prJ���9\�z�f�]dqB�%���s?_����h�}��&�����
*�K����|��P������w��N������4�h����:���8 >5�������U�2J6�?���>�d�^�.���6��d��}�N!����������fc3Y�j<��i��?��}��*�i��@�[O�7��6������\�N{�h9�F!y��1���gY^p�����ur[���mr�zF�by��u�:L��H�����W;��lgg�����o	��+��F�x����NA���~�U#[�i�uC��\�-���B��U����v}���	���2vv@�PM��;LzE`uu�Y�S��~����a��L�8M���s�H��&9���6�2�-d�*���A�g��j��$��w������>���Ff�%��?rz���~��*��,'����gX�xj���h������`;��1��}�^�\��?���o��d7��yg��[�\�����G���������u�Y�r��*#�O0
\gd�'~N������b����r��
(��<�	E��`%������^a�����h�&�w�����t�}N'���PU�����hL��X�a|��t�Q�J
����jO�YO��+,��k�~D!7�� 	�	���2V����q��Vl����~!v��7��%�U��o5���+82"�_�:��1S;��2)7���9CU-�n-;�����w.����
��W��t������������O�Ux�y[�\w����T�&�M���VR���|*���=+G���������:��:|���U�	Q��Z�����������UAZ,�w(MVg�V�!Sp�s�=��h��9���PQ �2��+4����<T����-�l�/������0995e�<RH���E�-y��i�����
���o?�T������Y]��@���
���������O�C�=B=���b�,������v��^��f4��8��v��1`���?����dW^�ofj�O-��c0��@��n^w/�x
�J����h��~���/��NQl�������v=6�}���������~~��/����v����I^=$�Y�����]��b�m���6?�OB����E����{�����u%�]A���$:�$z{�V@HaO�\�������.(QB?�yk'I������/N9�KP�|i��.���{�thC������:*o�]?I��-Xn�[�(����]�X*
�b����m��Xl��%/L.=^c�a����!�(��k��P�u����)�~��-�}r��6�?b�����K�����Y&;K~&7���>������]�T�59�rrn�����X����@���S���F����Y��a�G��$P�e��<s�H��Y�;r�,=97���Y���3z.z��%rO���\"^���&������8�8�����b�������>���3n��*�I�Qd�`��uF����O���Z�vV-W;��c����-���������faqP�OL-Tc��(M�y��&X��F�mzei�tXUT�)�g��w��ub�����J�[�kg��C���B|l�G��V."5?�0����%������Oi�V_�lD�d�\Gk�Z��b��l�Evz$�s���V��=B5�Euk��&�x�Hr2��.���Vg:E��
����f��1�������_W�" d��m;��hW\�'Q�.�O�0�
�>����s�,Q�C\Wh�U��c�g������s�V}�p>;n���E�/���Ha���-�6�:��V�����b���s����\��rNi~;�Z����97�;o}p�Y�M�JV!��m�1������9UF�Db��rA�+X]3�E� j�d�O���
���`;���j��"
���s�6s�+����V;p�Dky�vl�=����RaJ6��H0
RjQ�A%/��E��f-_=7##X�15:���N��<Ae��3��n�)� q>��(�p~X]����)@i�l&���O6��WW�p+�4�
v33-0002-Remove-How-it-Works-section.patch.gzapplication/gzip; name="=?UTF-8?Q?v33-0002-Remove-How-it-Works-section.patch.gz?="Download
v33-0003-Check-both-servers-before-exiting.patch.gzapplication/gzip; name="=?UTF-8?Q?v33-0003-Check-both-servers-before-exiting.patch.gz?="Download
#227Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#225)
Re: speed up a logical replica setup

On Thu, Mar 21, 2024 at 8:00 PM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Mar 21, 2024, at 10:33 AM, vignesh C wrote:

If we commit this we might not be able to change the way the option
behaves once the customers starts using it. How about removing these
options in the first version and adding it in the next version after
more discussion.

We don't need to redesign this one if we want to add a format string in a next
version. A long time ago, pg_dump started to accept pattern for tables without
breaking or deprecating the -t option. If you have 100 databases and you don't
want to specify the options or use a script to generate it for you, you also
have the option to let pg_createsubscriber generate the object names for you.
Per my experience, it will be a rare case.

But, why go with some UI in the first place which we don't think is a
good one, or at least don't have a broader agreement that it is a good
one? The problem with self-generated names for users could be that
they won't be able to make much out of it. If one has to always use
those internally then probably that would be acceptable. I would
prefer what Tomas proposed a few emails ago as compared to this one as
that has fewer options to be provided by users but still, they can
later identify objects. But surely, we should discuss if you or others
have better alternatives.

The user choosing to convert a physical standby to a subscriber would
in some cases be interested in converting it for all the databases
(say for the case of upgrade [1]This tool can be used in an upgrade where the user first converts physical standby to subscriber to get incremental changes and then performs an online upgrade.) and giving options for each database
would be cumbersome for her.

Currently dry-run will do the check and might fail on identifying a
few failures like after checking subscriber configurations. Then the
user will have to correct the configuration and re-run then fix the
next set of failures. Whereas the suggest-config will display all the
optimal configuration for both the primary and the standby in a single
shot. This is not a must in the first version, it can be done as a
subsequent enhancement.

Do you meant publisher, right? Per order, check_subscriber is done before
check_publisher and it checks all settings on the subscriber before exiting. In
v30, I changed the way it provides the required settings. In a previous version,
it fails when it found a wrong setting; the current version, check all settings
from that server before providing a suitable error.

pg_createsubscriber: checking settings on publisher
pg_createsubscriber: primary has replication slot "physical_slot"
pg_createsubscriber: error: publisher requires wal_level >= logical
pg_createsubscriber: error: publisher requires 2 replication slots, but only 0 remain
pg_createsubscriber: hint: Consider increasing max_replication_slots to at least 3.
pg_createsubscriber: error: publisher requires 2 wal sender processes, but only 0 remain
pg_createsubscriber: hint: Consider increasing max_wal_senders to at least 3.

If you have such an error, you will fix them all and rerun using dry run mode
again to verify everything is ok. I don't have a strong preference about it. It
can be changed easily (unifying the check functions or providing a return for
each of the check functions).

We can unify the checks but not sure if it is worth it. I am fine
either way. It would have been better if we provided a way for a user
to know the tool's requirement rather than letting him know via errors
but I think it should be okay to extend it later as well.

[1]: This tool can be used in an upgrade where the user first converts physical standby to subscriber to get incremental changes and then performs an online upgrade.
converts physical standby to subscriber to get incremental changes and
then performs an online upgrade.

--
With Regards,
Amit Kapila.

#228Fabrízio de Royes Mello
fabriziomello@gmail.com
In reply to: Amit Kapila (#227)
Re: speed up a logical replica setup

On Fri, Mar 22, 2024 at 12:54 AM Amit Kapila <amit.kapila16@gmail.com>
wrote:

The user choosing to convert a physical standby to a subscriber would
in some cases be interested in converting it for all the databases
(say for the case of upgrade [1]) and giving options for each database
would be cumbersome for her.

...

[1] - This tool can be used in an upgrade where the user first
converts physical standby to subscriber to get incremental changes and
then performs an online upgrade.

The first use case me and Euler discussed some time ago to upstream this
tool was exactly what Amit described so IMHO we should make it easier for
users to subscribe to all existing user databases.

Regards,

--
Fabrízio de Royes Mello

#229Amit Kapila
amit.kapila16@gmail.com
In reply to: Fabrízio de Royes Mello (#228)
Re: speed up a logical replica setup

On Fri, Mar 22, 2024 at 9:44 AM Fabrízio de Royes Mello
<fabriziomello@gmail.com> wrote:

On Fri, Mar 22, 2024 at 12:54 AM Amit Kapila <amit.kapila16@gmail.com> wrote:

The user choosing to convert a physical standby to a subscriber would
in some cases be interested in converting it for all the databases
(say for the case of upgrade [1]) and giving options for each database
would be cumbersome for her.

...

[1] - This tool can be used in an upgrade where the user first
converts physical standby to subscriber to get incremental changes and
then performs an online upgrade.

The first use case me and Euler discussed some time ago to upstream this tool was exactly what Amit described so IMHO we should make it easier for users to subscribe to all existing user databases.

I feel that will be a good use case for this tool especially now
(with this release) when we allow upgrade of subscriptions. In this
regard, I feel if the user doesn't specify any database it should have
subscriptions for all databases and the user should have a way to
provide publication/slot/subscription names.

--
With Regards,
Amit Kapila.

#230vignesh C
vignesh21@gmail.com
In reply to: Euler Taveira (#226)
Re: speed up a logical replica setup

On Fri, 22 Mar 2024 at 09:01, Euler Taveira <euler@eulerto.com> wrote:

On Thu, Mar 21, 2024, at 6:49 AM, Shlok Kyal wrote:

There is a compilation error while building postgres with the patch
due to a recent commit. I have attached a top-up patch v32-0003 to
resolve this compilation error.
I have not updated the version of the patch as I have not made any
change in v32-0001 and v32-0002 patch.

I'm attaching a new version (v33) to incorporate this fix (v32-0003) into the
main patch (v32-0001). This version also includes 2 new tests:

- refuse to run if the standby server is running
- refuse to run if the standby was promoted e.g. it is not in recovery

The first one exercises a recent change (standby should be stopped) and the
second one covers an important requirement.

Few comments:
1) In error case PQclear and PQfinish should be called:
+       /* Secure search_path */
+       res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               pg_log_error("could not clear search_path: %s",
+                                        PQresultErrorMessage(res));
+               if (exit_on_error)
+                       exit(1);
+
+               return NULL;
+       }
+       PQclear(res);
2) Call fflush here before calling system command to get proper
ordered console output:
a) Call fflush:
+               int                     rc = system(cmd_str);
+
+               if (rc == 0)
+                       pg_log_info("subscriber successfully changed
the system identifier");
+               else
+                       pg_fatal("subscriber failed to change system
identifier: exit code: %d", rc);
+       }
b) similarly here:
+       pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+       rc = system(pg_ctl_cmd->data);
+       pg_ctl_status(pg_ctl_cmd->data, rc);
+       standby_running = true;
c) similarly here:
+       pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+                                                 datadir);
+       pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+       rc = system(pg_ctl_cmd);
+       pg_ctl_status(pg_ctl_cmd, rc);
+       standby_running = false;
3) Call PQClear in error cases:
a) Call PQClear
+       res = PQexec(conn, "SELECT system_identifier FROM
pg_catalog.pg_control_system()");
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               pg_log_error("could not get system identifier: %s",
+                                        PQresultErrorMessage(res));
+               disconnect_database(conn, true);
+       }
b) similarly here
+       if (PQntuples(res) != 1)
+       {
+               pg_log_error("could not get system identifier: got %d
rows, expected %d row",
+                                        PQntuples(res), 1);
+               disconnect_database(conn, true);
+       }
+
c) similarly here
+       res = PQexec(conn,
+                                "SELECT oid FROM pg_catalog.pg_database "
+                                "WHERE datname =
pg_catalog.current_database()");
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+               pg_log_error("could not obtain database OID: %s",
+                                        PQresultErrorMessage(res));
+               disconnect_database(conn, true);
+       }
+

d) There are few more places like this.

4) Since we are using a global variable, we might be able to remove
initializing many of them.
+       /* Default settings */
+       subscriber_dir = NULL;
+       opt.config_file = NULL;
+       opt.pub_conninfo_str = NULL;
+       opt.socket_dir = NULL;
+       opt.sub_port = DEFAULT_SUB_PORT;
+       opt.sub_username = NULL;
+       opt.database_names = (SimpleStringList){0};
+       opt.recovery_timeout = 0;

Regards,
Vignesh

#231Shubham Khanna
khannashubham1197@gmail.com
In reply to: Euler Taveira (#226)
1 attachment(s)
Re: speed up a logical replica setup

On Fri, Mar 22, 2024 at 9:02 AM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Mar 21, 2024, at 6:49 AM, Shlok Kyal wrote:

There is a compilation error while building postgres with the patch
due to a recent commit. I have attached a top-up patch v32-0003 to
resolve this compilation error.
I have not updated the version of the patch as I have not made any
change in v32-0001 and v32-0002 patch.

I'm attaching a new version (v33) to incorporate this fix (v32-0003) into the
main patch (v32-0001). This version also includes 2 new tests:

- refuse to run if the standby server is running
- refuse to run if the standby was promoted e.g. it is not in recovery

The first one exercises a recent change (standby should be stopped) and the
second one covers an important requirement.

Based on the discussion [1] about the check functions, Vignesh suggested that it
should check both server before exiting. v33-0003 implements it. I don't have a
strong preference; feel free to apply it.

[1] /messages/by-id/CALDaNm1Dg5tDRmaabk+ZND4WF17NrNq52WZxCE+90-PGz5trQQ@mail.gmail.com

I had run valgrind with pg_createsubscriber to see if there were any
issues. Valgrind reported the following issues:
==651272== LEAK SUMMARY:
==651272== definitely lost: 1,319 bytes in 18 blocks
==651272== indirectly lost: 1,280 bytes in 2 blocks
==651272== possibly lost: 44 bytes in 3 blocks
==651272== still reachable: 3,066 bytes in 22 blocks
==651272== suppressed: 0 bytes in 0 blocks
==651272==
==651272== For lists of detected and suppressed errors, rerun with: -s
==651272== ERROR SUMMARY: 17 errors from 17 contexts (suppressed: 0 from 0)
The attached report has the details of the same.

Thanks and Regards:
Shubham Khanna.

Attachments:

valgrind.logapplication/octet-stream; name=valgrind.logDownload
#232Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Amit Kapila (#227)
RE: speed up a logical replica setup

Dear Amit, Euler,

I also want to share my opinion, just in case.

On Thu, Mar 21, 2024 at 8:00 PM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Mar 21, 2024, at 10:33 AM, vignesh C wrote:

If we commit this we might not be able to change the way the option
behaves once the customers starts using it. How about removing these
options in the first version and adding it in the next version after
more discussion.

We don't need to redesign this one if we want to add a format string in a next
version. A long time ago, pg_dump started to accept pattern for tables without
breaking or deprecating the -t option. If you have 100 databases and you don't
want to specify the options or use a script to generate it for you, you also
have the option to let pg_createsubscriber generate the object names for you.
Per my experience, it will be a rare case.

But, why go with some UI in the first place which we don't think is a
good one, or at least don't have a broader agreement that it is a good
one? The problem with self-generated names for users could be that
they won't be able to make much out of it. If one has to always use
those internally then probably that would be acceptable. I would
prefer what Tomas proposed a few emails ago as compared to this one as
that has fewer options to be provided by users but still, they can
later identify objects. But surely, we should discuss if you or others
have better alternatives.

IIUC, added options were inspired by Tomas. And he said the limitation (pub/sub/slot
name cannot be specified) was acceptable for the first version. I agree with him.
(To be honest, I feel that options should be fewer for the first release)

The user choosing to convert a physical standby to a subscriber would
in some cases be interested in converting it for all the databases
(say for the case of upgrade [1]) and giving options for each database
would be cumbersome for her.

+1 for the primary use case.

Currently dry-run will do the check and might fail on identifying a
few failures like after checking subscriber configurations. Then the
user will have to correct the configuration and re-run then fix the
next set of failures. Whereas the suggest-config will display all the
optimal configuration for both the primary and the standby in a single
shot. This is not a must in the first version, it can be done as a
subsequent enhancement.

Do you meant publisher, right? Per order, check_subscriber is done before
check_publisher and it checks all settings on the subscriber before exiting. In
v30, I changed the way it provides the required settings. In a previous version,
it fails when it found a wrong setting; the current version, check all settings
from that server before providing a suitable error.

pg_createsubscriber: checking settings on publisher
pg_createsubscriber: primary has replication slot "physical_slot"
pg_createsubscriber: error: publisher requires wal_level >= logical
pg_createsubscriber: error: publisher requires 2 replication slots, but only 0

remain

pg_createsubscriber: hint: Consider increasing max_replication_slots to at least

3.

pg_createsubscriber: error: publisher requires 2 wal sender processes, but only

0 remain

pg_createsubscriber: hint: Consider increasing max_wal_senders to at least 3.

If you have such an error, you will fix them all and rerun using dry run mode
again to verify everything is ok. I don't have a strong preference about it. It
can be changed easily (unifying the check functions or providing a return for
each of the check functions).

We can unify the checks but not sure if it is worth it. I am fine
either way. It would have been better if we provided a way for a user
to know the tool's requirement rather than letting him know via errors
but I think it should be okay to extend it later as well.

Both ways are OK, but I prefer to unify checks a bit. The number of working modes
in the same executables should be reduced as much as possible.

Also, I feel the current specification is acceptable. pg_upgrade checks one by
one and exits soon in bad cases. If both old and new clusters have issues, the
first run only reports the old one's issues. After DBA fixes and runs again,
issues on the new cluster are output.

[1]: /messages/by-id/8d52c226-7e34-44f7-a919-759bf8d81541@enterprisedb.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#233Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Hayato Kuroda (Fujitsu) (#232)
RE: speed up a logical replica setup

IIUC, added options were inspired by Tomas. And he said the limitation
(pub/sub/slot
name cannot be specified) was acceptable for the first version. I agree with him.
(To be honest, I feel that options should be fewer for the first release)

Just to confirm - I don't think it is not completely needed. If we can agree a specification
in sometime, it's OK for me to add them. If you ask me, I still prefer Tomas's approach.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#234Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Shubham Khanna (#231)
4 attachment(s)
RE: speed up a logical replica setup

Dear Shubham,

I had run valgrind with pg_createsubscriber to see if there were any
issues.

Thanks for running the tool!

Valgrind reported the following issues:
==651272== LEAK SUMMARY:
==651272== definitely lost: 1,319 bytes in 18 blocks
==651272== indirectly lost: 1,280 bytes in 2 blocks
==651272== possibly lost: 44 bytes in 3 blocks
==651272== still reachable: 3,066 bytes in 22 blocks
==651272== suppressed: 0 bytes in 0 blocks
==651272==
==651272== For lists of detected and suppressed errors, rerun with: -s
==651272== ERROR SUMMARY: 17 errors from 17 contexts (suppressed: 0 from
0)
The attached report has the details of the same.

I read the report. I'm not sure all entries must be fixed. Based on other client
tools, old discussions [1]/messages/by-id/40595e73-c7e1-463a-b8be-49792e870007@app.fastmail.com, and current codes, I thought we could determine the below
rule:

* For global variables (and their attributes), no need to free the allocated memory.
* For local variables (and their attributes) in main(), no need to free the allocated memory.
* For local variables in other functions, they should be free'd at the end of the function.

Per above rule and your report, I made a top-up patch which adds pg_free() and
destroyPQExpBuffer() several places. How do you think?

[1]: /messages/by-id/40595e73-c7e1-463a-b8be-49792e870007@app.fastmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

v34-0001-pg_createsubscriber-creates-a-new-logical-replic.patchapplication/octet-stream; name=v34-0001-pg_createsubscriber-creates-a-new-logical-replic.patchDownload
From d199ea65120d7690da1c1c678dd3e751a8816e96 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 5 Jun 2023 14:39:40 -0400
Subject: [PATCH v34 1/4] pg_createsubscriber: creates a new logical replica
 from a standby server

It must be run on the target server and should be able to connect to the
source server (publisher) and the target server (subscriber). All tables
in the specified database(s) are included in the logical replication
setup. A pair of publication and subscription objects are created for
each database.

The main advantage of pg_createsubscriber over the common logical
replication setup is the initial data copy. It also reduces the catchup
phase.

Some prerequisites must be met to successfully run it. It is basically
the logical replication requirements. It starts creating a publication
using FOR ALL TABLES and a replication slot for each specified database.
Write recovery parameters into the target data directory and start the
target server. It specifies the LSN of the last replication slot
(replication start point) up to which the recovery will proceed. Wait
until the target server is promoted. Create one subscription per
specified database (using publication and replication slot created in a
previous step) on the target server. Set the replication progress to the
replication start point for each subscription. Enable the subscription
for each specified database on the target server. And finally, change
the system identifier on the target server.
---
 doc/src/sgml/ref/allfiles.sgml                |    1 +
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  525 ++++
 doc/src/sgml/reference.sgml                   |    1 +
 src/bin/pg_basebackup/.gitignore              |    1 +
 src/bin/pg_basebackup/Makefile                |   11 +-
 src/bin/pg_basebackup/meson.build             |   19 +
 src/bin/pg_basebackup/nls.mk                  |    1 +
 src/bin/pg_basebackup/pg_createsubscriber.c   | 2118 +++++++++++++++++
 .../t/040_pg_createsubscriber.pl              |  364 +++
 9 files changed, 3038 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/pg_createsubscriber.sgml
 create mode 100644 src/bin/pg_basebackup/pg_createsubscriber.c
 create mode 100644 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b18..f5be638867 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -205,6 +205,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY pgCombinebackup    SYSTEM "pg_combinebackup.sgml">
 <!ENTITY pgConfig           SYSTEM "pg_config-ref.sgml">
 <!ENTITY pgControldata      SYSTEM "pg_controldata.sgml">
+<!ENTITY pgCreateSubscriber SYSTEM "pg_createsubscriber.sgml">
 <!ENTITY pgCtl              SYSTEM "pg_ctl-ref.sgml">
 <!ENTITY pgDump             SYSTEM "pg_dump.sgml">
 <!ENTITY pgDumpall          SYSTEM "pg_dumpall.sgml">
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
new file mode 100644
index 0000000000..f0cfed8c47
--- /dev/null
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -0,0 +1,525 @@
+<!--
+doc/src/sgml/ref/pg_createsubscriber.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="app-pgcreatesubscriber">
+ <indexterm zone="app-pgcreatesubscriber">
+  <primary>pg_createsubscriber</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle><application>pg_createsubscriber</application></refentrytitle>
+  <manvolnum>1</manvolnum>
+  <refmiscinfo>Application</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>pg_createsubscriber</refname>
+  <refpurpose>convert a physical replica into a new logical replica</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>pg_createsubscriber</command>
+   <arg rep="repeat"><replaceable>option</replaceable></arg>
+   <group choice="plain">
+    <group choice="req">
+     <arg choice="plain"><option>-d</option></arg>
+     <arg choice="plain"><option>--database</option></arg>
+    </group>
+    <replaceable>dbname</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-D</option> </arg>
+     <arg choice="plain"><option>--pgdata</option></arg>
+    </group>
+    <replaceable>datadir</replaceable>
+    <group choice="req">
+     <arg choice="plain"><option>-P</option></arg>
+     <arg choice="plain"><option>--publisher-server</option></arg>
+    </group>
+    <replaceable>connstr</replaceable>
+   </group>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+  <para>
+    <application>pg_createsubscriber</application> creates a new logical
+    replica from a physical standby server. All tables in the specified
+    database are included in the logical replication setup. A pair of
+    publication and subscription objects are created for each database. It
+    must be run at the target server.
+  </para>
+
+  <para>
+    After a successful run, the state of the target server is analogous to a
+    fresh logical replication setup. The main difference between the logical
+    replication setup and <application>pg_createsubscriber</application>
+    is the initial data copy. It does only the synchronization phase, which
+    ensures each table is brought up to a synchronized state.
+  </para>
+
+  <para>
+   The <application>pg_createsubscriber</application> targets large database
+   systems because in logical replication setup, most of the time is spent
+   doing the initial data copy. Furthermore, a side effect of this long time
+   spent synchronizing data is usually a large amount of changes to be applied
+   (that were produced during the initial data copy) which increases even more
+   the time when the logical replica will be available. For smaller databases,
+   <link linkend="logical-replication">initial data synchronization</link> is
+   recommended.
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Options</title>
+
+   <para>
+    <application>pg_createsubscriber</application> accepts the following
+    command-line arguments:
+
+    <variablelist>
+     <varlistentry>
+      <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
+      <term><option>--database=<replaceable class="parameter">dbname</replaceable></option></term>
+      <listitem>
+       <para>
+        The database name to create the subscription. Multiple databases can be
+        selected by writing multiple <option>-d</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-D <replaceable class="parameter">directory</replaceable></option></term>
+      <term><option>--pgdata=<replaceable class="parameter">directory</replaceable></option></term>
+      <listitem>
+       <para>
+        The target directory that contains a cluster directory from a physical
+        replica.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-n</option></term>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Do everything except actually modifying the target directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-p <replaceable class="parameter">port</replaceable></option></term>
+      <term><option>--subscriber-port=<replaceable class="parameter">port</replaceable></option></term>
+      <listitem>
+       <para>
+        The port number on which the target server is listening for connections.
+        Defaults to running the target server on port 50432 to avoid unintended
+        client connections.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-P <replaceable class="parameter">connstr</replaceable></option></term>
+      <term><option>--publisher-server=<replaceable class="parameter">connstr</replaceable></option></term>
+      <listitem>
+       <para>
+        The connection string to the publisher. For details see <xref linkend="libpq-connstring"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-s <replaceable class="parameter">dir</replaceable></option></term>
+      <term><option>--socket-directory=<replaceable class="parameter">dir</replaceable></option></term>
+      <listitem>
+       <para>
+        The directory to use for postmaster sockets on target server. The
+        default is current directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-t <replaceable class="parameter">seconds</replaceable></option></term>
+       <term><option>--recovery-timeout=<replaceable class="parameter">seconds</replaceable></option></term>
+       <listitem>
+       <para>
+        The maximum number of seconds to wait for recovery to end. Setting to 0
+        disables. The default is 0.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-U <replaceable class="parameter">username</replaceable></option></term>
+      <term><option>--subscriber-username=<replaceable class="parameter">username</replaceable></option></term>
+      <listitem>
+       <para>
+        The username to connect as on target server. Defaults to the current
+        operating system user name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>-v</option></term>
+      <term><option>--verbose</option></term>
+      <listitem>
+       <para>
+        Enables verbose mode. This will cause
+        <application>pg_createsubscriber</application> to output progress messages
+        and detailed information about each step to standard error.
+        Repeating the option causes additional debug-level messages to appear on
+        standard error.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--config-file=<replaceable class="parameter">filename</replaceable></option></term>
+      <listitem>
+       <para>
+        Use the specified main server configuration file for the target
+        data directory. The <application>pg_createsubscriber</application>
+        uses internally the <application>pg_ctl</application> command to start
+        and stop the target server. It allows you to specify the actual
+        <filename>postgresql.conf</filename> configuration file if it is stored
+        outside the data directory.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--publication=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The publication name to set up the logical replication. Multiple
+        publications can be specified by writing multiple
+        <option>--publication</option> switches. The number of publication
+        names must match the number of specified databases, otherwise an error
+        is reported. The order of the multiple publication name switches must
+        match the order of database switches. If this option is not specified,
+        a generated name is assigned to the publication name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--subscription=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The subscription name to set up the logical replication. Multiple
+        subscriptions can be specified by writing multiple
+        <option>--subscription</option> switches. The number of subscription
+        names must match the number of specified databases, otherwise an error
+        is reported. The order of the multiple subscription name switches must
+        match the order of database switches. If this option is not specified,
+        a generated name is assigned to the subscription name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--replication-slot=<replaceable class="parameter">name</replaceable></option></term>
+      <listitem>
+       <para>
+        The replication slot name to set up the logical replication. Multiple
+        replication slots can be specified by writing multiple
+        <option>--replication-slot</option> switches. The number of replication
+        slot names must match the number of specified databases, otherwise an
+        error is reported. The order of the multiple replication slot name
+        switches must match the order of database switches. If this option is
+        not specified, the subscription name is assigned to the replication
+        slot name.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-?</option></term>
+       <term><option>--help</option></term>
+       <listitem>
+       <para>
+       Show help about <application>pg_createsubscriber</application> command
+       line arguments, and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+     <varlistentry>
+       <term><option>-V</option></term>
+       <term><option>--version</option></term>
+       <listitem>
+       <para>
+       Print the <application>pg_createsubscriber</application> version and exit.
+       </para>
+       </listitem>
+     </varlistentry>
+
+    </variablelist>
+   </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   There are some prerequisites for
+   <application>pg_createsubscriber</application> to convert the target server
+   into a logical replica. If these are not met, an error will be reported. The
+   source and target servers must have the same major version as the
+   <application>pg_createsubscriber</application>. The given target data
+   directory must have the same system identifier as the source data directory.
+   The given database user for the target data directory must have privileges
+   for creating <link linkend="sql-createsubscription">subscriptions</link> and
+   using
+   <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>.
+  </para>
+
+  <para>
+   The target server must be used as a physical standby. The target server
+   must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   and
+   <link linkend="guc-max-logical-replication-workers"><varname>max_logical_replication_workers</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases. The target server must have
+   <link linkend="guc-max-worker-processes"><varname>max_worker_processes</varname></link>
+   configured to a value greater than the number of specified databases. The
+   target server must accept local connections.
+  </para>
+
+  <para>
+   The source server must accept connections from the target server. The
+   source server must not be in recovery. The source server must have
+   <link linkend="guc-wal-level"><varname>wal_level</varname></link> as
+   <literal>logical</literal>.
+   The source server must have
+   <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases plus existing replication slots. The source server must have
+   <link linkend="guc-max-wal-senders"><varname>max_wal_senders</varname></link>
+   configured to a value greater than or equal to the number of specified
+   databases and existing <literal>walsender</literal> processes.
+  </para>
+
+  <warning>
+   <title>Warning</title>
+   <para>
+    If <application>pg_createsubscriber</application> fails after the target
+    server was promoted, then the data directory is likely not in a state that
+    can be recovered. In such case, creating a new standby server is
+    recommended.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> usually starts the target
+    server with different connection settings during transformation. Hence,
+    connections to the target server should fail.
+   </para>
+
+   <para>
+    During the recovery process, if the target server disconnects from the
+    source server, <application>pg_createsubscriber</application> will check
+    a few times if the connection has been reestablished to stream the required
+    WAL. After a few attempts, it terminates with an error.
+   </para>
+
+   <para>
+    Since DDL commands are not replicated by logical replication, avoid
+    executing DDL commands that change the database schema while running
+    <application>pg_createsubscriber</application>. If the target server has
+    already been converted to logical replica, the DDL commands must not be
+    replicated which might cause an error.
+   </para>
+
+   <para>
+    If <application>pg_createsubscriber</application> fails while processing,
+    objects (publications, replication slots) created on the source server
+    are removed. The removal might fail if the target server cannot connect to
+    the source server. In such a case, a warning message will inform the
+    objects left. If the target server is running, it will be stopped.
+   </para>
+
+   <para>
+    If the replication is using a
+    <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+    it will be removed from the source server after the logical replication setup.
+   </para>
+
+   <para>
+    If the target server is a synchronous replica, transaction commits on the
+    primary might wait for replication while running
+    <application>pg_createsubscriber</application>.
+   </para>
+
+   <para>
+    <application>pg_createsubscriber</application> changes the system identifier
+    using <application>pg_resetwal</application>. It would avoid situations in
+    which the target server might use WAL files from the source server. If the
+    target server has a standby, replication will break and a fresh standby
+    should be created.
+   </para>
+  </warning>
+
+ </refsect1>
+
+ <refsect1>
+  <title>How It Works</title>
+
+  <para>
+    The basic idea is to have a replication start point from the source server
+    and set up a logical replication to start from this point:
+  </para>
+
+  <procedure>
+   <step>
+    <para>
+     Start the target server with the specified command-line options. If the
+     target server is running, <application>pg_createsubscriber</application>
+     will terminate with an error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Check if the target server can be converted. There are also a few checks
+     on the source server. If any of the prerequisites are not met,
+     <application>pg_createsubscriber</application> will terminate with an
+     error.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a publication and replication slot for each specified database on
+     the source server. Each publication is created using
+     <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>.
+     If <option>publication-name</option> option is not specified, it has the
+     following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameter:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     If <option>replication-slot-name</option> is not specified, the
+     replication slot has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     These replication slots will be used by the subscriptions in a future step.
+     The last replication slot LSN is used as a stopping point in the
+     <xref linkend="guc-recovery-target-lsn"/> parameter and by the
+     subscriptions as a replication start point. It guarantees that no
+     transaction will be lost.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Write recovery parameters into the target data directory and restart the
+     target server. It specifies an LSN (<xref linkend="guc-recovery-target-lsn"/>)
+     of the write-ahead log location up to which recovery will proceed. It also
+     specifies <literal>promote</literal> as the action that the server should
+     take once the recovery target is reached. Additional
+     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
+     are added to avoid unexpected behavior during the recovery process such as
+     end of the recovery as soon as a consistent state is reached (WAL should
+     be applied until the replication start location) and multiple recovery
+     targets that can cause a failure. This step finishes once the server ends
+     standby mode and is accepting read-write transactions.
+     If <option>--recovery-timeout</option> option is set,
+     <application>pg_createsubscriber</application> terminates if recovery does
+     not end until the given number of seconds.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Create a subscription for each specified database on the target server.
+     If <option>subscription-name</option> is not specified, the subscription
+     has the following name pattern:
+     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
+     database <parameter>oid</parameter>, random <parameter>int</parameter>).
+     It does not copy existing data from the source server. It does not create
+     a replication slot. Instead, it uses the replication slot that was created
+     in a previous step. The subscription is created but it is not enabled yet.
+     The reason is the replication progress must be set to the replication
+     start point before starting the replication.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Drop publications on the target server that were replicated because they
+     were created before the replication start location. It has no use on the
+     subscriber.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Set the replication progress to the replication start point for each
+     subscription. When the target server starts the recovery process, it
+     catches up to the replication start point. This is the exact LSN to be used
+     as a initial replication location for each subscription. The replication
+     origin name is obtained since the subscription was created. The replication
+     origin name and the replication start point are used in
+     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
+     to set up the initial replication location.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Enable the subscription for each specified database on the target server.
+     The subscription starts applying transactions from the replication start
+     point.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     If the standby server was using
+     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
+     it has no use from now on so drop it.
+    </para>
+   </step>
+
+   <step>
+    <para>
+     Update the system identifier on the target server. The
+     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
+     The target server is stopped as a <command>pg_resetwal</command> requirement.
+    </para>
+   </step>
+  </procedure>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To create a logical replica for databases <literal>hr</literal> and
+   <literal>finance</literal> from a physical replica at <literal>foo</literal>:
+<screen>
+<prompt>$</prompt> <userinput>pg_createsubscriber -D /usr/local/pgsql/data -P "host=foo" -d hr -d finance</userinput>
+</screen>
+  </para>
+
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="app-pgbasebackup"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6..ff85ace83f 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -282,6 +282,7 @@
    &pgarchivecleanup;
    &pgChecksums;
    &pgControldata;
+   &pgCreateSubscriber;
    &pgCtl;
    &pgResetwal;
    &pgRewind;
diff --git a/src/bin/pg_basebackup/.gitignore b/src/bin/pg_basebackup/.gitignore
index 26048bdbd8..14d5de6c01 100644
--- a/src/bin/pg_basebackup/.gitignore
+++ b/src/bin/pg_basebackup/.gitignore
@@ -1,4 +1,5 @@
 /pg_basebackup
+/pg_createsubscriber
 /pg_receivewal
 /pg_recvlogical
 
diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index abfb6440ec..26c53e473f 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -44,11 +44,14 @@ BBOBJS = \
 	bbstreamer_tar.o \
 	bbstreamer_zstd.o
 
-all: pg_basebackup pg_receivewal pg_recvlogical
+all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
+pg_createsubscriber: pg_createsubscriber.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 pg_receivewal: pg_receivewal.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_receivewal.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
@@ -57,6 +60,7 @@ pg_recvlogical: pg_recvlogical.o $(OBJS) | submake-libpq submake-libpgport subma
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_basebackup$(X) '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	$(INSTALL_PROGRAM) pg_createsubscriber$(X) '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	$(INSTALL_PROGRAM) pg_receivewal$(X) '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	$(INSTALL_PROGRAM) pg_recvlogical$(X) '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
@@ -65,12 +69,13 @@ installdirs:
 
 uninstall:
 	rm -f '$(DESTDIR)$(bindir)/pg_basebackup$(X)'
+	rm -f '$(DESTDIR)$(bindir)/pg_createsubscriber$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_receivewal$(X)'
 	rm -f '$(DESTDIR)$(bindir)/pg_recvlogical$(X)'
 
 clean distclean:
-	rm -f pg_basebackup$(X) pg_receivewal$(X) pg_recvlogical$(X) \
-		$(BBOBJS) pg_receivewal.o pg_recvlogical.o \
+	rm -f pg_basebackup$(X) pg_createsubscriber$(X) pg_receivewal$(X) pg_recvlogical$(X) \
+		$(BBOBJS) pg_createsubscriber.o pg_receivewal.o pg_recvlogical.o \
 		$(OBJS)
 	rm -rf tmp_check
 
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index f7e60e6670..c00acd5e11 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -38,6 +38,24 @@ pg_basebackup = executable('pg_basebackup',
 bin_targets += pg_basebackup
 
 
+pg_createsubscriber_sources = files(
+  'pg_createsubscriber.c'
+)
+
+if host_system == 'windows'
+  pg_createsubscriber_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+	'--NAME', 'pg_createsubscriber',
+	'--FILEDESC', 'pg_createsubscriber - create a new logical replica from a standby server',])
+endif
+
+pg_createsubscriber = executable('pg_createsubscriber',
+  pg_createsubscriber_sources,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args,
+)
+bin_targets += pg_createsubscriber
+
+
 pg_receivewal_sources = files(
   'pg_receivewal.c',
 )
@@ -89,6 +107,7 @@ tests += {
       't/011_in_place_tablespace.pl',
       't/020_pg_receivewal.pl',
       't/030_pg_recvlogical.pl',
+      't/040_pg_createsubscriber.pl',
     ],
   },
 }
diff --git a/src/bin/pg_basebackup/nls.mk b/src/bin/pg_basebackup/nls.mk
index fc475003e8..7870cea71c 100644
--- a/src/bin/pg_basebackup/nls.mk
+++ b/src/bin/pg_basebackup/nls.mk
@@ -8,6 +8,7 @@ GETTEXT_FILES    = $(FRONTEND_COMMON_GETTEXT_FILES) \
                    bbstreamer_tar.c \
                    bbstreamer_zstd.c \
                    pg_basebackup.c \
+                   pg_createsubscriber.c \
                    pg_receivewal.c \
                    pg_recvlogical.c \
                    receivelog.c \
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
new file mode 100644
index 0000000000..3a8240ea78
--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -0,0 +1,2118 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_createsubscriber.c
+ *	  Create a new logical replica from a standby server
+ *
+ * Copyright (C) 2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/bin/pg_basebackup/pg_createsubscriber.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "catalog/pg_authid_d.h"
+#include "common/connect.h"
+#include "common/controldata_utils.h"
+#include "common/file_perm.h"
+#include "common/logging.h"
+#include "common/pg_prng.h"
+#include "common/restricted_token.h"
+#include "fe_utils/recovery_gen.h"
+#include "fe_utils/simple_list.h"
+#include "getopt_long.h"
+
+#define	DEFAULT_SUB_PORT	"50432"
+
+/* Command-line options */
+struct CreateSubscriberOptions
+{
+	char	   *config_file;	/* configuration file */
+	char	   *pub_conninfo_str;	/* publisher connection string */
+	char	   *socket_dir;		/* directory for Unix-domain socket, if any */
+	char	   *sub_port;		/* subscriber port number */
+	const char *sub_username;	/* subscriber username */
+	SimpleStringList database_names;	/* list of database names */
+	SimpleStringList pub_names; /* list of publication names */
+	SimpleStringList sub_names; /* list of subscription names */
+	SimpleStringList replslot_names;	/* list of replication slot names */
+	int			recovery_timeout;	/* stop recovery after this time */
+};
+
+struct LogicalRepInfo
+{
+	Oid			oid;			/* database OID */
+	char	   *dbname;			/* database name */
+	char	   *pubconninfo;	/* publisher connection string */
+	char	   *subconninfo;	/* subscriber connection string */
+	char	   *pubname;		/* publication name */
+	char	   *subname;		/* subscription name */
+	char	   *replslotname;	/* replication slot name */
+
+	bool		made_replslot;	/* replication slot was created */
+	bool		made_publication;	/* publication was created */
+};
+
+static void cleanup_objects_atexit(void);
+static void usage();
+static char *get_base_conninfo(const char *conninfo, char **dbname);
+static char *get_sub_conninfo(const struct CreateSubscriberOptions *opt);
+static char *get_exec_path(const char *argv0, const char *progname);
+static void check_data_directory(const char *datadir);
+static char *concat_conninfo_dbname(const char *conninfo, const char *dbname);
+static struct LogicalRepInfo *store_pub_sub_info(const struct CreateSubscriberOptions *opt,
+												 const char *pub_base_conninfo,
+												 const char *sub_base_conninfo);
+static PGconn *connect_database(const char *conninfo, bool exit_on_error);
+static void disconnect_database(PGconn *conn, bool exit_on_error);
+static uint64 get_primary_sysid(const char *conninfo);
+static uint64 get_standby_sysid(const char *datadir);
+static void modify_subscriber_sysid(const struct CreateSubscriberOptions *opt);
+static bool server_is_in_recovery(PGconn *conn);
+static char *generate_object_name(PGconn *conn);
+static void check_publisher(const struct LogicalRepInfo *dbinfo);
+static char *setup_publisher(struct LogicalRepInfo *dbinfo);
+static void check_subscriber(const struct LogicalRepInfo *dbinfo);
+static void setup_subscriber(struct LogicalRepInfo *dbinfo,
+							 const char *consistent_lsn);
+static void setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir,
+						   const char *lsn);
+static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
+										  const char *slotname);
+static char *create_logical_replication_slot(PGconn *conn,
+											 struct LogicalRepInfo *dbinfo);
+static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+								  const char *slot_name);
+static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
+static void start_standby_server(const struct CreateSubscriberOptions *opt,
+								 bool restricted_access);
+static void stop_standby_server(const char *datadir);
+static void wait_for_end_recovery(const char *conninfo,
+								  const struct CreateSubscriberOptions *opt);
+static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
+static void create_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo);
+static void set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo,
+									 const char *lsn);
+static void enable_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo);
+
+#define	USEC_PER_SEC	1000000
+#define	WAIT_INTERVAL	1		/* 1 second */
+
+static const char *progname;
+
+static char *primary_slot_name = NULL;
+static bool dry_run = false;
+
+static bool success = false;
+
+static struct LogicalRepInfo *dbinfo;
+static int	num_dbs = 0;		/* number of specified databases */
+static int	num_pubs = 0;		/* number of specified publications */
+static int	num_subs = 0;		/* number of specified subscriptions */
+static int	num_replslots = 0;	/* number of specified replication slots */
+
+static pg_prng_state prng_state;
+
+static char *pg_ctl_path = NULL;
+static char *pg_resetwal_path = NULL;
+
+/* standby / subscriber data directory */
+static char *subscriber_dir = NULL;
+
+static bool recovery_ended = false;
+static bool standby_running = false;
+
+enum WaitPMResult
+{
+	POSTMASTER_READY,
+	POSTMASTER_STILL_STARTING
+};
+
+
+/*
+ * Cleanup objects that were created by pg_createsubscriber if there is an
+ * error.
+ *
+ * Publications and replication slots are created on primary. Depending on the
+ * step it failed, it should remove the already created objects if it is
+ * possible (sometimes it won't work due to a connection issue).
+ * There is no cleanup on the target server. The steps on the target server are
+ * executed *after* promotion, hence, at this point, a failure means recreate
+ * the physical replica and start again.
+ */
+static void
+cleanup_objects_atexit(void)
+{
+	if (success)
+		return;
+
+	/*
+	 * If the server is promoted, there is no way to use the current setup
+	 * again. Warn the user that a new replication setup should be done before
+	 * trying again.
+	 */
+	if (recovery_ended)
+	{
+		pg_log_warning("failed after the end of recovery");
+		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.  "
+							"You must recreate the physical replica before continuing.");
+	}
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		if (dbinfo[i].made_publication || dbinfo[i].made_replslot)
+		{
+			PGconn	   *conn;
+
+			conn = connect_database(dbinfo[i].pubconninfo, false);
+			if (conn != NULL)
+			{
+				if (dbinfo[i].made_publication)
+					drop_publication(conn, &dbinfo[i]);
+				if (dbinfo[i].made_replslot)
+					drop_replication_slot(conn, &dbinfo[i], dbinfo[i].replslotname);
+				disconnect_database(conn, false);
+			}
+			else
+			{
+				/*
+				 * If a connection could not be established, inform the user
+				 * that some objects were left on primary and should be
+				 * removed before trying again.
+				 */
+				if (dbinfo[i].made_publication)
+				{
+					pg_log_warning("publication \"%s\" in database \"%s\" on primary might be left behind",
+								   dbinfo[i].pubname, dbinfo[i].dbname);
+					pg_log_warning_hint("Consider dropping this publication before trying again.");
+				}
+				if (dbinfo[i].made_replslot)
+				{
+					pg_log_warning("replication slot \"%s\" in database \"%s\" on primary might be left behind",
+								   dbinfo[i].replslotname, dbinfo[i].dbname);
+					pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+				}
+			}
+		}
+	}
+
+	if (standby_running)
+		stop_standby_server(subscriber_dir);
+}
+
+static void
+usage(void)
+{
+	printf(_("%s creates a new logical replica from a standby server.\n\n"),
+		   progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]...\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_(" -d, --database=DBNAME               database to create a subscription\n"));
+	printf(_(" -D, --pgdata=DATADIR                location for the subscriber data directory\n"));
+	printf(_(" -n, --dry-run                       dry run, just show what would be done\n"));
+	printf(_(" -p, --subscriber-port=PORT          subscriber port number (default %s)\n"), DEFAULT_SUB_PORT);
+	printf(_(" -P, --publisher-server=CONNSTR      publisher connection string\n"));
+	printf(_(" -s, --socket-directory=DIR          socket directory to use (default current directory)\n"));
+	printf(_(" -t, --recovery-timeout=SECS         seconds to wait for recovery to end\n"));
+	printf(_(" -U, --subscriber-username=NAME      subscriber username\n"));
+	printf(_(" -v, --verbose                       output verbose messages\n"));
+	printf(_("     --config-file=FILENAME          use specified main server configuration\n"
+			 "                                     file when running target cluster\n"));
+	printf(_("     --publication=NAME              publication name\n"));
+	printf(_("     --replication-slot=NAME         replication slot name\n"));
+	printf(_("     --subscription=NAME             subscription name\n"));
+	printf(_(" -V, --version                       output version information, then exit\n"));
+	printf(_(" -?, --help                          show this help, then exit\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
+
+/*
+ * Validate a connection string. Returns a base connection string that is a
+ * connection string without a database name.
+ *
+ * Since we might process multiple databases, each database name will be
+ * appended to this base connection string to provide a final connection
+ * string. If the second argument (dbname) is not null, returns dbname if the
+ * provided connection string contains it. If option --database is not
+ * provided, uses dbname as the only database to setup the logical replica.
+ *
+ * It is the caller's responsibility to free the returned connection string and
+ * dbname.
+ */
+static char *
+get_base_conninfo(const char *conninfo, char **dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	PQconninfoOption *conn_opts = NULL;
+	PQconninfoOption *conn_opt;
+	char	   *errmsg = NULL;
+	char	   *ret;
+	int			i;
+
+	conn_opts = PQconninfoParse(conninfo, &errmsg);
+	if (conn_opts == NULL)
+	{
+		pg_log_error("could not parse connection string: %s", errmsg);
+		return NULL;
+	}
+
+	i = 0;
+	for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++)
+	{
+		if (strcmp(conn_opt->keyword, "dbname") == 0 && conn_opt->val != NULL)
+		{
+			if (dbname)
+				*dbname = pg_strdup(conn_opt->val);
+			continue;
+		}
+
+		if (conn_opt->val != NULL && conn_opt->val[0] != '\0')
+		{
+			if (i > 0)
+				appendPQExpBufferChar(buf, ' ');
+			appendPQExpBuffer(buf, "%s=%s", conn_opt->keyword, conn_opt->val);
+			i++;
+		}
+	}
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+	PQconninfoFree(conn_opts);
+
+	return ret;
+}
+
+/*
+ * Build a subscriber connection string. Only a few parameters are supported
+ * since it starts a server with restricted access.
+ */
+static char *
+get_sub_conninfo(const struct CreateSubscriberOptions *opt)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	appendPQExpBuffer(buf, "port=%s", opt->sub_port);
+#if !defined(WIN32)
+	appendPQExpBuffer(buf, " host=%s", opt->socket_dir);
+#endif
+	if (opt->sub_username != NULL)
+		appendPQExpBuffer(buf, " user=%s", opt->sub_username);
+	appendPQExpBuffer(buf, " fallback_application_name=%s", progname);
+
+	ret = pg_strdup(buf->data);
+
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Verify if a PostgreSQL binary (progname) is available in the same directory as
+ * pg_createsubscriber and it has the same version.  It returns the absolute
+ * path of the progname.
+ */
+static char *
+get_exec_path(const char *argv0, const char *progname)
+{
+	char	   *versionstr;
+	char	   *exec_path;
+	int			ret;
+
+	versionstr = psprintf("%s (PostgreSQL) %s\n", progname, PG_VERSION);
+	exec_path = pg_malloc(MAXPGPATH);
+	ret = find_other_exec(argv0, progname, versionstr, exec_path);
+
+	if (ret < 0)
+	{
+		char		full_path[MAXPGPATH];
+
+		if (find_my_exec(argv0, full_path) < 0)
+			strlcpy(full_path, progname, sizeof(full_path));
+
+		if (ret == -1)
+			pg_fatal("program \"%s\" is needed by %s but was not found in the same directory as \"%s\"",
+					 progname, "pg_createsubscriber", full_path);
+		else
+			pg_fatal("program \"%s\" was found by \"%s\" but was not the same version as %s",
+					 progname, full_path, "pg_createsubscriber");
+	}
+
+	pg_log_debug("%s path is:  %s", progname, exec_path);
+
+	return exec_path;
+}
+
+/*
+ * Is it a cluster directory? These are preliminary checks. It is far from
+ * making an accurate check. If it is not a clone from the publisher, it will
+ * eventually fail in a future step.
+ */
+static void
+check_data_directory(const char *datadir)
+{
+	struct stat statbuf;
+	char		versionfile[MAXPGPATH];
+
+	pg_log_info("checking if directory \"%s\" is a cluster data directory",
+				datadir);
+
+	if (stat(datadir, &statbuf) != 0)
+	{
+		if (errno == ENOENT)
+			pg_fatal("data directory \"%s\" does not exist", datadir);
+		else
+			pg_fatal("could not access directory \"%s\": %s", datadir,
+					 strerror(errno));
+	}
+
+	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
+	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	{
+		pg_fatal("directory \"%s\" is not a database cluster directory",
+				 datadir);
+	}
+}
+
+/*
+ * Append database name into a base connection string.
+ *
+ * dbname is the only parameter that changes so it is not included in the base
+ * connection string. This function concatenates dbname to build a "real"
+ * connection string.
+ */
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);
+
+	ret = pg_strdup(buf->data);
+	destroyPQExpBuffer(buf);
+
+	return ret;
+}
+
+/*
+ * Store publication and subscription information.
+ *
+ * If publication, replication slot and subscription names were specified,
+ * store it here. Otherwise, a generated name will be assigned to the object in
+ * setup_publisher().
+ */
+static struct LogicalRepInfo *
+store_pub_sub_info(const struct CreateSubscriberOptions *opt,
+				   const char *pub_base_conninfo,
+				   const char *sub_base_conninfo)
+{
+	struct LogicalRepInfo *dbinfo;
+	SimpleStringListCell *pubcell = NULL;
+	SimpleStringListCell *subcell = NULL;
+	SimpleStringListCell *replslotcell = NULL;
+	int			i = 0;
+
+	dbinfo = pg_malloc_array(struct LogicalRepInfo, num_dbs);
+
+	if (num_pubs > 0)
+		pubcell = opt->pub_names.head;
+	if (num_subs > 0)
+		subcell = opt->sub_names.head;
+	if (num_replslots > 0)
+		replslotcell = opt->replslot_names.head;
+
+	for (SimpleStringListCell *cell = opt->database_names.head; cell; cell = cell->next)
+	{
+		char	   *conninfo;
+
+		/* Fill publisher attributes */
+		conninfo = concat_conninfo_dbname(pub_base_conninfo, cell->val);
+		dbinfo[i].pubconninfo = conninfo;
+		dbinfo[i].dbname = cell->val;
+		if (num_pubs > 0)
+			dbinfo[i].pubname = pubcell->val;
+		else
+			dbinfo[i].pubname = NULL;
+		if (num_replslots > 0)
+			dbinfo[i].replslotname = replslotcell->val;
+		else
+			dbinfo[i].replslotname = NULL;
+		dbinfo[i].made_replslot = false;
+		dbinfo[i].made_publication = false;
+		/* Fill subscriber attributes */
+		conninfo = concat_conninfo_dbname(sub_base_conninfo, cell->val);
+		dbinfo[i].subconninfo = conninfo;
+		if (num_subs > 0)
+			dbinfo[i].subname = subcell->val;
+		else
+			dbinfo[i].subname = NULL;
+		/* Other fields will be filled later */
+
+		pg_log_debug("publisher(%d): publication: %s ; replication slot: %s ; connection string: %s", i,
+					 dbinfo[i].pubname ? dbinfo[i].pubname : "(auto)",
+					 dbinfo[i].replslotname ? dbinfo[i].replslotname : "(auto)",
+					 dbinfo[i].pubconninfo);
+		pg_log_debug("subscriber(%d): subscription: %s ; connection string: %s", i,
+					 dbinfo[i].subname ? dbinfo[i].subname : "(auto)",
+					 dbinfo[i].subconninfo);
+
+		if (num_pubs > 0)
+			pubcell = pubcell->next;
+		if (num_subs > 0)
+			subcell = subcell->next;
+		if (num_replslots > 0)
+			replslotcell = replslotcell->next;
+
+		i++;
+	}
+
+	return dbinfo;
+}
+
+/*
+ * Open a new connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static PGconn *
+connect_database(const char *conninfo, bool exit_on_error)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		pg_log_error("connection to database failed: %s",
+					 PQerrorMessage(conn));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+
+	/* Secure search_path */
+	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not clear search_path: %s",
+					 PQresultErrorMessage(res));
+		if (exit_on_error)
+			exit(1);
+
+		return NULL;
+	}
+	PQclear(res);
+
+	return conn;
+}
+
+/*
+ * Close the connection. If exit_on_error is true, it has an undesired
+ * condition and it should exit immediately.
+ */
+static void
+disconnect_database(PGconn *conn, bool exit_on_error)
+{
+	Assert(conn != NULL);
+
+	PQfinish(conn);
+
+	if (exit_on_error)
+		exit(1);
+}
+
+/*
+ * Obtain the system identifier using the provided connection. It will be used
+ * to compare if a data directory is a clone of another one.
+ */
+static uint64
+get_primary_sysid(const char *conninfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from publisher");
+
+	conn = connect_database(conninfo, true);
+
+	res = PQexec(conn, "SELECT system_identifier FROM pg_catalog.pg_control_system()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not get system identifier: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not get system identifier: got %d rows, expected %d row",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	sysid = strtou64(PQgetvalue(res, 0, 0), NULL, 10);
+
+	pg_log_info("system identifier is %llu on publisher",
+				(unsigned long long) sysid);
+
+	PQclear(res);
+	disconnect_database(conn, false);
+
+	return sysid;
+}
+
+/*
+ * Obtain the system identifier from control file. It will be used to compare
+ * if a data directory is a clone of another one. This routine is used locally
+ * and avoids a connection.
+ */
+static uint64
+get_standby_sysid(const char *datadir)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	uint64		sysid;
+
+	pg_log_info("getting system identifier from subscriber");
+
+	cf = get_controlfile(datadir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	sysid = cf->system_identifier;
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) sysid);
+
+	pg_free(cf);
+
+	return sysid;
+}
+
+/*
+ * Modify the system identifier. Since a standby server preserves the system
+ * identifier, it makes sense to change it to avoid situations in which WAL
+ * files from one of the systems might be used in the other one.
+ */
+static void
+modify_subscriber_sysid(const struct CreateSubscriberOptions *opt)
+{
+	ControlFileData *cf;
+	bool		crc_ok;
+	struct timeval tv;
+
+	char	   *cmd_str;
+
+	pg_log_info("modifying system identifier of subscriber");
+
+	cf = get_controlfile(subscriber_dir, &crc_ok);
+	if (!crc_ok)
+		pg_fatal("control file appears to be corrupt");
+
+	/*
+	 * Select a new system identifier.
+	 *
+	 * XXX this code was extracted from BootStrapXLOG().
+	 */
+	gettimeofday(&tv, NULL);
+	cf->system_identifier = ((uint64) tv.tv_sec) << 32;
+	cf->system_identifier |= ((uint64) tv.tv_usec) << 12;
+	cf->system_identifier |= getpid() & 0xFFF;
+
+	if (!dry_run)
+		update_controlfile(subscriber_dir, cf, true);
+
+	pg_log_info("system identifier is %llu on subscriber",
+				(unsigned long long) cf->system_identifier);
+
+	pg_log_info("running pg_resetwal on the subscriber");
+
+	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
+					   subscriber_dir, DEVNULL);
+
+	pg_log_debug("pg_resetwal command is: %s", cmd_str);
+
+	if (!dry_run)
+	{
+		int			rc = system(cmd_str);
+
+		if (rc == 0)
+			pg_log_info("subscriber successfully changed the system identifier");
+		else
+			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
+	}
+
+	pg_free(cf);
+}
+
+/*
+ * Generate an object name using a prefix, database oid and a random integer.
+ * It is used in case the user does not specify an object name (publication,
+ * subscription, replication slot).
+ */
+static char *
+generate_object_name(PGconn *conn)
+{
+	PGresult   *res;
+	Oid			oid;
+	uint32		rand;
+	char	   *objname;
+
+	res = PQexec(conn,
+				 "SELECT oid FROM pg_catalog.pg_database "
+				 "WHERE datname = pg_catalog.current_database()");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain database OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1)
+	{
+		pg_log_error("could not obtain database OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	/* Database OID */
+	oid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+
+	PQclear(res);
+
+	/* Random unsigned integer */
+	rand = pg_prng_uint32(&prng_state);
+
+	/*
+	 * Build the object name. The name must not exceed NAMEDATALEN - 1. This
+	 * current schema uses a maximum of 40 characters (20 + 10 + 1 + 8 +
+	 * '\0').
+	 */
+	objname = psprintf("pg_createsubscriber_%u_%x", oid, rand);
+
+	return objname;
+}
+
+/*
+ * Create the publications and replication slots in preparation for logical
+ * replication. Returns the LSN from latest replication slot. It will be the
+ * replication start point that is used to adjust the subscriptions (see
+ * set_replication_progress).
+ */
+static char *
+setup_publisher(struct LogicalRepInfo *dbinfo)
+{
+	char	   *lsn = NULL;
+
+	pg_prng_seed(&prng_state, (uint64) (getpid() ^ time(NULL)));
+
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		char	   *genname = NULL;
+
+		conn = connect_database(dbinfo[i].pubconninfo, true);
+
+		/*
+		 * If an object name was not specified as command-line options, assign
+		 * a generated object name. The replication slot has a different rule.
+		 * The subscription name is assigned to the replication slot name if no
+		 * replication slot is specified. It follows the same rule as CREATE
+		 * SUBSCRIPTION.
+		 */
+		if (num_pubs == 0 || num_subs == 0 || num_replslots == 0)
+			genname = generate_object_name(conn);
+		if (num_pubs == 0)
+			dbinfo[i].pubname = pg_strdup(genname);
+		if (num_subs == 0)
+			dbinfo[i].subname = pg_strdup(genname);
+		if (num_replslots == 0)
+			dbinfo[i].replslotname = pg_strdup(dbinfo[i].subname);
+
+		/*
+		 * Create publication on publisher. This step should be executed
+		 * *before* promoting the subscriber to avoid any transactions between
+		 * consistent LSN and the new publication rows (such transactions
+		 * wouldn't see the new publication rows resulting in an error).
+		 */
+		create_publication(conn, &dbinfo[i]);
+
+		/* Create replication slot on publisher */
+		if (lsn)
+			pg_free(lsn);
+		lsn = create_logical_replication_slot(conn, &dbinfo[i]);
+		if (lsn != NULL || dry_run)
+			pg_log_info("create replication slot \"%s\" on publisher",
+						dbinfo[i].replslotname);
+		else
+			exit(1);
+
+		disconnect_database(conn, false);
+	}
+
+	return lsn;
+}
+
+/*
+ * Is recovery still in progress?
+ */
+static bool
+server_is_in_recovery(PGconn *conn)
+{
+	PGresult   *res;
+	int			ret;
+
+	res = PQexec(conn, "SELECT pg_catalog.pg_is_in_recovery()");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain recovery progress: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+
+	ret = strcmp("t", PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+
+	return ret == 0;
+}
+
+/*
+ * Is the primary server ready for logical replication?
+ *
+ * XXX Does it not allow a synchronous replica?
+ */
+static void
+check_publisher(const struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	bool		failed = false;
+
+	char	   *wal_level;
+	int			max_repslots;
+	int			cur_repslots;
+	int			max_walsenders;
+	int			cur_walsenders;
+
+	pg_log_info("checking settings on publisher");
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * If the primary server is in recovery (i.e. cascading replication),
+	 * objects (publication) cannot be created because it is read only.
+	 */
+	if (server_is_in_recovery(conn))
+	{
+		pg_log_error("primary server cannot be in recovery");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on publisher.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - wal_level = logical
+	 * - max_replication_slots >= current + number of dbs to be converted
+	 * - max_wal_senders >= current + number of dbs to be converted
+	 * -----------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "WITH wl AS "
+				 "(SELECT setting AS wallevel FROM pg_catalog.pg_settings "
+				 "WHERE name = 'wal_level'), "
+				 "total_mrs AS "
+				 "(SELECT setting AS tmrs FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_replication_slots'), "
+				 "cur_mrs AS "
+				 "(SELECT count(*) AS cmrs "
+				 "FROM pg_catalog.pg_replication_slots), "
+				 "total_mws AS "
+				 "(SELECT setting AS tmws FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_wal_senders'), "
+				 "cur_mws AS "
+				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
+				 "WHERE backend_type = 'walsender') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publisher settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	wal_level = pg_strdup(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 0, 1));
+	cur_repslots = atoi(PQgetvalue(res, 0, 2));
+	max_walsenders = atoi(PQgetvalue(res, 0, 3));
+	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+
+	PQclear(res);
+
+	pg_log_debug("publisher: wal_level: %s", wal_level);
+	pg_log_debug("publisher: max_replication_slots: %d", max_repslots);
+	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
+	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
+	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+
+	/*
+	 * If standby sets primary_slot_name, check if this replication slot is in
+	 * use on primary for WAL retention purposes. This replication slot has no
+	 * use after the transformation, hence, it will be removed at the end of
+	 * this process.
+	 */
+	if (primary_slot_name)
+	{
+		PQExpBuffer str = createPQExpBuffer();
+
+		appendPQExpBuffer(str,
+						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
+						  "WHERE active AND slot_name = '%s'",
+						  primary_slot_name);
+
+		pg_log_debug("command is: %s", str->data);
+
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not obtain replication slot information: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		if (PQntuples(res) != 1)
+		{
+			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
+						 PQntuples(res), 1);
+			disconnect_database(conn, true);
+		}
+		else
+			pg_log_info("primary has replication slot \"%s\"",
+						primary_slot_name);
+
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	if (strcmp(wal_level, "logical") != 0)
+	{
+		pg_log_error("publisher requires wal_level >= logical");
+		failed = true;
+	}
+
+	if (max_repslots - cur_repslots < num_dbs)
+	{
+		pg_log_error("publisher requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots - cur_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  cur_repslots + num_dbs);
+		failed = true;
+	}
+
+	if (max_walsenders - cur_walsenders < num_dbs)
+	{
+		pg_log_error("publisher requires %d wal sender processes, but only %d remain",
+					 num_dbs, max_walsenders - cur_walsenders);
+		pg_log_error_hint("Consider increasing max_wal_senders to at least %d.",
+						  cur_walsenders + num_dbs);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Is the standby server ready for logical replication?
+ *
+ * XXX Does it not allow a time-delayed replica?
+ *
+ * XXX In a cascaded replication scenario (P -> S -> C), if the target server
+ * is S, it cannot detect there is a replica (server C) because server S starts
+ * accepting only local connections and server C cannot connect to it. Hence,
+ * there is not a reliable way to provide a suitable error saying the server C
+ * will be broken at the end of this process (due to pg_resetwal).
+ */
+static void
+check_subscriber(const struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+	bool		failed = false;
+
+	int			max_lrworkers;
+	int			max_repslots;
+	int			max_wprocs;
+
+	pg_log_info("checking settings on subscriber");
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	/* The target server must be a standby */
+	if (!server_is_in_recovery(conn))
+	{
+		pg_log_error("target server must be a standby");
+		disconnect_database(conn, true);
+	}
+
+	/*------------------------------------------------------------------------
+	 * Logical replication requires a few parameters to be set on subscriber.
+	 * Since these parameters are not a requirement for physical replication,
+	 * we should check it to make sure it won't fail.
+	 *
+	 * - max_replication_slots >= number of dbs to be converted
+	 * - max_logical_replication_workers >= number of dbs to be converted
+	 * - max_worker_processes >= 1 + number of dbs to be converted
+	 *------------------------------------------------------------------------
+	 */
+	res = PQexec(conn,
+				 "SELECT setting FROM pg_catalog.pg_settings WHERE name IN ("
+				 "'max_logical_replication_workers', "
+				 "'max_replication_slots', "
+				 "'max_worker_processes', "
+				 "'primary_slot_name') "
+				 "ORDER BY name");
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscriber settings: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	max_lrworkers = atoi(PQgetvalue(res, 0, 0));
+	max_repslots = atoi(PQgetvalue(res, 1, 0));
+	max_wprocs = atoi(PQgetvalue(res, 2, 0));
+	if (strcmp(PQgetvalue(res, 3, 0), "") != 0)
+		primary_slot_name = pg_strdup(PQgetvalue(res, 3, 0));
+
+	pg_log_debug("subscriber: max_logical_replication_workers: %d",
+				 max_lrworkers);
+	pg_log_debug("subscriber: max_replication_slots: %d", max_repslots);
+	pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs);
+	if (primary_slot_name)
+		pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name);
+
+	PQclear(res);
+
+	disconnect_database(conn, false);
+
+	if (max_repslots < num_dbs)
+	{
+		pg_log_error("subscriber requires %d replication slots, but only %d remain",
+					 num_dbs, max_repslots);
+		pg_log_error_hint("Consider increasing max_replication_slots to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_lrworkers < num_dbs)
+	{
+		pg_log_error("subscriber requires %d logical replication workers, but only %d remain",
+					 num_dbs, max_lrworkers);
+		pg_log_error_hint("Consider increasing max_logical_replication_workers to at least %d.",
+						  num_dbs);
+		failed = true;
+	}
+
+	if (max_wprocs < num_dbs + 1)
+	{
+		pg_log_error("subscriber requires %d worker processes, but only %d remain",
+					 num_dbs + 1, max_wprocs);
+		pg_log_error_hint("Consider increasing max_worker_processes to at least %d.",
+						  num_dbs + 1);
+		failed = true;
+	}
+
+	if (failed)
+		exit(1);
+}
+
+/*
+ * Create the subscriptions, adjust the initial location for logical
+ * replication and enable the subscriptions. That's the last step for logical
+ * replication setup.
+ */
+static void
+setup_subscriber(struct LogicalRepInfo *dbinfo, const char *consistent_lsn)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+
+		/* Connect to subscriber. */
+		conn = connect_database(dbinfo[i].subconninfo, true);
+
+		/*
+		 * Since the publication was created before the consistent LSN, it is
+		 * available on the subscriber when the physical replica is promoted.
+		 * Remove publications from the subscriber because it has no use.
+		 */
+		drop_publication(conn, &dbinfo[i]);
+
+		create_subscription(conn, &dbinfo[i]);
+
+		/* Set the replication progress to the correct LSN */
+		set_replication_progress(conn, &dbinfo[i], consistent_lsn);
+
+		/* Enable subscription */
+		enable_subscription(conn, &dbinfo[i]);
+
+		disconnect_database(conn, false);
+	}
+}
+
+/*
+ * Write the required recovery parameters.
+ */
+static void
+setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const char *lsn)
+{
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	/*
+	 * Despite of the recovery parameters will be written to the subscriber,
+	 * use a publisher connection. The primary_conninfo is generated using the
+	 * connection settings.
+	 */
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	/*
+	 * Write recovery parameters.
+	 *
+	 * The subscriber is not running yet. In dry run mode, the recovery
+	 * parameters *won't* be written. An invalid LSN is used for printing
+	 * purposes. Additional recovery parameters are added here. It avoids
+	 * unexpected behavior such as end of recovery as soon as a consistent
+	 * state is reached (recovery_target) and failure due to multiple recovery
+	 * targets (name, time, xid, LSN).
+	 */
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target = ''\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_timeline = 'latest'\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_inclusive = true\n");
+	appendPQExpBuffer(recoveryconfcontents,
+					  "recovery_target_action = promote\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_name = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_time = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_xid = ''\n");
+
+	if (dry_run)
+	{
+		appendPQExpBuffer(recoveryconfcontents, "# dry run mode");
+		appendPQExpBuffer(recoveryconfcontents,
+						  "recovery_target_lsn = '%X/%X'\n",
+						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
+						  lsn);
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+}
+
+/*
+ * Drop physical replication slot on primary if the standby was using it. After
+ * the transformation, it has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, const char *slotname)
+{
+	PGconn	   *conn;
+
+	/* Replication slot does not exist, do nothing */
+	if (!primary_slot_name)
+		return;
+
+	conn = connect_database(dbinfo[0].pubconninfo, false);
+	if (conn != NULL)
+	{
+		drop_replication_slot(conn, &dbinfo[0], slotname);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop replication slot \"%s\" on primary",
+					   slotname);
+		pg_log_warning_hint("Drop this replication slot soon to avoid retention of WAL files.");
+	}
+}
+
+/*
+ * Create a logical replication slot and returns a LSN.
+ *
+ * CreateReplicationSlot() is not used because it does not provide the one-row
+ * result set that contains the LSN.
+ */
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	const char *slot_name = dbinfo->replslotname;
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot('%s', '%s', %s, false, false)",
+					  slot_name, "pgoutput", "false");
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not create replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname,
+						 PQresultErrorMessage(res));
+			return NULL;
+		}
+
+		lsn = pg_strdup(PQgetvalue(res, 0, 0));
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_replslot = true;
+
+	destroyPQExpBuffer(str);
+
+	return lsn;
+}
+
+static void
+drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
+					  const char *slot_name)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+
+	Assert(conn != NULL);
+
+	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "SELECT pg_catalog.pg_drop_replication_slot('%s')", slot_name);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not drop replication slot \"%s\" on database \"%s\": %s",
+						 slot_name, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_replslot = false;	/* don't try again. */
+		}
+
+		PQclear(res);
+	}
+
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Reports a suitable message if pg_ctl fails.
+ */
+static void
+pg_ctl_status(const char *pg_ctl_cmd, int rc)
+{
+	if (rc != 0)
+	{
+		if (WIFEXITED(rc))
+		{
+			pg_log_error("pg_ctl failed with exit code %d", WEXITSTATUS(rc));
+		}
+		else if (WIFSIGNALED(rc))
+		{
+#if defined(WIN32)
+			pg_log_error("pg_ctl was terminated by exception 0x%X",
+						 WTERMSIG(rc));
+			pg_log_error_detail("See C include file \"ntstatus.h\" for a description of the hexadecimal value.");
+#else
+			pg_log_error("pg_ctl was terminated by signal %d: %s",
+						 WTERMSIG(rc), pg_strsignal(WTERMSIG(rc)));
+#endif
+		}
+		else
+		{
+			pg_log_error("pg_ctl exited with unrecognized status %d", rc);
+		}
+
+		pg_log_error_detail("The failed command was: %s", pg_ctl_cmd);
+		exit(1);
+	}
+}
+
+static void
+start_standby_server(const struct CreateSubscriberOptions *opt, bool restricted_access)
+{
+	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
+	int			rc;
+
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+					  pg_ctl_path, subscriber_dir);
+	if (restricted_access)
+	{
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-p %s\"", opt->sub_port);
+#if !defined(WIN32)
+
+		/*
+		 * An empty listen_addresses list means the server does not listen on
+		 * any IP interfaces; only Unix-domain sockets can be used to connect
+		 * to the server. Prevent external connections to minimize the chance
+		 * of failure.
+		 */
+		appendPQExpBufferStr(pg_ctl_cmd, " -o \"-c listen_addresses='' -c unix_socket_permissions=0700");
+		if (opt->socket_dir)
+			appendPQExpBuffer(pg_ctl_cmd, " -c unix_socket_directories='%s'",
+							  opt->socket_dir);
+		appendPQExpBufferChar(pg_ctl_cmd, '"');
+#endif
+	}
+	if (opt->config_file != NULL)
+		appendPQExpBuffer(pg_ctl_cmd, " -o \"-c config_file=%s\"",
+						  opt->config_file);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
+	rc = system(pg_ctl_cmd->data);
+	pg_ctl_status(pg_ctl_cmd->data, rc);
+	standby_running = true;
+	destroyPQExpBuffer(pg_ctl_cmd);
+	pg_log_info("server was started");
+}
+
+static void
+stop_standby_server(const char *datadir)
+{
+	char	   *pg_ctl_cmd;
+	int			rc;
+
+	pg_ctl_cmd = psprintf("\"%s\" stop -D \"%s\" -s", pg_ctl_path,
+						  datadir);
+	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd);
+	rc = system(pg_ctl_cmd);
+	pg_ctl_status(pg_ctl_cmd, rc);
+	standby_running = false;
+	pg_log_info("server was stopped");
+}
+
+/*
+ * Returns after the server finishes the recovery process.
+ *
+ * If recovery_timeout option is set, terminate abnormally without finishing
+ * the recovery process. By default, it waits forever.
+ */
+static void
+wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions *opt)
+{
+	PGconn	   *conn;
+	int			status = POSTMASTER_STILL_STARTING;
+	int			timer = 0;
+	int			count = 0;		/* number of consecutive connection attempts */
+
+#define NUM_CONN_ATTEMPTS	10
+
+	pg_log_info("waiting for the target server to reach the consistent state");
+
+	conn = connect_database(conninfo, true);
+
+	for (;;)
+	{
+		PGresult   *res;
+		bool		in_recovery = server_is_in_recovery(conn);
+
+		/*
+		 * Does the recovery process finish? In dry run mode, there is no
+		 * recovery mode. Bail out as the recovery process has ended.
+		 */
+		if (!in_recovery || dry_run)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
+		/*
+		 * If it is still in recovery, make sure the target server is
+		 * connected to the primary so it can receive the required WAL to
+		 * finish the recovery process. If it is disconnected try
+		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 */
+		res = PQexec(conn,
+					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
+		if (PQntuples(res) == 0)
+		{
+			if (++count > NUM_CONN_ATTEMPTS)
+			{
+				stop_standby_server(subscriber_dir);
+				pg_log_error("standby server disconnected from the primary");
+				break;
+			}
+		}
+		else
+			count = 0;			/* reset counter if it connects again */
+
+		PQclear(res);
+
+		/* Bail out after recovery_timeout seconds if this option is set */
+		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
+		{
+			stop_standby_server(subscriber_dir);
+			pg_log_error("recovery timed out");
+			disconnect_database(conn, true);
+		}
+
+		/* Keep waiting */
+		pg_usleep(WAIT_INTERVAL * USEC_PER_SEC);
+
+		timer += WAIT_INTERVAL;
+	}
+
+	disconnect_database(conn, false);
+
+	if (status == POSTMASTER_STILL_STARTING)
+		pg_fatal("server did not end recovery");
+
+	pg_log_info("target server reached the consistent state");
+	pg_log_info_hint("If pg_createsubscriber fails after this point, you must recreate the physical replica before continuing.");
+}
+
+/*
+ * Create a publication that includes all tables in the database.
+ */
+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *ipubname;
+	char	   *spubname;
+
+	Assert(conn != NULL);
+
+	ipubname = PQescapeIdentifier(conn, dbinfo->pubname, strlen(dbinfo->pubname));
+	spubname = PQescapeLiteral(conn, dbinfo->pubname, strlen(dbinfo->pubname));
+
+	/* Check if the publication already exists */
+	appendPQExpBuffer(str,
+					  "SELECT 1 FROM pg_catalog.pg_publication "
+					  "WHERE pubname = %s",
+					  spubname);
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain publication information: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) == 1)
+	{
+		/*
+		 * Unfortunately, if it reaches this code path, it will always fail
+		 * (unless you decide to change the existing publication name). That's
+		 * bad but it is very unlikely that the user will choose a name with
+		 * pg_createsubscriber_ prefix followed by the exact database oid and
+		 * a random number.
+		 */
+		pg_log_error("publication \"%s\" already exists", dbinfo->pubname);
+		pg_log_error_hint("Consider renaming this publication before continuing.");
+		disconnect_database(conn, true);
+	}
+
+	PQclear(res);
+	resetPQExpBuffer(str);
+
+	pg_log_info("creating publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  ipubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	/* For cleanup purposes */
+	dbinfo->made_publication = true;
+
+	pg_free(ipubname);
+	pg_free(spubname);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Remove publication if it couldn't finish all steps.
+ */
+static void
+drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *pubname;
+
+	Assert(conn != NULL);
+
+	pubname = PQescapeIdentifier(conn, dbinfo->pubname, strlen(dbinfo->pubname));
+
+	pg_log_info("dropping publication \"%s\" on database \"%s\"",
+				dbinfo->pubname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "DROP PUBLICATION %s", pubname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not drop publication \"%s\" on database \"%s\": %s",
+						 dbinfo->pubname, dbinfo->dbname, PQresultErrorMessage(res));
+			dbinfo->made_publication = false;	/* don't try again. */
+
+			/*
+			 * Don't disconnect and exit here. This routine is used by primary
+			 * (cleanup publication / replication slot due to an error) and
+			 * subscriber (remove the replicated publications). In both cases,
+			 * it can continue and provide instructions for the user to remove
+			 * it later if cleanup fails.
+			 */
+		}
+		PQclear(res);
+	}
+
+	pg_free(pubname);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Create a subscription with some predefined options.
+ *
+ * A replication slot was already created in a previous step. Let's use it.  It
+ * is not required to copy data. The subscription will be created but it will
+ * not be enabled now. That's because the replication progress must be set and
+ * the replication origin name (one of the function arguments) contains the
+ * subscription OID in its name. Once the subscription is created,
+ * set_replication_progress() can obtain the chosen origin name and set up its
+ * initial location.
+ */
+static void
+create_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *pubname;
+	char	   *subname;
+
+	Assert(conn != NULL);
+
+	pubname = PQescapeIdentifier(conn, dbinfo->pubname, strlen(dbinfo->pubname));
+	subname = PQescapeIdentifier(conn, dbinfo->subname, strlen(dbinfo->subname));
+
+	pg_log_info("creating subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str,
+					  "CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s "
+					  "WITH (create_slot = false, enabled = false, "
+					  "slot_name = '%s', copy_data = false)",
+					  subname, dbinfo->pubconninfo, pubname,
+					  dbinfo->replslotname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not create subscription \"%s\" on database \"%s\": %s",
+						 dbinfo->subname, dbinfo->dbname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	pg_free(pubname);
+	pg_free(subname);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Sets the replication progress to the consistent LSN.
+ *
+ * The subscriber caught up to the consistent LSN provided by the last
+ * replication slot that was created. The goal is to set up the initial
+ * location for the logical replication that is the exact LSN that the
+ * subscriber was promoted. Once the subscription is enabled it will start
+ * streaming from that location onwards.  In dry run mode, the subscription OID
+ * and LSN are set to invalid values for printing purposes.
+ */
+static void
+set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, const char *lsn)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	Oid			suboid;
+	char	   *subname;
+	char	   *dbname;
+	char	   *originname;
+	char	   *lsnstr;
+
+	Assert(conn != NULL);
+
+	subname = PQescapeLiteral(conn, dbinfo->subname, strlen(dbinfo->subname));
+	dbname = PQescapeLiteral(conn, dbinfo->dbname, strlen(dbinfo->dbname));
+
+	appendPQExpBuffer(str,
+					  "SELECT s.oid FROM pg_catalog.pg_subscription s "
+					  "INNER JOIN pg_catalog.pg_database d ON (s.subdbid = d.oid) "
+					  "WHERE s.subname = %s AND d.datname = %s",
+					  subname, dbname);
+
+	res = PQexec(conn, str->data);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		pg_log_error("could not obtain subscription OID: %s",
+					 PQresultErrorMessage(res));
+		disconnect_database(conn, true);
+	}
+
+	if (PQntuples(res) != 1 && !dry_run)
+	{
+		pg_log_error("could not obtain subscription OID: got %d rows, expected %d rows",
+					 PQntuples(res), 1);
+		disconnect_database(conn, true);
+	}
+
+	if (dry_run)
+	{
+		suboid = InvalidOid;
+		lsnstr = psprintf("%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+	}
+	else
+	{
+		suboid = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
+		lsnstr = psprintf("%s", lsn);
+	}
+
+	PQclear(res);
+
+	/*
+	 * The origin name is defined as pg_%u. %u is the subscription OID. See
+	 * ApplyWorkerMain().
+	 */
+	originname = psprintf("pg_%u", suboid);
+
+	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+				originname, lsnstr, dbinfo->dbname);
+
+	resetPQExpBuffer(str);
+	appendPQExpBuffer(str,
+					  "SELECT pg_catalog.pg_replication_origin_advance('%s', '%s')",
+					  originname, lsnstr);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not set replication progress for the subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+		PQclear(res);
+	}
+
+	pg_free(subname);
+	pg_free(dbname);
+	pg_free(originname);
+	pg_free(lsnstr);
+	destroyPQExpBuffer(str);
+}
+
+/*
+ * Enables the subscription.
+ *
+ * The subscription was created in a previous step but it was disabled. After
+ * adjusting the initial logical replication location, enable the subscription.
+ */
+static void
+enable_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res;
+	char	   *subname;
+
+	Assert(conn != NULL);
+
+	subname = PQescapeIdentifier(conn, dbinfo->subname, strlen(dbinfo->subname));
+
+	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
+				dbinfo->subname, dbinfo->dbname);
+
+	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", subname);
+
+	pg_log_debug("command is: %s", str->data);
+
+	if (!dry_run)
+	{
+		res = PQexec(conn, str->data);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			pg_log_error("could not enable subscription \"%s\": %s",
+						 dbinfo->subname, PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+
+		PQclear(res);
+	}
+
+	pg_free(subname);
+	destroyPQExpBuffer(str);
+}
+
+int
+main(int argc, char **argv)
+{
+	static struct option long_options[] =
+	{
+		{"database", required_argument, NULL, 'd'},
+		{"pgdata", required_argument, NULL, 'D'},
+		{"dry-run", no_argument, NULL, 'n'},
+		{"subscriber-port", required_argument, NULL, 'p'},
+		{"publisher-server", required_argument, NULL, 'P'},
+		{"socket-directory", required_argument, NULL, 's'},
+		{"recovery-timeout", required_argument, NULL, 't'},
+		{"subscriber-username", required_argument, NULL, 'U'},
+		{"verbose", no_argument, NULL, 'v'},
+		{"version", no_argument, NULL, 'V'},
+		{"help", no_argument, NULL, '?'},
+		{"config-file", required_argument, NULL, 1},
+		{"publication", required_argument, NULL, 2},
+		{"replication-slot", required_argument, NULL, 3},
+		{"subscription", required_argument, NULL, 4},
+		{NULL, 0, NULL, 0}
+	};
+
+	struct CreateSubscriberOptions opt = {0};
+
+	int			c;
+	int			option_index;
+
+	char	   *pub_base_conninfo;
+	char	   *sub_base_conninfo;
+	char	   *dbname_conninfo = NULL;
+
+	uint64		pub_sysid;
+	uint64		sub_sysid;
+	struct stat statbuf;
+
+	char	   *consistent_lsn;
+
+	char		pidfile[MAXPGPATH];
+
+	pg_logging_init(argv[0]);
+	pg_logging_set_level(PG_LOG_WARNING);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_createsubscriber"));
+
+	if (argc > 1)
+	{
+		if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
+		{
+			usage();
+			exit(0);
+		}
+		else if (strcmp(argv[1], "-V") == 0
+				 || strcmp(argv[1], "--version") == 0)
+		{
+			puts("pg_createsubscriber (PostgreSQL) " PG_VERSION);
+			exit(0);
+		}
+	}
+
+	/* Default settings */
+	subscriber_dir = NULL;
+	opt.config_file = NULL;
+	opt.pub_conninfo_str = NULL;
+	opt.socket_dir = NULL;
+	opt.sub_port = DEFAULT_SUB_PORT;
+	opt.sub_username = NULL;
+	opt.database_names = (SimpleStringList){0};
+	opt.recovery_timeout = 0;
+
+	/*
+	 * Don't allow it to be run as root. It uses pg_ctl which does not allow
+	 * it either.
+	 */
+#ifndef WIN32
+	if (geteuid() == 0)
+	{
+		pg_log_error("cannot be executed by \"root\"");
+		pg_log_error_hint("You must run %s as the PostgreSQL superuser.",
+						  progname);
+		exit(1);
+	}
+#endif
+
+	get_restricted_token();
+
+	while ((c = getopt_long(argc, argv, "d:D:np:P:s:t:U:v",
+							long_options, &option_index)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				if (!simple_string_list_member(&opt.database_names, optarg))
+				{
+					simple_string_list_append(&opt.database_names, optarg);
+					num_dbs++;
+				}
+				else
+				{
+					pg_log_error("duplicate database \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 'D':
+				subscriber_dir = pg_strdup(optarg);
+				canonicalize_path(subscriber_dir);
+				break;
+			case 'n':
+				dry_run = true;
+				break;
+			case 'p':
+				opt.sub_port = pg_strdup(optarg);
+				break;
+			case 'P':
+				opt.pub_conninfo_str = pg_strdup(optarg);
+				break;
+			case 's':
+				opt.socket_dir = pg_strdup(optarg);
+				canonicalize_path(opt.socket_dir);
+				break;
+			case 't':
+				opt.recovery_timeout = atoi(optarg);
+				break;
+			case 'U':
+				opt.sub_username = pg_strdup(optarg);
+				break;
+			case 'v':
+				pg_logging_increase_verbosity();
+				break;
+			case 1:
+				opt.config_file = pg_strdup(optarg);
+				break;
+			case 2:
+				if (!simple_string_list_member(&opt.pub_names, optarg))
+				{
+					simple_string_list_append(&opt.pub_names, optarg);
+					num_pubs++;
+				}
+				else
+				{
+					pg_log_error("duplicate publication \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 3:
+				if (!simple_string_list_member(&opt.replslot_names, optarg))
+				{
+					simple_string_list_append(&opt.replslot_names, optarg);
+					num_replslots++;
+				}
+				else
+				{
+					pg_log_error("duplicate replication slot \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			case 4:
+				if (!simple_string_list_member(&opt.sub_names, optarg))
+				{
+					simple_string_list_append(&opt.sub_names, optarg);
+					num_subs++;
+				}
+				else
+				{
+					pg_log_error("duplicate subscription \"%s\"", optarg);
+					exit(1);
+				}
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/* Any non-option arguments? */
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* Required arguments */
+	if (subscriber_dir == NULL)
+	{
+		pg_log_error("no subscriber data directory specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* If socket directory is not provided, use the current directory */
+	if (opt.socket_dir == NULL)
+	{
+		char		cwd[MAXPGPATH];
+
+		if (!getcwd(cwd, MAXPGPATH))
+			pg_fatal("could not determine current directory");
+		opt.socket_dir = pg_strdup(cwd);
+		canonicalize_path(opt.socket_dir);
+	}
+
+	/*
+	 * Parse connection string. Build a base connection string that might be
+	 * reused by multiple databases.
+	 */
+	if (opt.pub_conninfo_str == NULL)
+	{
+		/*
+		 * TODO use primary_conninfo (if available) from subscriber and
+		 * extract publisher connection string. Assume that there are
+		 * identical entries for physical and logical replication. If there is
+		 * not, we would fail anyway.
+		 */
+		pg_log_error("no publisher connection string specified");
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+	pg_log_info("validating connection string on publisher");
+	pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str,
+										  &dbname_conninfo);
+	if (pub_base_conninfo == NULL)
+		exit(1);
+
+	pg_log_info("validating connection string on subscriber");
+	sub_base_conninfo = get_sub_conninfo(&opt);
+
+	if (opt.database_names.head == NULL)
+	{
+		pg_log_info("no database was specified");
+
+		/*
+		 * If --database option is not provided, try to obtain the dbname from
+		 * the publisher conninfo. If dbname parameter is not available, error
+		 * out.
+		 */
+		if (dbname_conninfo)
+		{
+			simple_string_list_append(&opt.database_names, dbname_conninfo);
+			num_dbs++;
+
+			pg_log_info("database \"%s\" was extracted from the publisher connection string",
+						dbname_conninfo);
+		}
+		else
+		{
+			pg_log_error("no database name specified");
+			pg_log_error_hint("Try \"%s --help\" for more information.",
+							  progname);
+			exit(1);
+		}
+	}
+
+	/* Number of object names must match number of databases */
+	if (num_pubs > 0 && num_pubs != num_dbs)
+	{
+		pg_log_error("wrong number of publication names");
+		pg_log_error_hint("Number of publication names (%d) must match number of database names (%d).",
+						  num_pubs, num_dbs);
+		exit(1);
+	}
+	if (num_subs > 0 && num_subs != num_dbs)
+	{
+		pg_log_error("wrong number of subscription names");
+		pg_log_error_hint("Number of subscription names (%d) must match number of database names (%d).",
+						  num_subs, num_dbs);
+		exit(1);
+	}
+	if (num_replslots > 0 && num_replslots != num_dbs)
+	{
+		pg_log_error("wrong number of replication slot names");
+		pg_log_error_hint("Number of replication slot names (%d) must match number of database names (%d).",
+						  num_replslots, num_dbs);
+		exit(1);
+	}
+
+	/* Get the absolute path of pg_ctl and pg_resetwal on the subscriber */
+	pg_ctl_path = get_exec_path(argv[0], "pg_ctl");
+	pg_resetwal_path = get_exec_path(argv[0], "pg_resetwal");
+
+	/* Rudimentary check for a data directory */
+	check_data_directory(subscriber_dir);
+
+	/*
+	 * Store database information for publisher and subscriber. It should be
+	 * called before atexit() because its return is used in the
+	 * cleanup_objects_atexit().
+	 */
+	dbinfo = store_pub_sub_info(&opt, pub_base_conninfo, sub_base_conninfo);
+
+	/* Register a function to clean up objects in case of failure */
+	atexit(cleanup_objects_atexit);
+
+	/*
+	 * Check if the subscriber data directory has the same system identifier
+	 * than the publisher data directory.
+	 */
+	pub_sysid = get_primary_sysid(dbinfo[0].pubconninfo);
+	sub_sysid = get_standby_sysid(subscriber_dir);
+	if (pub_sysid != sub_sysid)
+		pg_fatal("subscriber data directory is not a copy of the source database cluster");
+
+	/* Subscriber PID file */
+	snprintf(pidfile, MAXPGPATH, "%s/postmaster.pid", subscriber_dir);
+
+	/*
+	 * The standby server must not be running. If the server is started under
+	 * service manager and pg_createsubscriber stops it, the service manager
+	 * might react to this action and start the server again. Therefore, refuse
+	 * to proceed if the server is running to avoid possible failures.
+	 */
+	if (stat(pidfile, &statbuf) == 0)
+	{
+		pg_log_error("standby is up and running");
+		pg_log_error_hint("Stop the standby and try again.");
+		exit(1);
+	}
+
+	/*
+	 * Start a short-lived standby server with temporary parameters (provided
+	 * by command-line options). The goal is to avoid connections during the
+	 * transformation steps.
+	 */
+	pg_log_info("starting the standby with command-line options");
+	start_standby_server(&opt, true);
+
+	/* Check if the standby server is ready for logical replication */
+	check_subscriber(dbinfo);
+
+	/*
+	 * Check if the primary server is ready for logical replication. This
+	 * routine checks if a replication slot is in use on primary so it relies
+	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
+	 * called after it.
+	 */
+	check_publisher(dbinfo);
+
+	/*
+	 * Stop the target server. The recovery process requires that the server
+	 * reaches a consistent state before targeting the recovery stop point.
+	 * Make sure a consistent state is reached (stop the target server
+	 * guarantees it) *before* creating the replication slots in
+	 * setup_publisher().
+	 */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(subscriber_dir);
+
+	/*
+	 * Create the required objects for each database on publisher. This step
+	 * is here mainly because if we stop the standby we cannot verify if the
+	 * primary slot is in use. We could use an extra connection for it but it
+	 * doesn't seem worth.
+	 */
+	consistent_lsn = setup_publisher(dbinfo);
+
+	/* Write the required recovery parameters */
+	setup_recovery(dbinfo, subscriber_dir, consistent_lsn);
+
+	/*
+	 * Start subscriber so the recovery parameters will take effect. Wait
+	 * until accepting connections.
+	 */
+	pg_log_info("starting the subscriber");
+	start_standby_server(&opt, true);
+
+	/* Waiting the subscriber to be promoted */
+	wait_for_end_recovery(dbinfo[0].subconninfo, &opt);
+
+	/*
+	 * Create the subscription for each database on subscriber. It does not
+	 * enable it immediately because it needs to adjust the replication start
+	 * point to the LSN reported by setup_publisher().  It also cleans up
+	 * publications created by this tool and replication to the standby.
+	 */
+	setup_subscriber(dbinfo, consistent_lsn);
+
+	/* Remove primary_slot_name if it exists on primary */
+	drop_primary_replication_slot(dbinfo, primary_slot_name);
+
+	/* Stop the subscriber */
+	pg_log_info("stopping the subscriber");
+	stop_standby_server(subscriber_dir);
+
+	/* Change system identifier from subscriber */
+	modify_subscriber_sysid(&opt);
+
+	success = true;
+
+	pg_log_info("Done!");
+
+	return 0;
+}
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
new file mode 100644
index 0000000000..63ae6fdfc6
--- /dev/null
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -0,0 +1,364 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+#
+# Test using a standby server as the subscriber.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+program_help_ok('pg_createsubscriber');
+program_version_ok('pg_createsubscriber');
+program_options_handling_ok('pg_createsubscriber');
+
+my $datadir = PostgreSQL::Test::Utils::tempdir;
+
+#
+# Test mandatory options
+command_fails(['pg_createsubscriber'],
+	'no subscriber data directory specified');
+command_fails(
+	[ 'pg_createsubscriber', '--pgdata', $datadir ],
+	'no publisher connection string specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432'
+	],
+	'no database name specified');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--database', 'pg1',
+		'--database', 'pg1'
+	],
+	'duplicate database name');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'duplicate publication name');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of publication names');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo2',
+		'--subscription', 'bar1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of subscription names');
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $datadir,
+		'--publisher-server', 'port=5432',
+		'--publication', 'foo1',
+		'--publication', 'foo2',
+		'--subscription', 'bar1',
+		'--subscription', 'bar2',
+		'--replication-slot', 'baz1',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'wrong number of replication slot names');
+
+# Set up node P as primary
+my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+$node_p->init(allows_streaming => 'logical');
+$node_p->start;
+
+# Set up node F as about-to-fail node
+# Force it to initialize a new cluster instead of copying a
+# previously initdb'd cluster. New cluster has a different system identifier so
+# we can test if the target cluster is a copy of the source cluster.
+my $node_f = PostgreSQL::Test::Cluster->new('node_f');
+$node_f->init(force_initdb => 1, allows_streaming => 'logical');
+
+# On node P
+# - create databases
+# - create test tables
+# - insert a row
+# - create a physical replication slot
+$node_p->safe_psql(
+	'postgres', q(
+	CREATE DATABASE pg1;
+	CREATE DATABASE pg2;
+));
+$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $slotname = 'physical_slot';
+$node_p->safe_psql('pg2',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Set up node S as standby linking to node P
+$node_p->backup('backup_1');
+my $node_s = PostgreSQL::Test::Cluster->new('node_s');
+$node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_s->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_s->set_standby_mode();
+$node_s->start;
+
+# Set up node T as standby linking to node P then promote it
+my $node_t = PostgreSQL::Test::Cluster->new('node_t');
+$node_t->init_from_backup($node_p, 'backup_1', has_streaming => 1);
+$node_t->set_standby_mode();
+$node_t->start;
+$node_t->promote;
+$node_t->stop;
+
+# Run pg_createsubscriber on a promoted server
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_t->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_t->host,
+		'--subscriber-port', $node_t->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'target server is not in recovery');
+
+# Run pg_createsubscriber when standby is running
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'standby is up and running');
+
+# Run pg_createsubscriber on about-to-fail node F
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--pgdata', $node_f->data_dir,
+		'--publisher-server', $node_p->connstr('pg1'),
+		'--socket-directory', $node_f->host,
+		'--subscriber-port', $node_f->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'subscriber data directory is not a copy of the source database cluster');
+
+# Set up node C as standby linking to node S
+$node_s->backup('backup_2');
+my $node_c = PostgreSQL::Test::Cluster->new('node_c');
+$node_c->init_from_backup($node_s, 'backup_2', has_streaming => 1);
+$node_c->adjust_conf('postgresql.conf', 'primary_slot_name', undef);
+$node_c->set_standby_mode();
+
+# Run pg_createsubscriber on node C (P -> S -> C)
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_c->data_dir, '--publisher-server',
+		$node_s->connstr('pg1'),
+		'--socket-directory', $node_c->host,
+		'--subscriber-port', $node_c->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary server is in recovery');
+
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
+# Check some unmet conditions on node P
+$node_p->append_conf('postgresql.conf', q{
+wal_level = replica
+max_replication_slots = 1
+max_wal_senders = 1
+max_worker_processes = 2
+});
+$node_p->restart;
+$node_s->stop;
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'primary contains unmet conditions on node P');
+# Restore default settings here but only apply it after testing standby. Some
+# standby settings should not be a lower setting than on the primary.
+$node_p->append_conf('postgresql.conf', q{
+wal_level = logical
+max_replication_slots = 10
+max_wal_senders = 10
+max_worker_processes = 8
+});
+
+# Check some unmet conditions on node S
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 1
+max_logical_replication_workers = 1
+max_worker_processes = 2
+});
+command_fails(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'standby contains unmet conditions on node S');
+$node_s->append_conf('postgresql.conf', q{
+max_replication_slots = 10
+max_logical_replication_workers = 4
+max_worker_processes = 8
+});
+# Restore default settings on both servers
+$node_p->restart;
+
+# dry run mode on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--publication', 'pub1',
+		'--publication', 'pub2',
+		'--subscription', 'sub1',
+		'--subscription', 'sub2',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber --dry-run on node S');
+
+# Check if node S is still a standby
+$node_s->start;
+is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'),
+	't', 'standby is in recovery');
+$node_s->stop;
+
+# pg_createsubscriber can run without --databases option
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--dry-run', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--replication-slot', 'replslot1'
+	],
+	'run pg_createsubscriber without --databases');
+
+# Run pg_createsubscriber on node S
+command_ok(
+	[
+		'pg_createsubscriber', '--verbose',
+		'--verbose', '--pgdata',
+		$node_s->data_dir, '--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory', $node_s->host,
+		'--subscriber-port', $node_s->port,
+		'--publication', 'pub1',
+		'--publication', 'Pub2',
+		'--replication-slot', 'replslot1',
+		'--replication-slot', 'replslot2',
+		'--database', 'pg1',
+		'--database', 'pg2'
+	],
+	'run pg_createsubscriber on node S');
+
+# Confirm the physical replication slot has been removed
+my $result = $node_p->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
+);
+is($result, qq(0),
+	'the physical replication slot used as primary_slot_name has been removed'
+);
+
+# Insert rows on P
+$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+
+# Start subscriber
+$node_s->start;
+
+# Get subscription names
+$result = $node_s->safe_psql(
+	'postgres', qq(
+	SELECT subname FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_'
+));
+my @subnames = split("\n", $result);
+
+# Wait subscriber to catch up
+$node_s->wait_for_subscription_sync($node_p, $subnames[0]);
+$node_s->wait_for_subscription_sync($node_p, $subnames[1]);
+
+# Check result on database pg1
+$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+is( $result, qq(first row
+second row
+third row),
+	'logical replication works on database pg1');
+
+# Check result on database pg2
+$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database pg2');
+
+# Different system identifier?
+my $sysid_p = $node_p->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+my $sysid_s = $node_s->safe_psql('postgres',
+	'SELECT system_identifier FROM pg_control_system()');
+ok($sysid_p != $sysid_s, 'system identifier was changed');
+
+# clean up
+$node_p->teardown_node;
+$node_s->teardown_node;
+$node_t->teardown_node;
+$node_f->teardown_node;
+
+done_testing();
-- 
2.43.0

v34-0002-Remove-How-it-Works-section.patchapplication/octet-stream; name=v34-0002-Remove-How-it-Works-section.patchDownload
From d96560546eada40748bc6e81e225c51bd9a8eab2 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Thu, 21 Mar 2024 00:16:21 -0300
Subject: [PATCH v34 2/4] Remove How it Works section

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 128 ----------------------
 1 file changed, 128 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index f0cfed8c47..fc890492a8 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -373,134 +373,6 @@ PostgreSQL documentation
 
  </refsect1>
 
- <refsect1>
-  <title>How It Works</title>
-
-  <para>
-    The basic idea is to have a replication start point from the source server
-    and set up a logical replication to start from this point:
-  </para>
-
-  <procedure>
-   <step>
-    <para>
-     Start the target server with the specified command-line options. If the
-     target server is running, <application>pg_createsubscriber</application>
-     will terminate with an error.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Check if the target server can be converted. There are also a few checks
-     on the source server. If any of the prerequisites are not met,
-     <application>pg_createsubscriber</application> will terminate with an
-     error.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Create a publication and replication slot for each specified database on
-     the source server. Each publication is created using
-     <link linkend="sql-createpublication-params-for-all-tables"><literal>FOR ALL TABLES</literal></link>.
-     If <option>publication-name</option> option is not specified, it has the
-     following name pattern:
-     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameter:
-     database <parameter>oid</parameter>, random <parameter>int</parameter>).
-     If <option>replication-slot-name</option> is not specified, the
-     replication slot has the following name pattern:
-     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
-     database <parameter>oid</parameter>, random <parameter>int</parameter>).
-     These replication slots will be used by the subscriptions in a future step.
-     The last replication slot LSN is used as a stopping point in the
-     <xref linkend="guc-recovery-target-lsn"/> parameter and by the
-     subscriptions as a replication start point. It guarantees that no
-     transaction will be lost.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Write recovery parameters into the target data directory and restart the
-     target server. It specifies an LSN (<xref linkend="guc-recovery-target-lsn"/>)
-     of the write-ahead log location up to which recovery will proceed. It also
-     specifies <literal>promote</literal> as the action that the server should
-     take once the recovery target is reached. Additional
-     <link linkend="runtime-config-wal-recovery-target">recovery parameters</link>
-     are added to avoid unexpected behavior during the recovery process such as
-     end of the recovery as soon as a consistent state is reached (WAL should
-     be applied until the replication start location) and multiple recovery
-     targets that can cause a failure. This step finishes once the server ends
-     standby mode and is accepting read-write transactions.
-     If <option>--recovery-timeout</option> option is set,
-     <application>pg_createsubscriber</application> terminates if recovery does
-     not end until the given number of seconds.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Create a subscription for each specified database on the target server.
-     If <option>subscription-name</option> is not specified, the subscription
-     has the following name pattern:
-     <quote><literal>pg_createsubscriber_%u_%x</literal></quote> (parameters:
-     database <parameter>oid</parameter>, random <parameter>int</parameter>).
-     It does not copy existing data from the source server. It does not create
-     a replication slot. Instead, it uses the replication slot that was created
-     in a previous step. The subscription is created but it is not enabled yet.
-     The reason is the replication progress must be set to the replication
-     start point before starting the replication.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Drop publications on the target server that were replicated because they
-     were created before the replication start location. It has no use on the
-     subscriber.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Set the replication progress to the replication start point for each
-     subscription. When the target server starts the recovery process, it
-     catches up to the replication start point. This is the exact LSN to be used
-     as a initial replication location for each subscription. The replication
-     origin name is obtained since the subscription was created. The replication
-     origin name and the replication start point are used in
-     <link linkend="pg-replication-origin-advance"><function>pg_replication_origin_advance()</function></link>
-     to set up the initial replication location.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Enable the subscription for each specified database on the target server.
-     The subscription starts applying transactions from the replication start
-     point.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     If the standby server was using
-     <link linkend="guc-primary-slot-name"><varname>primary_slot_name</varname></link>,
-     it has no use from now on so drop it.
-    </para>
-   </step>
-
-   <step>
-    <para>
-     Update the system identifier on the target server. The
-     <xref linkend="app-pgresetwal"/> is run to modify the system identifier.
-     The target server is stopped as a <command>pg_resetwal</command> requirement.
-    </para>
-   </step>
-  </procedure>
- </refsect1>
-
  <refsect1>
   <title>Examples</title>
 
-- 
2.43.0

v34-0003-Check-both-servers-before-exiting.patchapplication/octet-stream; name=v34-0003-Check-both-servers-before-exiting.patchDownload
From dad4ae45515e566477c5574232a7ced1f957a89a Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Thu, 21 Mar 2024 23:38:11 -0300
Subject: [PATCH v34 3/4] Check both servers before exiting

The current code provides multiple errors for each server. If any
subscriber condition is not met, it terminates without checking the
publisher conditions.
This change allows it to check both servers before terminating if any
condition is not met. It saves at least one dry run execution.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 24 ++++++++++++---------
 1 file changed, 14 insertions(+), 10 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 3a8240ea78..1139c15896 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -76,9 +76,9 @@ static uint64 get_standby_sysid(const char *datadir);
 static void modify_subscriber_sysid(const struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static char *generate_object_name(PGconn *conn);
-static void check_publisher(const struct LogicalRepInfo *dbinfo);
+static bool check_publisher(const struct LogicalRepInfo *dbinfo);
 static char *setup_publisher(struct LogicalRepInfo *dbinfo);
-static void check_subscriber(const struct LogicalRepInfo *dbinfo);
+static bool check_subscriber(const struct LogicalRepInfo *dbinfo);
 static void setup_subscriber(struct LogicalRepInfo *dbinfo,
 							 const char *consistent_lsn);
 static void setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir,
@@ -798,7 +798,7 @@ server_is_in_recovery(PGconn *conn)
  *
  * XXX Does it not allow a synchronous replica?
  */
-static void
+static bool
 check_publisher(const struct LogicalRepInfo *dbinfo)
 {
 	PGconn	   *conn;
@@ -939,8 +939,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 		failed = true;
 	}
 
-	if (failed)
-		exit(1);
+	return failed;
 }
 
 /*
@@ -954,7 +953,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
  * there is not a reliable way to provide a suitable error saying the server C
  * will be broken at the end of this process (due to pg_resetwal).
  */
-static void
+static bool
 check_subscriber(const struct LogicalRepInfo *dbinfo)
 {
 	PGconn	   *conn;
@@ -1045,8 +1044,7 @@ check_subscriber(const struct LogicalRepInfo *dbinfo)
 		failed = true;
 	}
 
-	if (failed)
-		exit(1);
+	return failed;
 }
 
 /*
@@ -1763,6 +1761,9 @@ main(int argc, char **argv)
 
 	char		pidfile[MAXPGPATH];
 
+	bool		failed_pub = false;
+	bool		failed_sub = false;
+
 	pg_logging_init(argv[0]);
 	pg_logging_set_level(PG_LOG_WARNING);
 	progname = get_progname(argv[0]);
@@ -2051,7 +2052,7 @@ main(int argc, char **argv)
 	start_standby_server(&opt, true);
 
 	/* Check if the standby server is ready for logical replication */
-	check_subscriber(dbinfo);
+	failed_sub = check_subscriber(dbinfo);
 
 	/*
 	 * Check if the primary server is ready for logical replication. This
@@ -2059,7 +2060,10 @@ main(int argc, char **argv)
 	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
 	 * called after it.
 	 */
-	check_publisher(dbinfo);
+	failed_pub = check_publisher(dbinfo);
+
+	if (failed_pub || failed_sub)
+		exit(1);
 
 	/*
 	 * Stop the target server. The recovery process requires that the server
-- 
2.43.0

v34-0004-Free-malloc-d-memory-if-no-variables-could-be-re.patchapplication/octet-stream; name=v34-0004-Free-malloc-d-memory-if-no-variables-could-be-re.patchDownload
From 8f77e86f29f5bdc0365a83abced96b2ab746747e Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 25 Mar 2024 07:38:06 +0000
Subject: [PATCH v34 4/4] Free malloc'd memory if no variables could be
 referred the region

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1139c15896..392eec11b0 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -656,6 +656,7 @@ modify_subscriber_sysid(const struct CreateSubscriberOptions *opt)
 			pg_fatal("subscriber failed to change system identifier: exit code: %d", rc);
 	}
 
+	pg_free(cmd_str);
 	pg_free(cf);
 }
 
@@ -762,6 +763,7 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 			exit(1);
 
 		disconnect_database(conn, false);
+		pg_free(genname);
 	}
 
 	return lsn;
@@ -910,6 +912,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 			pg_log_info("primary has replication slot \"%s\"",
 						primary_slot_name);
 
+		destroyPQExpBuffer(str);
 		PQclear(res);
 	}
 
@@ -939,6 +942,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 		failed = true;
 	}
 
+	pg_free(wal_level);
 	return failed;
 }
 
@@ -1135,6 +1139,7 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	disconnect_database(conn, false);
 
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+	destroyPQExpBuffer(recoveryconfcontents);
 }
 
 /*
@@ -1330,6 +1335,7 @@ stop_standby_server(const char *datadir)
 	pg_ctl_status(pg_ctl_cmd, rc);
 	standby_running = false;
 	pg_log_info("server was stopped");
+	pg_free(pg_ctl_cmd);
 }
 
 /*
-- 
2.43.0

#235Peter Eisentraut
peter@eisentraut.org
In reply to: Euler Taveira (#226)
Re: speed up a logical replica setup

On 22.03.24 04:31, Euler Taveira wrote:

On Thu, Mar 21, 2024, at 6:49 AM, Shlok Kyal wrote:

There is a compilation error while building postgres with the patch
due to a recent commit. I have attached a top-up patch v32-0003 to
resolve this compilation error.
I have not updated the version of the patch as I have not made any
change in v32-0001 and v32-0002 patch.

I'm attaching a new version (v33) to incorporate this fix (v32-0003)
into the
main patch (v32-0001). This version also includes 2 new tests:

- refuse to run if the standby server is running
- refuse to run if the standby was promoted e.g. it is not in recovery

The first one exercises a recent change (standby should be stopped) and the
second one covers an important requirement.

I have committed your version v33. I did another pass over the
identifier and literal quoting. I added quoting for replication slot
names, for example, even though they can only contain a restricted set
of characters, but it felt better to be defensive there.

I'm happy to entertain follow-up patches on some of the details like
option naming that were still being discussed. I just wanted to get the
main functionality in in good time. We can fine-tune the rest over the
next few weeks.

Based on the discussion [1] about the check functions, Vignesh suggested
that it
should check both server before exiting. v33-0003 implements it. I don't
have a
strong preference; feel free to apply it.

I haven't done anything about this.

#236Bharath Rupireddy
bharath.rupireddyforpostgres@gmail.com
In reply to: Peter Eisentraut (#235)
Re: speed up a logical replica setup

On Mon, Mar 25, 2024 at 5:25 PM Peter Eisentraut <peter@eisentraut.org> wrote:

I have committed your version v33.

Looks like BF animals aren't happy, please check -
https://buildfarm.postgresql.org/cgi-bin/show_failures.pl.

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

#237Peter Eisentraut
peter@eisentraut.org
In reply to: Bharath Rupireddy (#236)
Re: speed up a logical replica setup

On 25.03.24 13:36, Bharath Rupireddy wrote:

On Mon, Mar 25, 2024 at 5:25 PM Peter Eisentraut <peter@eisentraut.org> wrote:

I have committed your version v33.

Looks like BF animals aren't happy, please check -
https://buildfarm.postgresql.org/cgi-bin/show_failures.pl.

Looks like sanitizer failures. There were a few messages about that
recently, but those were all just about freeing memory after use, which
we don't necessarily require for client programs. So maybe something else.

#238Euler Taveira
euler@eulerto.com
In reply to: Peter Eisentraut (#235)
Re: speed up a logical replica setup

On Mon, Mar 25, 2024, at 8:55 AM, Peter Eisentraut wrote:

On 22.03.24 04:31, Euler Taveira wrote:

On Thu, Mar 21, 2024, at 6:49 AM, Shlok Kyal wrote:

There is a compilation error while building postgres with the patch
due to a recent commit. I have attached a top-up patch v32-0003 to
resolve this compilation error.
I have not updated the version of the patch as I have not made any
change in v32-0001 and v32-0002 patch.

I'm attaching a new version (v33) to incorporate this fix (v32-0003)
into the
main patch (v32-0001). This version also includes 2 new tests:

- refuse to run if the standby server is running
- refuse to run if the standby was promoted e.g. it is not in recovery

The first one exercises a recent change (standby should be stopped) and the
second one covers an important requirement.

I have committed your version v33. I did another pass over the
identifier and literal quoting. I added quoting for replication slot
names, for example, even though they can only contain a restricted set
of characters, but it felt better to be defensive there.

Thanks.

I'm happy to entertain follow-up patches on some of the details like
option naming that were still being discussed. I just wanted to get the
main functionality in in good time. We can fine-tune the rest over the
next few weeks.

Agree. Let's continue the discussion about the details.

Based on the discussion [1] about the check functions, Vignesh suggested
that it
should check both server before exiting. v33-0003 implements it. I don't
have a
strong preference; feel free to apply it.

I haven't done anything about this.

... including this one.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#239Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Peter Eisentraut (#237)
RE: speed up a logical replica setup

Dear Bharath, Peter,

Looks like BF animals aren't happy, please check -

https://buildfarm.postgresql.org/cgi-bin/show_failures.pl.

Looks like sanitizer failures. There were a few messages about that
recently, but those were all just about freeing memory after use, which
we don't necessarily require for client programs. So maybe something else.

It seems that there are several time of failures, [1]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=serinus&amp;dt=2024-03-25%2013%3A03%3A07 and [2]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=culicidae&amp;dt=2024-03-25%2013%3A53%3A58.

## Analysis for failure 1

The failure caused by a time lag between walreceiver finishes and pg_is_in_recovery()
returns true.

According to the output [1]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=serinus&amp;dt=2024-03-25%2013%3A03%3A07, it seems that the tool failed at wait_for_end_recovery()
with the message "standby server disconnected from the primary". Also, lines
"redo done at..." and "terminating walreceiver process due to administrator command"
meant that walreceiver was requested to shut down by XLogShutdownWalRcv().

According to the source, we confirm that walreceiver is shut down in
StartupXLOG()->FinishWalRecovery()->XLogShutdownWalRcv(). Also, SharedRecoveryState
is changed to RECOVERY_STATE_DONE (this meant the pg_is_in_recovery() return true)
at the latter part of StartupXLOG().

So, if there is a delay between FinishWalRecovery() and change the state, the check
in wait_for_end_recovery() would be failed during the time. Since we allow to miss
the walreceiver 10 times and it is checked once per second, the failure occurs if
the time lag is longer than 10 seconds.

I do not have a good way to fix it. One approach is make NUM_CONN_ATTEMPTS larger,
but it's not a fundamental solution.

## Analysis for failure 2

According to [2]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=culicidae&amp;dt=2024-03-25%2013%3A53%3A58, the physical replication slot which is specified as primary_slot_name
was not used by the walsender process. At that time walsender has not existed.

```
...
pg_createsubscriber: publisher: current wal senders: 0
pg_createsubscriber: command is: SELECT 1 FROM pg_catalog.pg_replication_slots WHERE active AND slot_name = 'physical_slot'
pg_createsubscriber: error: could not obtain replication slot information: got 0 rows, expected 1 row
...
```

Currently standby must be stopped before the command and current code does not
block the flow to ensure the replication is started. So there is a possibility
that the checking is run before walsender is launched.

One possible approach is to wait until the replication starts. Alternative one is
to ease the condition.

How do you think?

[1]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=serinus&amp;dt=2024-03-25%2013%3A03%3A07
[2]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=culicidae&amp;dt=2024-03-25%2013%3A53%3A58

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#240vignesh C
vignesh21@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#239)
Re: speed up a logical replica setup

On Mon, 25 Mar 2024 at 21:36, Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear Bharath, Peter,

Looks like BF animals aren't happy, please check -

https://buildfarm.postgresql.org/cgi-bin/show_failures.pl.

Looks like sanitizer failures. There were a few messages about that
recently, but those were all just about freeing memory after use, which
we don't necessarily require for client programs. So maybe something else.

It seems that there are several time of failures, [1] and [2].

## Analysis for failure 1

The failure caused by a time lag between walreceiver finishes and pg_is_in_recovery()
returns true.

According to the output [1], it seems that the tool failed at wait_for_end_recovery()
with the message "standby server disconnected from the primary". Also, lines
"redo done at..." and "terminating walreceiver process due to administrator command"
meant that walreceiver was requested to shut down by XLogShutdownWalRcv().

According to the source, we confirm that walreceiver is shut down in
StartupXLOG()->FinishWalRecovery()->XLogShutdownWalRcv(). Also, SharedRecoveryState
is changed to RECOVERY_STATE_DONE (this meant the pg_is_in_recovery() return true)
at the latter part of StartupXLOG().

So, if there is a delay between FinishWalRecovery() and change the state, the check
in wait_for_end_recovery() would be failed during the time. Since we allow to miss
the walreceiver 10 times and it is checked once per second, the failure occurs if
the time lag is longer than 10 seconds.

I do not have a good way to fix it. One approach is make NUM_CONN_ATTEMPTS larger,
but it's not a fundamental solution.

I agree with your analysis, another way to fix could be to remove the
following check as increasing the count might still have the race
condition issue:
/*
* If it is still in recovery, make sure the target server is
* connected to the primary so it can receive the required WAL to
* finish the recovery process. If it is disconnected try
* NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
*/
res = PQexec(conn,
"SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");

I'm not sure whether we should worry about the condition where
recovery is not done and pg_stat_wal_receiver is exited as we have the
following sanity check in check_subscriber before we wait for recovery
to be finished:
/* The target server must be a standby */
if (!server_is_in_recovery(conn))
{
pg_log_error("target server must be a standby");
disconnect_database(conn, true);
}

Regards,
Vignesh

#241Amit Kapila
amit.kapila16@gmail.com
In reply to: Peter Eisentraut (#235)
Re: speed up a logical replica setup

On Mon, Mar 25, 2024 at 5:25 PM Peter Eisentraut <peter@eisentraut.org> wrote:

I have committed your version v33. I did another pass over the
identifier and literal quoting. I added quoting for replication slot
names, for example, even though they can only contain a restricted set
of characters, but it felt better to be defensive there.

I'm happy to entertain follow-up patches on some of the details like
option naming that were still being discussed. I just wanted to get the
main functionality in in good time. We can fine-tune the rest over the
next few weeks.

I was looking at prior discussions on this topic to see if there are
any other open design points apart from this and noticed that the
points raised/discussed in the email [1]/messages/by-id/CAExHW5t4ew7ZrgcDdTv7YmuG7LVQT1ZaEny_EvtngHtEBNyjcQ@mail.gmail.com are also not addressed. IIRC,
the key point we discussed was that after promotion, the existing
replication objects should be removed (either optionally or always),
otherwise, it can lead to a new subscriber not being able to restart
or getting some unwarranted data.

[1]: /messages/by-id/CAExHW5t4ew7ZrgcDdTv7YmuG7LVQT1ZaEny_EvtngHtEBNyjcQ@mail.gmail.com

--
With Regards,
Amit Kapila.

#242Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#239)
2 attachment(s)
Re: speed up a logical replica setup

On Mon, Mar 25, 2024, at 1:06 PM, Hayato Kuroda (Fujitsu) wrote:

## Analysis for failure 1

The failure caused by a time lag between walreceiver finishes and pg_is_in_recovery()
returns true.

According to the output [1], it seems that the tool failed at wait_for_end_recovery()
with the message "standby server disconnected from the primary". Also, lines
"redo done at..." and "terminating walreceiver process due to administrator command"
meant that walreceiver was requested to shut down by XLogShutdownWalRcv().

According to the source, we confirm that walreceiver is shut down in
StartupXLOG()->FinishWalRecovery()->XLogShutdownWalRcv(). Also, SharedRecoveryState
is changed to RECOVERY_STATE_DONE (this meant the pg_is_in_recovery() return true)
at the latter part of StartupXLOG().

So, if there is a delay between FinishWalRecovery() and change the state, the check
in wait_for_end_recovery() would be failed during the time. Since we allow to miss
the walreceiver 10 times and it is checked once per second, the failure occurs if
the time lag is longer than 10 seconds.

I do not have a good way to fix it. One approach is make NUM_CONN_ATTEMPTS larger,
but it's not a fundamental solution.

I was expecting that slow hosts might have issues in wait_for_end_recovery().
As you said it took a lot of steps between FinishWalRecovery() (where
walreceiver is shutdown -- XLogShutdownWalRcv) and SharedRecoveryState is set to
RECOVERY_STATE_DONE. If this window takes longer than NUM_CONN_ATTEMPTS *
WAIT_INTERVAL (10 seconds), it aborts the execution. That's a bad decision
because it already finished the promotion and it is just doing the final
preparation for the host to become a primary.

/*
* If it is still in recovery, make sure the target server is
* connected to the primary so it can receive the required WAL to
* finish the recovery process. If it is disconnected try
* NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
*/
res = PQexec(conn,
"SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
if (PQntuples(res) == 0)
{
if (++count > NUM_CONN_ATTEMPTS)
{
stop_standby_server(subscriber_dir);
pg_log_error("standby server disconnected from the primary");
break;
}
}
else
count = 0; /* reset counter if it connects again */

This code was add to defend against the death/crash of the target server. There
are at least 3 options:

(1) increase NUM_CONN_ATTEMPTS * WAIT_INTERVAL seconds. We discussed this constant
and I decided to use 10 seconds because even in some slow hosts, this time
wasn't reached during my tests. It seems I forgot to test the combination of slow
host, asserts enabled, and ubsan. I didn't notice that pg_promote() uses 60
seconds as default wait. Maybe that's a reasonable value. I checked the
004_timeline_switch test and the last run took: 39.2s (serinus), 33.1s
(culicidae), 18.31s (calliphoridae) and 27.52s (olingo).

(2) check if the primary is not running when walreceiver is not available on the
target server. Increase the connection attempts iif the primary is not running.
Hence, the described case doesn't cause an increment on the count variable.

(3) set recovery_timeout default to != 0 and remove pg_stat_wal_receiver check
protection against the death/crash target server. I explained in a previous
message that timeout may occur in cases that WAL replay to reach consistent
state takes more than recovery-timeout seconds.

Option (1) is the easiest fix, however, we can have the same issue again if a
slow host decides to be even slower, hence, we have to adjust this value again.
Option (2) interprets the walreceiver absence as a recovery end and if the
primary server is running it can indicate that the target server is in the
imminence of the recovery end. Option (3) is not as resilient as the other
options.

The first patch implements a combination of (1) and (2).

## Analysis for failure 2

According to [2], the physical replication slot which is specified as primary_slot_name
was not used by the walsender process. At that time walsender has not existed.

```
...
pg_createsubscriber: publisher: current wal senders: 0
pg_createsubscriber: command is: SELECT 1 FROM pg_catalog.pg_replication_slots WHERE active AND slot_name = 'physical_slot'
pg_createsubscriber: error: could not obtain replication slot information: got 0 rows, expected 1 row
...
```

Currently standby must be stopped before the command and current code does not
block the flow to ensure the replication is started. So there is a possibility
that the checking is run before walsender is launched.

One possible approach is to wait until the replication starts. Alternative one is
to ease the condition.

That's my suggestion too. I reused NUM_CONN_ATTEMPTS (that was renamed to
NUM_ATTEMPTS in the first patch). See second patch.

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

v1-0001-Improve-the-code-that-checks-if-the-recovery-is-f.patchtext/x-patch; name="=?UTF-8?Q?v1-0001-Improve-the-code-that-checks-if-the-recovery-is-f.patc?= =?UTF-8?Q?h?="Download
From b2cfa13627bd844b28fe050ffb35e8b9696fdf2e Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 25 Mar 2024 22:01:52 -0300
Subject: [PATCH v1 1/2] Improve the code that checks if the recovery is
 finishing

The recovery process has a window between the walreceiver shutdown and
the pg_is_in_recovery function returns false. It means that the
pg_stat_wal_receiver checks can cause the server to finish the recovery
(even if it already reaches the recovery target). Since it checks the
pg_stat_wal_receiver to verify the primary is available, if it does not
return a row, PQping the primary server. If it is up and running, it
can indicate that the target server is finishing the recovery process,
hence, we shouldn't count it as an attempt. It avoids premature failures
on slow hosts.

While on it, increase the number of attempts (10 to 60). The wait time is
the same pg_promote function uses by default.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 30 +++++++++++++--------
 1 file changed, 19 insertions(+), 11 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index b8f8269340..cca93d8c25 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -30,6 +30,8 @@
 
 #define	DEFAULT_SUB_PORT	"50432"
 
+#define NUM_ATTEMPTS		60
+
 /* Command-line options */
 struct CreateSubscriberOptions
 {
@@ -93,7 +95,7 @@ static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
 static void start_standby_server(const struct CreateSubscriberOptions *opt,
 								 bool restricted_access);
 static void stop_standby_server(const char *datadir);
-static void wait_for_end_recovery(const char *conninfo,
+static void wait_for_end_recovery(const struct LogicalRepInfo *dbinfo,
 								  const struct CreateSubscriberOptions *opt);
 static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
@@ -1354,18 +1356,16 @@ stop_standby_server(const char *datadir)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions *opt)
+wait_for_end_recovery(const struct LogicalRepInfo *dbinfo, const struct CreateSubscriberOptions *opt)
 {
 	PGconn	   *conn;
 	int			status = POSTMASTER_STILL_STARTING;
 	int			timer = 0;
 	int			count = 0;		/* number of consecutive connection attempts */
 
-#define NUM_CONN_ATTEMPTS	10
-
 	pg_log_info("waiting for the target server to reach the consistent state");
 
-	conn = connect_database(conninfo, true);
+	conn = connect_database(dbinfo->subconninfo, true);
 
 	for (;;)
 	{
@@ -1384,16 +1384,24 @@ wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions
 		}
 
 		/*
-		 * If it is still in recovery, make sure the target server is
-		 * connected to the primary so it can receive the required WAL to
-		 * finish the recovery process. If it is disconnected try
-		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 * If it is still in recovery, make sure the target server is connected
+		 * to the primary so it can receive the required WAL to finish the
+		 * recovery process. If the walreceiver process is not running it
+		 * should indicate that (i) the recovery is almost finished or (ii) the
+		 * primary is not running or is not accpeting connections. It should
+		 * count as attempts iif (ii) is true. In this case, try NUM_ATTEMPTS
+		 * in a row and bail out if not succeed.
 		 */
 		res = PQexec(conn,
 					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
 		if (PQntuples(res) == 0)
 		{
-			if (++count > NUM_CONN_ATTEMPTS)
+			if (PQping(dbinfo->pubconninfo) != PQPING_OK)
+				count++;
+			else
+				count = 0;		/* reset counter if it connects again */
+
+			if (count > NUM_ATTEMPTS)
 			{
 				stop_standby_server(subscriber_dir);
 				pg_log_error("standby server disconnected from the primary");
@@ -2113,7 +2121,7 @@ main(int argc, char **argv)
 	start_standby_server(&opt, true);
 
 	/* Waiting the subscriber to be promoted */
-	wait_for_end_recovery(dbinfo[0].subconninfo, &opt);
+	wait_for_end_recovery(&dbinfo[0], &opt);
 
 	/*
 	 * Create the subscription for each database on subscriber. It does not
-- 
2.30.2

v1-0002-Improve-the-code-that-checks-if-the-primary-slot-.patchtext/x-patch; name="=?UTF-8?Q?v1-0002-Improve-the-code-that-checks-if-the-primary-slot-.patc?= =?UTF-8?Q?h?="Download
From 872510ed67b91cb7482dea3bce831549d3519e80 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 25 Mar 2024 23:25:30 -0300
Subject: [PATCH v1 2/2] Improve the code that checks if the primary slot is
 available

The target server is started a few instructions before checking the
primary server and the replication slot might not be active. Instead of
failing the first time, try a few times.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 35 +++++++++++++++------
 1 file changed, 25 insertions(+), 10 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index cca93d8c25..f17e9bde9c 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -887,6 +887,8 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 	{
 		PQExpBuffer str = createPQExpBuffer();
 		char	   *psn_esc = PQescapeLiteral(conn, primary_slot_name, strlen(primary_slot_name));
+		int			ntuples;
+		int			count = 0;
 
 		appendPQExpBuffer(str,
 						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
@@ -897,25 +899,38 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 
 		pg_log_debug("command is: %s", str->data);
 
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		/*
+		 * The replication slot might take some time to be active, try a few
+		 * times if necessary.
+		 */
+		do
 		{
-			pg_log_error("could not obtain replication slot information: %s",
-						 PQresultErrorMessage(res));
-			disconnect_database(conn, true);
-		}
+			res = PQexec(conn, str->data);
+			if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			{
+				pg_log_error("could not obtain replication slot information: %s",
+							 PQresultErrorMessage(res));
+				disconnect_database(conn, true);
+			}
 
-		if (PQntuples(res) != 1)
+			ntuples = PQntuples(res);
+			PQclear(res);
+
+			if (ntuples == 1)	/* replication slot is already active */
+				break;
+			else
+				count++;
+		} while (count > NUM_ATTEMPTS);
+
+		if (ntuples != 1)
 		{
 			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
-						 PQntuples(res), 1);
+						 ntuples, 1);
 			disconnect_database(conn, true);
 		}
 		else
 			pg_log_info("primary has replication slot \"%s\"",
 						primary_slot_name);
-
-		PQclear(res);
 	}
 
 	disconnect_database(conn, false);
-- 
2.30.2

#243Euler Taveira
euler@eulerto.com
In reply to: Amit Kapila (#241)
Re: speed up a logical replica setup

On Mon, Mar 25, 2024, at 11:33 PM, Amit Kapila wrote:

On Mon, Mar 25, 2024 at 5:25 PM Peter Eisentraut <peter@eisentraut.org> wrote:

I have committed your version v33. I did another pass over the
identifier and literal quoting. I added quoting for replication slot
names, for example, even though they can only contain a restricted set
of characters, but it felt better to be defensive there.

I'm happy to entertain follow-up patches on some of the details like
option naming that were still being discussed. I just wanted to get the
main functionality in in good time. We can fine-tune the rest over the
next few weeks.

I was looking at prior discussions on this topic to see if there are
any other open design points apart from this and noticed that the
points raised/discussed in the email [1] are also not addressed. IIRC,
the key point we discussed was that after promotion, the existing
replication objects should be removed (either optionally or always),
otherwise, it can lead to a new subscriber not being able to restart
or getting some unwarranted data.

See setup_subscriber.

/*
* Since the publication was created before the consistent LSN, it is
* available on the subscriber when the physical replica is promoted.
* Remove publications from the subscriber because it has no use.
*/
drop_publication(conn, &dbinfo[i]);

--
Euler Taveira
EDB https://www.enterprisedb.com/

#244Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#243)
Re: speed up a logical replica setup

On Tue, Mar 26, 2024 at 8:27 AM Euler Taveira <euler@eulerto.com> wrote:

On Mon, Mar 25, 2024, at 11:33 PM, Amit Kapila wrote:

On Mon, Mar 25, 2024 at 5:25 PM Peter Eisentraut <peter@eisentraut.org> wrote:

I have committed your version v33. I did another pass over the
identifier and literal quoting. I added quoting for replication slot
names, for example, even though they can only contain a restricted set
of characters, but it felt better to be defensive there.

I'm happy to entertain follow-up patches on some of the details like
option naming that were still being discussed. I just wanted to get the
main functionality in in good time. We can fine-tune the rest over the
next few weeks.

I was looking at prior discussions on this topic to see if there are
any other open design points apart from this and noticed that the
points raised/discussed in the email [1] are also not addressed. IIRC,
the key point we discussed was that after promotion, the existing
replication objects should be removed (either optionally or always),
otherwise, it can lead to a new subscriber not being able to restart
or getting some unwarranted data.

See setup_subscriber.

/*
* Since the publication was created before the consistent LSN, it is
* available on the subscriber when the physical replica is promoted.
* Remove publications from the subscriber because it has no use.
*/
drop_publication(conn, &dbinfo[I]);

This only drops the publications created by this tool, not the
pre-existing ones that we discussed in the link provided.

--
With Regards,
Amit Kapila.

#245Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Amit Kapila (#244)
RE: speed up a logical replica setup

Dear Amit, Euler,

This only drops the publications created by this tool, not the
pre-existing ones that we discussed in the link provided.

Another concern around here is the case which primary subscribes changes from others.
After the conversion, new subscriber also tries to connect to another publisher as
well - this may lead conflicts. This causes because both launcher/workers start
after recovery finishes. So, based on the Ashutosh's point, should we remove
such replication objects?

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#246Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Euler Taveira (#242)
Re: speed up a logical replica setup

On 3/26/24 03:53, Euler Taveira wrote:

On Mon, Mar 25, 2024, at 1:06 PM, Hayato Kuroda (Fujitsu) wrote:

## Analysis for failure 1

The failure caused by a time lag between walreceiver finishes and pg_is_in_recovery()
returns true.

According to the output [1], it seems that the tool failed at wait_for_end_recovery()
with the message "standby server disconnected from the primary". Also, lines
"redo done at..." and "terminating walreceiver process due to administrator command"
meant that walreceiver was requested to shut down by XLogShutdownWalRcv().

According to the source, we confirm that walreceiver is shut down in
StartupXLOG()->FinishWalRecovery()->XLogShutdownWalRcv(). Also, SharedRecoveryState
is changed to RECOVERY_STATE_DONE (this meant the pg_is_in_recovery() return true)
at the latter part of StartupXLOG().

So, if there is a delay between FinishWalRecovery() and change the state, the check
in wait_for_end_recovery() would be failed during the time. Since we allow to miss
the walreceiver 10 times and it is checked once per second, the failure occurs if
the time lag is longer than 10 seconds.

I do not have a good way to fix it. One approach is make NUM_CONN_ATTEMPTS larger,
but it's not a fundamental solution.

I was expecting that slow hosts might have issues in wait_for_end_recovery().
As you said it took a lot of steps between FinishWalRecovery() (where
walreceiver is shutdown -- XLogShutdownWalRcv) and SharedRecoveryState is set to
RECOVERY_STATE_DONE. If this window takes longer than NUM_CONN_ATTEMPTS *
WAIT_INTERVAL (10 seconds), it aborts the execution. That's a bad decision
because it already finished the promotion and it is just doing the final
preparation for the host to become a primary.

/*
* If it is still in recovery, make sure the target server is
* connected to the primary so it can receive the required WAL to
* finish the recovery process. If it is disconnected try
* NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
*/
res = PQexec(conn,
"SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
if (PQntuples(res) == 0)
{
if (++count > NUM_CONN_ATTEMPTS)
{
stop_standby_server(subscriber_dir);
pg_log_error("standby server disconnected from the primary");
break;
}
}
else
count = 0; /* reset counter if it connects again */

This code was add to defend against the death/crash of the target server. There
are at least 3 options:

(1) increase NUM_CONN_ATTEMPTS * WAIT_INTERVAL seconds. We discussed this constant
and I decided to use 10 seconds because even in some slow hosts, this time
wasn't reached during my tests. It seems I forgot to test the combination of slow
host, asserts enabled, and ubsan. I didn't notice that pg_promote() uses 60
seconds as default wait. Maybe that's a reasonable value. I checked the
004_timeline_switch test and the last run took: 39.2s (serinus), 33.1s
(culicidae), 18.31s (calliphoridae) and 27.52s (olingo).

(2) check if the primary is not running when walreceiver is not

available on the

target server. Increase the connection attempts iif the primary is not running.
Hence, the described case doesn't cause an increment on the count variable.

(3) set recovery_timeout default to != 0 and remove pg_stat_wal_receiver check
protection against the death/crash target server. I explained in a previous
message that timeout may occur in cases that WAL replay to reach consistent
state takes more than recovery-timeout seconds.

Option (1) is the easiest fix, however, we can have the same issue again if a
slow host decides to be even slower, hence, we have to adjust this value again.
Option (2) interprets the walreceiver absence as a recovery end and if the
primary server is running it can indicate that the target server is in the
imminence of the recovery end. Option (3) is not as resilient as the other
options.

The first patch implements a combination of (1) and (2).

## Analysis for failure 2

According to [2], the physical replication slot which is specified as primary_slot_name
was not used by the walsender process. At that time walsender has not existed.

```
...
pg_createsubscriber: publisher: current wal senders: 0
pg_createsubscriber: command is: SELECT 1 FROM pg_catalog.pg_replication_slots WHERE active AND slot_name = 'physical_slot'
pg_createsubscriber: error: could not obtain replication slot information: got 0 rows, expected 1 row
...
```

Currently standby must be stopped before the command and current code does not
block the flow to ensure the replication is started. So there is a possibility
that the checking is run before walsender is launched.

One possible approach is to wait until the replication starts. Alternative one is
to ease the condition.

That's my suggestion too. I reused NUM_CONN_ATTEMPTS (that was renamed to
NUM_ATTEMPTS in the first patch). See second patch.

Perhaps I'm missing something, but why is NUM_CONN_ATTEMPTS even needed?
Why isn't recovery_timeout enough to decide if wait_for_end_recovery()
waited long enough?

IMHO the test should simply pass PG_TEST_DEFAULT_TIMEOUT when calling
pg_createsubscriber, and that should do the trick.

Increasing PG_TEST_DEFAULT_TIMEOUT is what buildfarm animals doing
things like ubsan/valgrind already use to deal with exactly this kind of
timeout problem.

Or is there a deeper problem with deciding if the system is in recovery?

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#247Euler Taveira
euler@eulerto.com
In reply to: Tomas Vondra (#246)
Re: speed up a logical replica setup

On Tue, Mar 26, 2024, at 4:12 PM, Tomas Vondra wrote:

Perhaps I'm missing something, but why is NUM_CONN_ATTEMPTS even needed?
Why isn't recovery_timeout enough to decide if wait_for_end_recovery()
waited long enough?

It was an attempt to decoupled a connection failure (that keeps streaming the
WAL) from recovery timeout. The NUM_CONN_ATTEMPTS guarantees that if the primary
is gone during the standby recovery process, there is a way to bail out. The
recovery-timeout is 0 (infinite) by default so you have an infinite wait without
this check. The idea behind this implementation is to avoid exiting in this
critical code path. If it times out here you might have to rebuild the standby
and start again. Amit suggested [1]/messages/by-id/CAA4eK1JRgnhv_ySzuFjN7UaX9qxz5Hqcwew7Fk=+AbJbu0Kd9w@mail.gmail.com that we use a value as recovery-timeout but
how high is a good value? I've already saw some long recovery process using
pglogical equivalent that timeout out after hundreds of minutes. Maybe I'm too
worried about a small percentage of cases and we should use 1h as default, for
example. It would reduce the complexity since the recovery process lacks some
progress indicators (LSN is not sufficient in this case and there isn't a
function to provide the current state -- stop applying WAL, reach target, new
timeline, etc).

If we remove the pg_stat_wal_receiver check, we should avoid infinite recovery
by default otherwise we will have some reports saying the tool is hanging when
in reality the primary has gone and WAL should be streamed.

IMHO the test should simply pass PG_TEST_DEFAULT_TIMEOUT when calling
pg_createsubscriber, and that should do the trick.

That's a good idea. Tests are not exercising the recovery-timeout option.

Increasing PG_TEST_DEFAULT_TIMEOUT is what buildfarm animals doing
things like ubsan/valgrind already use to deal with exactly this kind of
timeout problem.

Or is there a deeper problem with deciding if the system is in recovery?

As I said with some recovery progress indicators it would be easier to make some
decisions like wait a few seconds because the WAL has already been applied and
it is creating a new timeline. The recovery timeout decision is a shot in the
dark because we might be aborting pg_createsubscriber when the target server is
about to set RECOVERY_STATE_DONE.

[1]: /messages/by-id/CAA4eK1JRgnhv_ySzuFjN7UaX9qxz5Hqcwew7Fk=+AbJbu0Kd9w@mail.gmail.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

#248Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Euler Taveira (#247)
Re: speed up a logical replica setup

On 3/26/24 21:17, Euler Taveira wrote:

On Tue, Mar 26, 2024, at 4:12 PM, Tomas Vondra wrote:

Perhaps I'm missing something, but why is NUM_CONN_ATTEMPTS even needed?
Why isn't recovery_timeout enough to decide if wait_for_end_recovery()
waited long enough?

It was an attempt to decoupled a connection failure (that keeps streaming the
WAL) from recovery timeout. The NUM_CONN_ATTEMPTS guarantees that if the primary
is gone during the standby recovery process, there is a way to bail out. The
recovery-timeout is 0 (infinite) by default so you have an infinite wait without
this check. The idea behind this implementation is to avoid exiting in this
critical code path. If it times out here you might have to rebuild the standby
and start again.

- This seems like something that should definitely be documented in the
comment before wait_for_end_recovery(). At the moment it only talks
about timeout, and nothing about NUM_CONN_ATTEMPTS.

- The NUM_CONN_ATTEMPTS name seems rather misleading, considering it
does not really count connection attempts, but number of times we have
not seen 1 in pg_catalog.pg_stat_wal_receiver.

- Not sure I follow the logic - it tries to avoid exiting by setting
infinite timeout, but it still exists based on NUM_CONN_ATTEMPTS. Isn't
that somewhat contradictory?

- Isn't the NUM_CONN_ATTEMPTS actually making it more fragile, i.e. more
likely to exit? For example, what if there's a short networking hiccup,
so that the standby can't connect to the primary.

- It seems a bit strange that even with the recovery timeout set, having
the limit of 10 "connection attempts" effectively establishes a separate
hard-coded limit of 10 seconds. Seems a bit surprising if I set recovery
limit to 1 minute, and it just dies after 10 seconds.

Amit suggested [1] that we use a value as recovery-timeout but
how high is a good value? I've already saw some long recovery process using
pglogical equivalent that timeout out after hundreds of minutes. Maybe I'm too
worried about a small percentage of cases and we should use 1h as default, for
example. It would reduce the complexity since the recovery process lacks some
progress indicators (LSN is not sufficient in this case and there isn't a
function to provide the current state -- stop applying WAL, reach target, new
timeline, etc).

If we remove the pg_stat_wal_receiver check, we should avoid infinite recovery
by default otherwise we will have some reports saying the tool is hanging when
in reality the primary has gone and WAL should be streamed.

I don't think there's a default timeout value that would work for
everyone. Either it's going to be too short for some cases, or it'll
take too long for some other cases.

I think there are two obvious default values for the timeout - infinity,
and 60 seconds, which is the default we use for other CLI tools (like
pg_ctl and so on). Considering the negative impact of exiting, I'd say
it's better to default to infinity. It's always possible to Ctrl-C or
terminate the process in some other way, if needed.

As for people complaining about infinite recovery - perhaps it'd be
sufficient to mention this in the messages printed by the tool, to make
it clearer. Or maybe even print something in the loop, because right now
it's entirely silent so it's easy to believe it's stuck. Perhaps not on
every loop, but at least in verbose mode it should print something.

IMHO the test should simply pass PG_TEST_DEFAULT_TIMEOUT when calling
pg_createsubscriber, and that should do the trick.

That's a good idea. Tests are not exercising the recovery-timeout option.

Increasing PG_TEST_DEFAULT_TIMEOUT is what buildfarm animals doing
things like ubsan/valgrind already use to deal with exactly this kind of
timeout problem.

Or is there a deeper problem with deciding if the system is in recovery?

As I said with some recovery progress indicators it would be easier to make some
decisions like wait a few seconds because the WAL has already been applied and
it is creating a new timeline. The recovery timeout decision is a shot in the
dark because we might be aborting pg_createsubscriber when the target server is
about to set RECOVERY_STATE_DONE.

Isn't it enough to check data in pg_stat_replication on the primary?

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#249Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#247)
Re: speed up a logical replica setup

On Wed, Mar 27, 2024 at 1:47 AM Euler Taveira <euler@eulerto.com> wrote:

On Tue, Mar 26, 2024, at 4:12 PM, Tomas Vondra wrote:

Perhaps I'm missing something, but why is NUM_CONN_ATTEMPTS even needed?
Why isn't recovery_timeout enough to decide if wait_for_end_recovery()
waited long enough?

It was an attempt to decoupled a connection failure (that keeps streaming the
WAL) from recovery timeout. The NUM_CONN_ATTEMPTS guarantees that if the primary
is gone during the standby recovery process, there is a way to bail out.

I think we don't need to check primary if the WAL corresponding to
consistent_lsn is already present on the standby. Shouldn't we first
check that? Once we ensure that the required WAL is copied, just
checking server_is_in_recovery() should be sufficient. I feel that
will be a direct way of ensuring what is required rather than
indirectly verifying the same (by checking pg_stat_wal_receiver) as we
are doing currently.

--
With Regards,
Amit Kapila.

#250Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#242)
Re: speed up a logical replica setup

On Tue, Mar 26, 2024 at 8:24 AM Euler Taveira <euler@eulerto.com> wrote:

On Mon, Mar 25, 2024, at 1:06 PM, Hayato Kuroda (Fujitsu) wrote:

The first patch implements a combination of (1) and (2).

## Analysis for failure 2

According to [2], the physical replication slot which is specified as primary_slot_name
was not used by the walsender process. At that time walsender has not existed.

```
...
pg_createsubscriber: publisher: current wal senders: 0
pg_createsubscriber: command is: SELECT 1 FROM pg_catalog.pg_replication_slots WHERE active AND slot_name = 'physical_slot'
pg_createsubscriber: error: could not obtain replication slot information: got 0 rows, expected 1 row
...
```

Currently standby must be stopped before the command and current code does not
block the flow to ensure the replication is started. So there is a possibility
that the checking is run before walsender is launched.

One possible approach is to wait until the replication starts. Alternative one is
to ease the condition.

That's my suggestion too. I reused NUM_CONN_ATTEMPTS (that was renamed to
NUM_ATTEMPTS in the first patch). See second patch.

How can we guarantee that the slot can become active after these many
attempts? It still could be possible that on some slow machines it
didn't get activated even after NUM_ATTEMPTS. BTW, in the first place,
why do we need to ensure that the 'primary_slot_name' is active on the
primary?

--
With Regards,
Amit Kapila.

#251Euler Taveira
euler@eulerto.com
In reply to: Amit Kapila (#249)
Re: speed up a logical replica setup

On Mon, Apr 29, 2024, at 6:56 AM, Amit Kapila wrote:

On Wed, Mar 27, 2024 at 1:47 AM Euler Taveira <euler@eulerto.com> wrote:

On Tue, Mar 26, 2024, at 4:12 PM, Tomas Vondra wrote:

Perhaps I'm missing something, but why is NUM_CONN_ATTEMPTS even needed?
Why isn't recovery_timeout enough to decide if wait_for_end_recovery()
waited long enough?

It was an attempt to decoupled a connection failure (that keeps streaming the
WAL) from recovery timeout. The NUM_CONN_ATTEMPTS guarantees that if the primary
is gone during the standby recovery process, there is a way to bail out.

I think we don't need to check primary if the WAL corresponding to
consistent_lsn is already present on the standby. Shouldn't we first
check that? Once we ensure that the required WAL is copied, just
checking server_is_in_recovery() should be sufficient. I feel that
will be a direct way of ensuring what is required rather than
indirectly verifying the same (by checking pg_stat_wal_receiver) as we
are doing currently.

How would you check it? WAL file? During recovery, you are not allowed to use
pg_current_wal_lsn.

Tomas suggested to me off-list that we should adopt a simple solution in
wait_for_end_recovery: wait for recovery_timeout without additional checks
(which means remove the pg_stat_wal_receiver logic). When we have additional
information that we can reliably use in this function, we can add it. Hence, it
is also easy to adjust the PG_TEST_TIMEOUT_DEFAULT to have stable tests.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#252Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#251)
Re: speed up a logical replica setup

On Mon, Apr 29, 2024 at 5:23 PM Euler Taveira <euler@eulerto.com> wrote:

On Mon, Apr 29, 2024, at 6:56 AM, Amit Kapila wrote:

On Wed, Mar 27, 2024 at 1:47 AM Euler Taveira <euler@eulerto.com> wrote:

On Tue, Mar 26, 2024, at 4:12 PM, Tomas Vondra wrote:

Perhaps I'm missing something, but why is NUM_CONN_ATTEMPTS even needed?
Why isn't recovery_timeout enough to decide if wait_for_end_recovery()
waited long enough?

It was an attempt to decoupled a connection failure (that keeps streaming the
WAL) from recovery timeout. The NUM_CONN_ATTEMPTS guarantees that if the primary
is gone during the standby recovery process, there is a way to bail out.

I think we don't need to check primary if the WAL corresponding to
consistent_lsn is already present on the standby. Shouldn't we first
check that? Once we ensure that the required WAL is copied, just
checking server_is_in_recovery() should be sufficient. I feel that
will be a direct way of ensuring what is required rather than
indirectly verifying the same (by checking pg_stat_wal_receiver) as we
are doing currently.

How would you check it? WAL file? During recovery, you are not allowed to use
pg_current_wal_lsn.

How about pg_last_wal_receive_lsn()?

--
With Regards,
Amit Kapila.

#253Amit Kapila
amit.kapila16@gmail.com
In reply to: Amit Kapila (#252)
1 attachment(s)
Re: speed up a logical replica setup

On Mon, Apr 29, 2024 at 5:28 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Mon, Apr 29, 2024 at 5:23 PM Euler Taveira <euler@eulerto.com> wrote:

I was trying to test this utility when 'sync_replication_slots' is on
and it gets in an ERROR loop [1]2024-04-30 11:50:43.239 IST [12536] LOG: slot sync worker started 2024-04-30 11:50:43.247 IST [12536] ERROR: slot synchronization requires dbname to be specified in primary_conninfo and never finishes. Please find the
postgresql.auto used on the standby attached. I think if the standby
has enabled sync_slots, you need to pass dbname in
GenerateRecoveryConfig(). I couldn't test it further but I wonder if
there are already synced slots on the standby (either due to
'sync_replication_slots' or users have used
pg_sync_replication_slots() before invoking pg_createsubscriber),
those would be retained as it is on new subscriber and lead to
unnecessary WAL retention and dead rows.

[1]: 2024-04-30 11:50:43.239 IST [12536] LOG: slot sync worker started 2024-04-30 11:50:43.247 IST [12536] ERROR: slot synchronization requires dbname to be specified in primary_conninfo
2024-04-30 11:50:43.239 IST [12536] LOG: slot sync worker started
2024-04-30 11:50:43.247 IST [12536] ERROR: slot synchronization
requires dbname to be specified in primary_conninfo

--
With Regards,
Amit Kapila.

Attachments:

postgresql.auto.standby.confapplication/octet-stream; name=postgresql.auto.standby.confDownload
#254Amit Kapila
amit.kapila16@gmail.com
In reply to: Amit Kapila (#253)
Re: speed up a logical replica setup

On Tue, Apr 30, 2024 at 12:04 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Mon, Apr 29, 2024 at 5:28 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Mon, Apr 29, 2024 at 5:23 PM Euler Taveira <euler@eulerto.com> wrote:

I was trying to test this utility when 'sync_replication_slots' is on
and it gets in an ERROR loop [1] and never finishes. Please find the
postgresql.auto used on the standby attached. I think if the standby
has enabled sync_slots, you need to pass dbname in
GenerateRecoveryConfig().

The other possibility is that when we start standby from
pg_createsubscriber, we specifically set 'sync_replication_slots' as
false.

I couldn't test it further but I wonder if
there are already synced slots on the standby (either due to
'sync_replication_slots' or users have used
pg_sync_replication_slots() before invoking pg_createsubscriber),
those would be retained as it is on new subscriber and lead to
unnecessary WAL retention and dead rows.

This still needs some handling.

BTW, I don't see the use of following messages in --dry-run mode or
rather they could be misleading:
pg_createsubscriber: hint: If pg_createsubscriber fails after this
point, you must recreate the physical replica before continuing.
...
...
pg_createsubscriber: setting the replication progress (node name
"pg_0" ; LSN 0/0) on database "postgres"

Similarly, we should think if below messages are useful in --dry-run mode:
pg_createsubscriber: dropping publication
"pg_createsubscriber_5_887f7991" on database "postgres"
pg_createsubscriber: creating subscription
"pg_createsubscriber_5_887f7991" on database "postgres"
...
pg_createsubscriber: enabling subscription
"pg_createsubscriber_5_887f7991" on database "postgres"

--
With Regards,
Amit Kapila.

#255Thomas Munro
thomas.munro@gmail.com
In reply to: Amit Kapila (#254)
Re: speed up a logical replica setup

040_pg_createsubscriber.pl seems to be failing occasionally on
culicidae near "--dry-run on node S". I couldn't immediately see why.
That animal is using EXEC_BACKEND and I also saw a one-off failure a
bit like that on my own local Linux + EXEC_BACKEND test run
(sorry I didn't keep the details around). Coincidence?

#256Amit Kapila
amit.kapila16@gmail.com
In reply to: Thomas Munro (#255)
Re: speed up a logical replica setup

On Sun, May 19, 2024 at 4:25 AM Thomas Munro <thomas.munro@gmail.com> wrote:

040_pg_createsubscriber.pl seems to be failing occasionally on
culicidae near "--dry-run on node S". I couldn't immediately see why.
That animal is using EXEC_BACKEND and I also saw a one-off failure a
bit like that on my own local Linux + EXEC_BACKEND test run
(sorry I didn't keep the details around). Coincidence?

AFAICS, this is the same as one of the two BF failures being discussed
in this thread.

--
With Regards,
Amit Kapila.

#257Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Amit Kapila (#253)
6 attachment(s)
Re: speed up a logical replica setup

Hi,

I was trying to test this utility when 'sync_replication_slots' is on
and it gets in an ERROR loop [1] and never finishes. Please find the
postgresql.auto used on the standby attached. I think if the standby
has enabled sync_slots, you need to pass dbname in
GenerateRecoveryConfig(). I couldn't test it further but I wonder if
there are already synced slots on the standby (either due to
'sync_replication_slots' or users have used
pg_sync_replication_slots() before invoking pg_createsubscriber),
those would be retained as it is on new subscriber and lead to
unnecessary WAL retention and dead rows.

[1]
2024-04-30 11:50:43.239 IST [12536] LOG: slot sync worker started
2024-04-30 11:50:43.247 IST [12536] ERROR: slot synchronization
requires dbname to be specified in primary_conninfo

Hi,

I tested the scenario posted by Amit in [1]/messages/by-id/CAA4eK1KdCb+5sjYu6qCMXXdCX1y_ihr8kFzMozq0=P=auYxgog@mail.gmail.com, in which retaining synced
slots lead to unnecessary WAL retention and ERROR. This is raised as
the second open point in [2]/messages/by-id/CAA4eK1J22UEfrqx222h5j9DQ7nxGrTbAa_BC+=mQXdXs-RCsew@mail.gmail.com.
The steps to reproduce the issue:
(1) Setup physical replication with sync slot feature turned on by
setting sync_replication_slots = 'true' or using
pg_sync_replication_slots() on the standby node.
For physical replication setup, run pg_basebackup with -R and -d option.
(2) Create a logical replication slot on primary node with failover
option as true. A corresponding slot is created on standby as part of
sync slot feature.
(3) Run pg_createsubscriber on standby node.
(4) On Checking for the replication slot on standby node, I noticed
that the logical slots created in step 2 are retained.
I have attached the script to reproduce the issue.

I and Kuroda-san worked to resolve open points. Here are patches to
solve the second and third point in [2]/messages/by-id/CAA4eK1J22UEfrqx222h5j9DQ7nxGrTbAa_BC+=mQXdXs-RCsew@mail.gmail.com.
Patches proposed by Euler are also attached just in case, but they
were not modified.

v2-0001: not changed
v2-0002: not changed
v2-0003: ensures the slot sync is disabled during the conversion. This
resolves the second point.
v2-0004: drops sync slots which may be retained after running. This
resolves the second point.
v2-0005: removes misleading output messages in dry-run. This resolves
the third point.

[1]: /messages/by-id/CAA4eK1KdCb+5sjYu6qCMXXdCX1y_ihr8kFzMozq0=P=auYxgog@mail.gmail.com
[2]: /messages/by-id/CAA4eK1J22UEfrqx222h5j9DQ7nxGrTbAa_BC+=mQXdXs-RCsew@mail.gmail.com

Thanks and Regards,
Shlok Kyal

Attachments:

test.shtext/x-sh; charset=US-ASCII; name=test.shDownload
v2-0001-Improve-the-code-that-checks-if-the-recovery-is-f.patchapplication/octet-stream; name=v2-0001-Improve-the-code-that-checks-if-the-recovery-is-f.patchDownload
From f555720f8910733d50793933d9fa377bf35378ce Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 25 Mar 2024 22:01:52 -0300
Subject: [PATCH v2 1/5] Improve the code that checks if the recovery is
 finishing

The recovery process has a window between the walreceiver shutdown and
the pg_is_in_recovery function returns false. It means that the
pg_stat_wal_receiver checks can cause the server to finish the recovery
(even if it already reaches the recovery target). Since it checks the
pg_stat_wal_receiver to verify the primary is available, if it does not
return a row, PQping the primary server. If it is up and running, it
can indicate that the target server is finishing the recovery process,
hence, we shouldn't count it as an attempt. It avoids premature failures
on slow hosts.

While on it, increase the number of attempts (10 to 60). The wait time is
the same pg_promote function uses by default.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 30 +++++++++++++--------
 1 file changed, 19 insertions(+), 11 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 90cc580811..b4424861b2 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -30,6 +30,8 @@
 
 #define	DEFAULT_SUB_PORT	"50432"
 
+#define NUM_ATTEMPTS		60
+
 /* Command-line options */
 struct CreateSubscriberOptions
 {
@@ -93,7 +95,7 @@ static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
 static void start_standby_server(const struct CreateSubscriberOptions *opt,
 								 bool restricted_access);
 static void stop_standby_server(const char *datadir);
-static void wait_for_end_recovery(const char *conninfo,
+static void wait_for_end_recovery(const struct LogicalRepInfo *dbinfo,
 								  const struct CreateSubscriberOptions *opt);
 static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
@@ -1362,18 +1364,16 @@ stop_standby_server(const char *datadir)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions *opt)
+wait_for_end_recovery(const struct LogicalRepInfo *dbinfo, const struct CreateSubscriberOptions *opt)
 {
 	PGconn	   *conn;
 	int			status = POSTMASTER_STILL_STARTING;
 	int			timer = 0;
 	int			count = 0;		/* number of consecutive connection attempts */
 
-#define NUM_CONN_ATTEMPTS	10
-
 	pg_log_info("waiting for the target server to reach the consistent state");
 
-	conn = connect_database(conninfo, true);
+	conn = connect_database(dbinfo->subconninfo, true);
 
 	for (;;)
 	{
@@ -1392,16 +1392,24 @@ wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions
 		}
 
 		/*
-		 * If it is still in recovery, make sure the target server is
-		 * connected to the primary so it can receive the required WAL to
-		 * finish the recovery process. If it is disconnected try
-		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 * If it is still in recovery, make sure the target server is connected
+		 * to the primary so it can receive the required WAL to finish the
+		 * recovery process. If the walreceiver process is not running it
+		 * should indicate that (i) the recovery is almost finished or (ii) the
+		 * primary is not running or is not accpeting connections. It should
+		 * count as attempts iif (ii) is true. In this case, try NUM_ATTEMPTS
+		 * in a row and bail out if not succeed.
 		 */
 		res = PQexec(conn,
 					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
 		if (PQntuples(res) == 0)
 		{
-			if (++count > NUM_CONN_ATTEMPTS)
+			if (PQping(dbinfo->pubconninfo) != PQPING_OK)
+				count++;
+			else
+				count = 0;		/* reset counter if it connects again */
+
+			if (count > NUM_ATTEMPTS)
 			{
 				stop_standby_server(subscriber_dir);
 				pg_log_error("standby server disconnected from the primary");
@@ -2121,7 +2129,7 @@ main(int argc, char **argv)
 	start_standby_server(&opt, true);
 
 	/* Waiting the subscriber to be promoted */
-	wait_for_end_recovery(dbinfo[0].subconninfo, &opt);
+	wait_for_end_recovery(&dbinfo[0], &opt);
 
 	/*
 	 * Create the subscription for each database on subscriber. It does not
-- 
2.34.1

v2-0002-Improve-the-code-that-checks-if-the-primary-slot-.patchapplication/octet-stream; name=v2-0002-Improve-the-code-that-checks-if-the-primary-slot-.patchDownload
From 66cdc07bb1e475993a6784158e9912b015dfb472 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 25 Mar 2024 23:25:30 -0300
Subject: [PATCH v2 2/5] Improve the code that checks if the primary slot is
 available

The target server is started a few instructions before checking the
primary server and the replication slot might not be active. Instead of
failing the first time, try a few times.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 35 +++++++++++++++------
 1 file changed, 25 insertions(+), 10 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index b4424861b2..e0a6e10578 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -891,6 +891,8 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 	{
 		PQExpBuffer str = createPQExpBuffer();
 		char	   *psn_esc = PQescapeLiteral(conn, primary_slot_name, strlen(primary_slot_name));
+		int			ntuples;
+		int			count = 0;
 
 		appendPQExpBuffer(str,
 						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
@@ -901,25 +903,38 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 
 		pg_log_debug("command is: %s", str->data);
 
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		/*
+		 * The replication slot might take some time to be active, try a few
+		 * times if necessary.
+		 */
+		do
 		{
-			pg_log_error("could not obtain replication slot information: %s",
-						 PQresultErrorMessage(res));
-			disconnect_database(conn, true);
-		}
+			res = PQexec(conn, str->data);
+			if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			{
+				pg_log_error("could not obtain replication slot information: %s",
+							 PQresultErrorMessage(res));
+				disconnect_database(conn, true);
+			}
 
-		if (PQntuples(res) != 1)
+			ntuples = PQntuples(res);
+			PQclear(res);
+
+			if (ntuples == 1)	/* replication slot is already active */
+				break;
+			else
+				count++;
+		} while (count > NUM_ATTEMPTS);
+
+		if (ntuples != 1)
 		{
 			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
-						 PQntuples(res), 1);
+						 ntuples, 1);
 			disconnect_database(conn, true);
 		}
 		else
 			pg_log_info("primary has replication slot \"%s\"",
 						primary_slot_name);
-
-		PQclear(res);
 	}
 
 	disconnect_database(conn, false);
-- 
2.34.1

v2-0003-Disable-slot-sync-during-the-convertion.patchapplication/octet-stream; name=v2-0003-Disable-slot-sync-during-the-convertion.patchDownload
From 43320584373fb44a68d8fdfb67ce8bb12df6e9af Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 9 May 2024 02:20:40 +0000
Subject: [PATCH v2 3/5] Disable slot sync during the convertion

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index e0a6e10578..c8849fbc82 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1349,6 +1349,19 @@ start_standby_server(const struct CreateSubscriberOptions *opt, bool restricted_
 	if (opt->config_file != NULL)
 		appendPQExpBuffer(pg_ctl_cmd, " -o \"-c config_file=%s\"",
 						  opt->config_file);
+
+	/*
+	 * We set up some recovery parameters in the middle part of the
+	 * pg_createsubscriber to ensure all changes are caught up. At that time,
+	 * primary_conninfo will be set without the dbname attribute. Basically, it
+	 * works well, but if the slot sync is enabled, the slot sync worker will
+	 * exit with ERROR because the process requires dbname within
+	 * primary_conninfo. To avoid the error, we override sync_replication_slots
+	 * to off. The synchronization won't happen after the conversion, so it is
+	 * acceptable to stop here as well.
+	 */
+	appendPQExpBuffer(pg_ctl_cmd, " -o \"-c sync_replication_slots=off\"");
+
 	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
 	rc = system(pg_ctl_cmd->data);
 	pg_ctl_status(pg_ctl_cmd->data, rc);
-- 
2.34.1

v2-0004-Drop-replication-slots-which-had-been-synchronize.patchapplication/octet-stream; name=v2-0004-Drop-replication-slots-which-had-been-synchronize.patchDownload
From ad7de281d514343df7a04d01314266837d544b33 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 9 May 2024 12:53:36 +0000
Subject: [PATCH v2 4/5] Drop replication slots which had been synchronized

After the transformation, the slotsync worker won't be available on the standby.
If synchronized slots are created, they do not consume changes, it may lead
PANIC. To avoid the issue, drop such slots at the end of the command.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 64 +++++++++++++++++++++
 1 file changed, 64 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index c8849fbc82..ee96a6fd0a 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -87,6 +87,7 @@ static void setup_recovery(const struct LogicalRepInfo *dbinfo, const char *data
 						   const char *lsn);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  const char *slotname);
+static void drop_replication_slot_on_subscriber(struct LogicalRepInfo *dbinfo);
 static char *create_logical_replication_slot(PGconn *conn,
 											 struct LogicalRepInfo *dbinfo);
 static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
@@ -1195,6 +1196,61 @@ drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, const char *slotnam
 	}
 }
 
+/*
+ * Drop logical replication slot on subscriber if they had been synchronized.
+ * After the transformation, they won't be updated.
+ */
+static void
+drop_replication_slot_on_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+
+		/* Connect to the subscriber */
+		conn = connect_database(dbinfo[i].subconninfo, false);
+
+		if (conn == NULL)
+		{
+			pg_log_warning("could not connect to the database \"%s\" on standby",
+						   dbinfo[i].dbname);
+			pg_log_warning_hint("You may have to drop replication slots which had been synchronized.");
+			continue;
+		}
+
+		/* Extract slots which had been synchronized. */
+		res = PQexec(conn,
+					 "SELECT slot_name FROM pg_catalog.pg_replication_slots "
+					 "WHERE failover;");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_warning("could not obtain synchronized slots information: %s",
+						   PQresultErrorMessage(res));
+			pg_log_warning_hint("You may have to drop replication slots which had been synchronized.");
+			disconnect_database(conn, false);
+			continue;
+		}
+
+		/* Skip if not found */
+		if (PQntuples(res) == 0)
+		{
+			PQclear(res);
+			disconnect_database(conn, false);
+			continue;
+		}
+
+		/* Drop all replication slots extracted by the query */
+		for (int j = 0; j < PQntuples(res); j++)
+			drop_replication_slot(conn, &dbinfo[i], PQgetvalue(res, j, 0));
+
+		/* Cleanup */
+		PQclear(res);
+		disconnect_database(conn, false);
+	}
+}
+
 /*
  * Create a logical replication slot and returns a LSN.
  *
@@ -2170,6 +2226,14 @@ main(int argc, char **argv)
 	/* Remove primary_slot_name if it exists on primary */
 	drop_primary_replication_slot(dbinfo, primary_slot_name);
 
+	/*
+	 * After the transformation, the slotsync worker won't be available on the
+	 * standby. If synchronized slots are created, they do not consume changes,
+	 * it may lead PANIC. To avoid the issue, drop such slots at the end of the
+	 * command.
+	 */
+	drop_replication_slot_on_subscriber(dbinfo);
+
 	/* Stop the subscriber */
 	pg_log_info("stopping the subscriber");
 	stop_standby_server(subscriber_dir);
-- 
2.34.1

v2-0005-Avoid-outputing-some-messages-in-dry_run-mode.patchapplication/octet-stream; name=v2-0005-Avoid-outputing-some-messages-in-dry_run-mode.patchDownload
From 37a1b81992eb55040322273a516df455088edc7d Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 9 May 2024 11:47:56 +0000
Subject: [PATCH v2 5/5] Avoid outputing some messages in dry_run mode

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 70 +++++++++++++--------
 1 file changed, 45 insertions(+), 25 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index ee96a6fd0a..3cd30b7b13 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -75,6 +75,7 @@ static PGconn *connect_database(const char *conninfo, bool exit_on_error);
 static void disconnect_database(PGconn *conn, bool exit_on_error);
 static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
+static void log_if_not_dry_run(const char *format,...) pg_attribute_printf(1, 2);
 static void modify_subscriber_sysid(const struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static char *generate_object_name(PGconn *conn);
@@ -161,7 +162,7 @@ cleanup_objects_atexit(void)
 	 * again. Warn the user that a new replication setup should be done before
 	 * trying again.
 	 */
-	if (recovery_ended)
+	if (recovery_ended && !dry_run)
 	{
 		pg_log_warning("failed after the end of recovery");
 		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.  "
@@ -190,13 +191,13 @@ cleanup_objects_atexit(void)
 				 * that some objects were left on primary and should be
 				 * removed before trying again.
 				 */
-				if (dbinfo[i].made_publication)
+				if (dbinfo[i].made_publication && !dry_run)
 				{
 					pg_log_warning("publication \"%s\" in database \"%s\" on primary might be left behind",
 								   dbinfo[i].pubname, dbinfo[i].dbname);
 					pg_log_warning_hint("Consider dropping this publication before trying again.");
 				}
-				if (dbinfo[i].made_replslot)
+				if (dbinfo[i].made_replslot && !dry_run)
 				{
 					pg_log_warning("replication slot \"%s\" in database \"%s\" on primary might be left behind",
 								   dbinfo[i].replslotname, dbinfo[i].dbname);
@@ -610,6 +611,22 @@ get_standby_sysid(const char *datadir)
 	return sysid;
 }
 
+/*
+ * Put the message if we are not in dry_run mode
+ */
+static void
+log_if_not_dry_run(const char *format,...)
+{
+	va_list		ap;
+
+	if (dry_run)
+		return;
+
+	va_start(ap, format);
+	pg_log_generic_v(PG_LOG_INFO, PG_LOG_PRIMARY, format, ap);
+	va_end(ap);
+}
+
 /*
  * Modify the system identifier. Since a standby server preserves the system
  * identifier, it makes sense to change it to avoid situations in which WAL
@@ -624,7 +641,7 @@ modify_subscriber_sysid(const struct CreateSubscriberOptions *opt)
 
 	char	   *cmd_str;
 
-	pg_log_info("modifying system identifier of subscriber");
+	log_if_not_dry_run("modifying system identifier of subscriber");
 
 	cf = get_controlfile(subscriber_dir, &crc_ok);
 	if (!crc_ok)
@@ -643,10 +660,10 @@ modify_subscriber_sysid(const struct CreateSubscriberOptions *opt)
 	if (!dry_run)
 		update_controlfile(subscriber_dir, cf, true);
 
-	pg_log_info("system identifier is %llu on subscriber",
-				(unsigned long long) cf->system_identifier);
+	log_if_not_dry_run("system identifier is %llu on subscriber",
+					   (unsigned long long) cf->system_identifier);
 
-	pg_log_info("running pg_resetwal on the subscriber");
+	log_if_not_dry_run("running pg_resetwal on the subscriber");
 
 	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
 					   subscriber_dir, DEVNULL);
@@ -762,10 +779,10 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		if (lsn)
 			pg_free(lsn);
 		lsn = create_logical_replication_slot(conn, &dbinfo[i]);
-		if (lsn != NULL || dry_run)
+		if (lsn != NULL)
 			pg_log_info("create replication slot \"%s\" on publisher",
 						dbinfo[i].replslotname);
-		else
+		else if (!dry_run)
 			exit(1);
 
 		disconnect_database(conn, false);
@@ -1268,8 +1285,8 @@ create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
-				slot_name, dbinfo->dbname);
+	log_if_not_dry_run("creating the replication slot \"%s\" on database \"%s\"",
+					   slot_name, dbinfo->dbname);
 
 	slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name));
 
@@ -1316,8 +1333,8 @@ drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
-				slot_name, dbinfo->dbname);
+	log_if_not_dry_run("dropping the replication slot \"%s\" on database \"%s\"",
+					   slot_name, dbinfo->dbname);
 
 	slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name));
 
@@ -1524,8 +1541,11 @@ wait_for_end_recovery(const struct LogicalRepInfo *dbinfo, const struct CreateSu
 	if (status == POSTMASTER_STILL_STARTING)
 		pg_fatal("server did not end recovery");
 
-	pg_log_info("target server reached the consistent state");
-	pg_log_info_hint("If pg_createsubscriber fails after this point, you must recreate the physical replica before continuing.");
+	if (!dry_run)
+	{
+		pg_log_info("target server reached the consistent state");
+		pg_log_info_hint("If pg_createsubscriber fails after this point, you must recreate the physical replica before continuing.");
+	}
 }
 
 /*
@@ -1574,8 +1594,8 @@ create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
 	PQclear(res);
 	resetPQExpBuffer(str);
 
-	pg_log_info("creating publication \"%s\" on database \"%s\"",
-				dbinfo->pubname, dbinfo->dbname);
+	log_if_not_dry_run("creating publication \"%s\" on database \"%s\"",
+					   dbinfo->pubname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
 					  ipubname_esc);
@@ -1616,8 +1636,8 @@ drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
 
 	pubname_esc = PQescapeIdentifier(conn, dbinfo->pubname, strlen(dbinfo->pubname));
 
-	pg_log_info("dropping publication \"%s\" on database \"%s\"",
-				dbinfo->pubname, dbinfo->dbname);
+	log_if_not_dry_run("dropping publication \"%s\" on database \"%s\"",
+					   dbinfo->pubname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "DROP PUBLICATION %s", pubname_esc);
 
@@ -1676,8 +1696,8 @@ create_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo)
 	pubconninfo_esc = PQescapeLiteral(conn, dbinfo->pubconninfo, strlen(dbinfo->pubconninfo));
 	replslotname_esc = PQescapeLiteral(conn, dbinfo->replslotname, strlen(dbinfo->replslotname));
 
-	pg_log_info("creating subscription \"%s\" on database \"%s\"",
-				dbinfo->subname, dbinfo->dbname);
+	log_if_not_dry_run("creating subscription \"%s\" on database \"%s\"",
+					   dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION %s PUBLICATION %s "
@@ -1773,8 +1793,8 @@ set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, cons
 	 */
 	originname = psprintf("pg_%u", suboid);
 
-	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
-				originname, lsnstr, dbinfo->dbname);
+	log_if_not_dry_run("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+					   originname, lsnstr, dbinfo->dbname);
 
 	resetPQExpBuffer(str);
 	appendPQExpBuffer(str,
@@ -1819,8 +1839,8 @@ enable_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo)
 
 	subname = PQescapeIdentifier(conn, dbinfo->subname, strlen(dbinfo->subname));
 
-	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
-				dbinfo->subname, dbinfo->dbname);
+	log_if_not_dry_run("enabling subscription \"%s\" on database \"%s\"",
+					   dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", subname);
 
-- 
2.34.1

#258Amit Kapila
amit.kapila16@gmail.com
In reply to: Shlok Kyal (#257)
Re: speed up a logical replica setup

On Mon, May 20, 2024 at 4:30 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I was trying to test this utility when 'sync_replication_slots' is on
and it gets in an ERROR loop [1] and never finishes. Please find the
postgresql.auto used on the standby attached. I think if the standby
has enabled sync_slots, you need to pass dbname in
GenerateRecoveryConfig(). I couldn't test it further but I wonder if
there are already synced slots on the standby (either due to
'sync_replication_slots' or users have used
pg_sync_replication_slots() before invoking pg_createsubscriber),
those would be retained as it is on new subscriber and lead to
unnecessary WAL retention and dead rows.

[1]
2024-04-30 11:50:43.239 IST [12536] LOG: slot sync worker started
2024-04-30 11:50:43.247 IST [12536] ERROR: slot synchronization
requires dbname to be specified in primary_conninfo

Hi,

I tested the scenario posted by Amit in [1], in which retaining synced
slots lead to unnecessary WAL retention and ERROR. This is raised as
the second open point in [2].
The steps to reproduce the issue:
(1) Setup physical replication with sync slot feature turned on by
setting sync_replication_slots = 'true' or using
pg_sync_replication_slots() on the standby node.
For physical replication setup, run pg_basebackup with -R and -d option.
(2) Create a logical replication slot on primary node with failover
option as true. A corresponding slot is created on standby as part of
sync slot feature.
(3) Run pg_createsubscriber on standby node.
(4) On Checking for the replication slot on standby node, I noticed
that the logical slots created in step 2 are retained.
I have attached the script to reproduce the issue.

I and Kuroda-san worked to resolve open points. Here are patches to
solve the second and third point in [2].
Patches proposed by Euler are also attached just in case, but they
were not modified.

v2-0001: not changed

Shouldn't we modify it as per the suggestion given in the email [1]/messages/by-id/CAA4eK1JJq_ER6Kq_H=jKHR75QPRd8y9_D=RtYw=aPYKMfqLi9A@mail.gmail.com? I
am wondering if we can entirely get rid of checking the primary
business and simply rely on recovery_timeout and keep checking
server_is_in_recovery(). If so, we can modify the test to use
non-default recovery_timeout (say 180s or something similar if we have
used it at any other place). As an additional check we can ensure that
constent_lsn is present on standby.

v2-0002: not changed

We have added more tries to see if the primary_slot_name becomes
active but I think it is still fragile because it is possible on slow
machines that the required slot didn't become active even after more
retries. I have raised the same comment previously [2]/messages/by-id/CAA4eK1LT3Z13Dg6p4Z+4adO_EY-ow5CmWfikEmBfL=eVrm8CPw@mail.gmail.com and asked an
additional question but didn't get any response.

[1]: /messages/by-id/CAA4eK1JJq_ER6Kq_H=jKHR75QPRd8y9_D=RtYw=aPYKMfqLi9A@mail.gmail.com
[2]: /messages/by-id/CAA4eK1LT3Z13Dg6p4Z+4adO_EY-ow5CmWfikEmBfL=eVrm8CPw@mail.gmail.com

--
With Regards,
Amit Kapila.

#259Euler Taveira
euler@eulerto.com
In reply to: Amit Kapila (#258)
Re: speed up a logical replica setup

On Wed, May 22, 2024, at 8:19 AM, Amit Kapila wrote:

v2-0001: not changed

Shouldn't we modify it as per the suggestion given in the email [1]? I
am wondering if we can entirely get rid of checking the primary
business and simply rely on recovery_timeout and keep checking
server_is_in_recovery(). If so, we can modify the test to use
non-default recovery_timeout (say 180s or something similar if we have
used it at any other place). As an additional check we can ensure that
constent_lsn is present on standby.

That's exactly what I want to propose as Tomas convinced me offlist that less is
better when we don't have a useful recovery progress reporting mechanism to make
sure it is still working on the recovery and we should wait.

v2-0002: not changed

We have added more tries to see if the primary_slot_name becomes
active but I think it is still fragile because it is possible on slow
machines that the required slot didn't become active even after more
retries. I have raised the same comment previously [2] and asked an
additional question but didn't get any response.

Following the same line that simplifies the code, we can: (a) add a loop in
check_subscriber() that waits until walreceiver is available on subscriber or
(b) use a timeout. The main advantage of (a) is that the primary slot is already
available but I'm afraid we need a escape mechanism for the loop (timeout?).

I'll summarize all issues as soon as I finish the review of sync slot support. I
think we should avoid new development if we judge that the item can be
documented as a limitation for this version. Nevertheless, I will share patches
so you can give your opinion on whether it is an open item or new development.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#260Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#259)
Re: speed up a logical replica setup

On Wed, May 22, 2024 at 8:46 PM Euler Taveira <euler@eulerto.com> wrote:

On Wed, May 22, 2024, at 8:19 AM, Amit Kapila wrote:

v2-0002: not changed

We have added more tries to see if the primary_slot_name becomes
active but I think it is still fragile because it is possible on slow
machines that the required slot didn't become active even after more
retries. I have raised the same comment previously [2] and asked an
additional question but didn't get any response.

Following the same line that simplifies the code, we can: (a) add a loop in
check_subscriber() that waits until walreceiver is available on subscriber or
(b) use a timeout. The main advantage of (a) is that the primary slot is already
available but I'm afraid we need a escape mechanism for the loop (timeout?).

Sorry, it is not clear to me why we need any additional loop in
check_subscriber(), aren't we speaking about the problem in
check_publisher() function?

Why in the first place do we need to ensure that primary_slot_name is
active on the primary? You mentioned something related to WAL
retention but I don't know how that is related to this tool's
functionality. If at all, we are bothered about WAL retention on the
primary that should be the WAL corresponding to consistent_lsn
computed by setup_publisher() but this check doesn't seem to ensure
that.

--
With Regards,
Amit Kapila.

#261Euler Taveira
euler@eulerto.com
In reply to: Amit Kapila (#260)
Re: speed up a logical replica setup

On Thu, May 23, 2024, at 5:54 AM, Amit Kapila wrote:

On Wed, May 22, 2024 at 8:46 PM Euler Taveira <euler@eulerto.com> wrote:

Following the same line that simplifies the code, we can: (a) add a loop in
check_subscriber() that waits until walreceiver is available on subscriber or
(b) use a timeout. The main advantage of (a) is that the primary slot is already
available but I'm afraid we need a escape mechanism for the loop (timeout?).

Sorry, it is not clear to me why we need any additional loop in
check_subscriber(), aren't we speaking about the problem in
check_publisher() function?

The idea is to use check_subscriber() to check pg_stat_walreceiver. Once this
view returns a row and primary_slot_name is set on standby, the referred
replication slot name should be active on primary. Hence, the query on
check_publisher() make sure that the referred replication slot is in use on
primary.

Why in the first place do we need to ensure that primary_slot_name is
active on the primary? You mentioned something related to WAL
retention but I don't know how that is related to this tool's
functionality. If at all, we are bothered about WAL retention on the
primary that should be the WAL corresponding to consistent_lsn
computed by setup_publisher() but this check doesn't seem to ensure
that.

Maybe it is a lot of checks. I'm afraid there isn't a simple way to get and
make sure the replication slot is used by the physical replication. I mean if
there is primary_slot_name = 'foo' on standby, there is no guarantee that the
replication slot 'foo' exists on primary. The idea is to get the exact
replication slot name used by physical replication to drop it. Once I posted a
patch it should be clear. (Another idea is to relax this check and rely only on
primary_slot_name to drop this replication slot on primary. The replication slot
might not exist and it shouldn't return an error in this case.)

--
Euler Taveira
EDB https://www.enterprisedb.com/

#262Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#261)
Re: speed up a logical replica setup

On Thu, May 23, 2024 at 8:43 PM Euler Taveira <euler@eulerto.com> wrote:

On Thu, May 23, 2024, at 5:54 AM, Amit Kapila wrote:

Why in the first place do we need to ensure that primary_slot_name is
active on the primary? You mentioned something related to WAL
retention but I don't know how that is related to this tool's
functionality. If at all, we are bothered about WAL retention on the
primary that should be the WAL corresponding to consistent_lsn
computed by setup_publisher() but this check doesn't seem to ensure
that.

Maybe it is a lot of checks. I'm afraid there isn't a simple way to get and
make sure the replication slot is used by the physical replication. I mean if
there is primary_slot_name = 'foo' on standby, there is no guarantee that the
replication slot 'foo' exists on primary. The idea is to get the exact
replication slot name used by physical replication to drop it. Once I posted a
patch it should be clear. (Another idea is to relax this check and rely only on
primary_slot_name to drop this replication slot on primary. The replication slot
might not exist and it shouldn't return an error in this case.)

I think your other idea is better than what we are doing currently.
Let's ignore the ERROR even if the primary_slot_name doesn't exist on
the primary.

--
With Regards,
Amit Kapila.

#263Shlok Kyal
shlok.kyal.oss@gmail.com
In reply to: Amit Kapila (#258)
5 attachment(s)
Re: speed up a logical replica setup

On Wed, 22 May 2024 at 16:50, Amit Kapila <amit.kapila16@gmail.com> wrote:

On Mon, May 20, 2024 at 4:30 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:

I was trying to test this utility when 'sync_replication_slots' is on
and it gets in an ERROR loop [1] and never finishes. Please find the
postgresql.auto used on the standby attached. I think if the standby
has enabled sync_slots, you need to pass dbname in
GenerateRecoveryConfig(). I couldn't test it further but I wonder if
there are already synced slots on the standby (either due to
'sync_replication_slots' or users have used
pg_sync_replication_slots() before invoking pg_createsubscriber),
those would be retained as it is on new subscriber and lead to
unnecessary WAL retention and dead rows.

[1]
2024-04-30 11:50:43.239 IST [12536] LOG: slot sync worker started
2024-04-30 11:50:43.247 IST [12536] ERROR: slot synchronization
requires dbname to be specified in primary_conninfo

Hi,

I tested the scenario posted by Amit in [1], in which retaining synced
slots lead to unnecessary WAL retention and ERROR. This is raised as
the second open point in [2].
The steps to reproduce the issue:
(1) Setup physical replication with sync slot feature turned on by
setting sync_replication_slots = 'true' or using
pg_sync_replication_slots() on the standby node.
For physical replication setup, run pg_basebackup with -R and -d option.
(2) Create a logical replication slot on primary node with failover
option as true. A corresponding slot is created on standby as part of
sync slot feature.
(3) Run pg_createsubscriber on standby node.
(4) On Checking for the replication slot on standby node, I noticed
that the logical slots created in step 2 are retained.
I have attached the script to reproduce the issue.

I and Kuroda-san worked to resolve open points. Here are patches to
solve the second and third point in [2].
Patches proposed by Euler are also attached just in case, but they
were not modified.

v2-0001: not changed

Shouldn't we modify it as per the suggestion given in the email [1]? I
am wondering if we can entirely get rid of checking the primary
business and simply rely on recovery_timeout and keep checking
server_is_in_recovery(). If so, we can modify the test to use
non-default recovery_timeout (say 180s or something similar if we have
used it at any other place). As an additional check we can ensure that
constent_lsn is present on standby.

I have made changes as per your suggestion. I have used
pg_last_wal_receive_lsn to get lsn last wal received on standby and
check if consistent_lsn is already present on the standby.
I have only made changes in v3-0001. v3-0002, v3-0003, v3-0004 and
v3-0005 are not modified.

Thanks and Regards,
Shlok Kyal

Attachments:

v3-0002-Improve-the-code-that-checks-if-the-primary-slot-.patchapplication/octet-stream; name=v3-0002-Improve-the-code-that-checks-if-the-primary-slot-.patchDownload
From c40bb962eacc1e3808e0e0bb0f41896f78904feb Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Mon, 25 Mar 2024 23:25:30 -0300
Subject: [PATCH v3 2/5] Improve the code that checks if the primary slot is
 available

The target server is started a few instructions before checking the
primary server and the replication slot might not be active. Instead of
failing the first time, try a few times.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 35 +++++++++++++++------
 1 file changed, 25 insertions(+), 10 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index d602aac405..72322babd3 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -891,6 +891,8 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 	{
 		PQExpBuffer str = createPQExpBuffer();
 		char	   *psn_esc = PQescapeLiteral(conn, primary_slot_name, strlen(primary_slot_name));
+		int			ntuples;
+		int			count = 0;
 
 		appendPQExpBuffer(str,
 						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
@@ -901,25 +903,38 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 
 		pg_log_debug("command is: %s", str->data);
 
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		/*
+		 * The replication slot might take some time to be active, try a few
+		 * times if necessary.
+		 */
+		do
 		{
-			pg_log_error("could not obtain replication slot information: %s",
-						 PQresultErrorMessage(res));
-			disconnect_database(conn, true);
-		}
+			res = PQexec(conn, str->data);
+			if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			{
+				pg_log_error("could not obtain replication slot information: %s",
+							 PQresultErrorMessage(res));
+				disconnect_database(conn, true);
+			}
+
+			ntuples = PQntuples(res);
+			PQclear(res);
+
+			if (ntuples == 1)	/* replication slot is already active */
+				break;
+			else
+				count++;
+		} while (count > NUM_ATTEMPTS);
 
-		if (PQntuples(res) != 1)
+		if (ntuples != 1)
 		{
 			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
-						 PQntuples(res), 1);
+						 ntuples, 1);
 			disconnect_database(conn, true);
 		}
 		else
 			pg_log_info("primary has replication slot \"%s\"",
 						primary_slot_name);
-
-		PQclear(res);
 	}
 
 	disconnect_database(conn, false);
-- 
2.34.1

v3-0005-Avoid-outputing-some-messages-in-dry_run-mode.patchapplication/octet-stream; name=v3-0005-Avoid-outputing-some-messages-in-dry_run-mode.patchDownload
From e53a459e9e410fa6b18714c955e74c6edf73e3ef Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 9 May 2024 11:47:56 +0000
Subject: [PATCH v3 5/5] Avoid outputing some messages in dry_run mode

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 70 +++++++++++++--------
 1 file changed, 45 insertions(+), 25 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index e07327cc52..8b481d50e3 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -74,6 +74,7 @@ static PGconn *connect_database(const char *conninfo, bool exit_on_error);
 static void disconnect_database(PGconn *conn, bool exit_on_error);
 static uint64 get_primary_sysid(const char *conninfo);
 static uint64 get_standby_sysid(const char *datadir);
+static void log_if_not_dry_run(const char *format,...) pg_attribute_printf(1, 2);
 static void modify_subscriber_sysid(const struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static char *generate_object_name(PGconn *conn);
@@ -161,7 +162,7 @@ cleanup_objects_atexit(void)
 	 * again. Warn the user that a new replication setup should be done before
 	 * trying again.
 	 */
-	if (recovery_ended)
+	if (recovery_ended && !dry_run)
 	{
 		pg_log_warning("failed after the end of recovery");
 		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.  "
@@ -190,13 +191,13 @@ cleanup_objects_atexit(void)
 				 * that some objects were left on primary and should be
 				 * removed before trying again.
 				 */
-				if (dbinfo[i].made_publication)
+				if (dbinfo[i].made_publication && !dry_run)
 				{
 					pg_log_warning("publication \"%s\" in database \"%s\" on primary might be left behind",
 								   dbinfo[i].pubname, dbinfo[i].dbname);
 					pg_log_warning_hint("Consider dropping this publication before trying again.");
 				}
-				if (dbinfo[i].made_replslot)
+				if (dbinfo[i].made_replslot && !dry_run)
 				{
 					pg_log_warning("replication slot \"%s\" in database \"%s\" on primary might be left behind",
 								   dbinfo[i].replslotname, dbinfo[i].dbname);
@@ -610,6 +611,22 @@ get_standby_sysid(const char *datadir)
 	return sysid;
 }
 
+/*
+ * Put the message if we are not in dry_run mode
+ */
+static void
+log_if_not_dry_run(const char *format,...)
+{
+	va_list		ap;
+
+	if (dry_run)
+		return;
+
+	va_start(ap, format);
+	pg_log_generic_v(PG_LOG_INFO, PG_LOG_PRIMARY, format, ap);
+	va_end(ap);
+}
+
 /*
  * Modify the system identifier. Since a standby server preserves the system
  * identifier, it makes sense to change it to avoid situations in which WAL
@@ -624,7 +641,7 @@ modify_subscriber_sysid(const struct CreateSubscriberOptions *opt)
 
 	char	   *cmd_str;
 
-	pg_log_info("modifying system identifier of subscriber");
+	log_if_not_dry_run("modifying system identifier of subscriber");
 
 	cf = get_controlfile(subscriber_dir, &crc_ok);
 	if (!crc_ok)
@@ -643,10 +660,10 @@ modify_subscriber_sysid(const struct CreateSubscriberOptions *opt)
 	if (!dry_run)
 		update_controlfile(subscriber_dir, cf, true);
 
-	pg_log_info("system identifier is %llu on subscriber",
-				(unsigned long long) cf->system_identifier);
+	log_if_not_dry_run("system identifier is %llu on subscriber",
+					   (unsigned long long) cf->system_identifier);
 
-	pg_log_info("running pg_resetwal on the subscriber");
+	log_if_not_dry_run("running pg_resetwal on the subscriber");
 
 	cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path,
 					   subscriber_dir, DEVNULL);
@@ -762,10 +779,10 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		if (lsn)
 			pg_free(lsn);
 		lsn = create_logical_replication_slot(conn, &dbinfo[i]);
-		if (lsn != NULL || dry_run)
+		if (lsn != NULL)
 			pg_log_info("create replication slot \"%s\" on publisher",
 						dbinfo[i].replslotname);
-		else
+		else if (!dry_run)
 			exit(1);
 
 		disconnect_database(conn, false);
@@ -1268,8 +1285,8 @@ create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
 
 	Assert(conn != NULL);
 
-	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
-				slot_name, dbinfo->dbname);
+	log_if_not_dry_run("creating the replication slot \"%s\" on database \"%s\"",
+					   slot_name, dbinfo->dbname);
 
 	slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name));
 
@@ -1316,8 +1333,8 @@ drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
 
 	Assert(conn != NULL);
 
-	pg_log_info("dropping the replication slot \"%s\" on database \"%s\"",
-				slot_name, dbinfo->dbname);
+	log_if_not_dry_run("dropping the replication slot \"%s\" on database \"%s\"",
+					   slot_name, dbinfo->dbname);
 
 	slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name));
 
@@ -1531,8 +1548,11 @@ wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions
 	if (status == POSTMASTER_STILL_STARTING)
 		pg_fatal("server did not end recovery");
 
-	pg_log_info("target server reached the consistent state");
-	pg_log_info_hint("If pg_createsubscriber fails after this point, you must recreate the physical replica before continuing.");
+	if (!dry_run)
+	{
+		pg_log_info("target server reached the consistent state");
+		pg_log_info_hint("If pg_createsubscriber fails after this point, you must recreate the physical replica before continuing.");
+	}
 }
 
 /*
@@ -1581,8 +1601,8 @@ create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
 	PQclear(res);
 	resetPQExpBuffer(str);
 
-	pg_log_info("creating publication \"%s\" on database \"%s\"",
-				dbinfo->pubname, dbinfo->dbname);
+	log_if_not_dry_run("creating publication \"%s\" on database \"%s\"",
+					   dbinfo->pubname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
 					  ipubname_esc);
@@ -1623,8 +1643,8 @@ drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
 
 	pubname_esc = PQescapeIdentifier(conn, dbinfo->pubname, strlen(dbinfo->pubname));
 
-	pg_log_info("dropping publication \"%s\" on database \"%s\"",
-				dbinfo->pubname, dbinfo->dbname);
+	log_if_not_dry_run("dropping publication \"%s\" on database \"%s\"",
+					   dbinfo->pubname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "DROP PUBLICATION %s", pubname_esc);
 
@@ -1683,8 +1703,8 @@ create_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo)
 	pubconninfo_esc = PQescapeLiteral(conn, dbinfo->pubconninfo, strlen(dbinfo->pubconninfo));
 	replslotname_esc = PQescapeLiteral(conn, dbinfo->replslotname, strlen(dbinfo->replslotname));
 
-	pg_log_info("creating subscription \"%s\" on database \"%s\"",
-				dbinfo->subname, dbinfo->dbname);
+	log_if_not_dry_run("creating subscription \"%s\" on database \"%s\"",
+					   dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str,
 					  "CREATE SUBSCRIPTION %s CONNECTION %s PUBLICATION %s "
@@ -1780,8 +1800,8 @@ set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, cons
 	 */
 	originname = psprintf("pg_%u", suboid);
 
-	pg_log_info("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
-				originname, lsnstr, dbinfo->dbname);
+	log_if_not_dry_run("setting the replication progress (node name \"%s\" ; LSN %s) on database \"%s\"",
+					   originname, lsnstr, dbinfo->dbname);
 
 	resetPQExpBuffer(str);
 	appendPQExpBuffer(str,
@@ -1826,8 +1846,8 @@ enable_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo)
 
 	subname = PQescapeIdentifier(conn, dbinfo->subname, strlen(dbinfo->subname));
 
-	pg_log_info("enabling subscription \"%s\" on database \"%s\"",
-				dbinfo->subname, dbinfo->dbname);
+	log_if_not_dry_run("enabling subscription \"%s\" on database \"%s\"",
+					   dbinfo->subname, dbinfo->dbname);
 
 	appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", subname);
 
-- 
2.34.1

v3-0003-Disable-slot-sync-during-the-convertion.patchapplication/octet-stream; name=v3-0003-Disable-slot-sync-during-the-convertion.patchDownload
From 1ea33b061d692b2b890e983104b4afd304200704 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 9 May 2024 02:20:40 +0000
Subject: [PATCH v3 3/5] Disable slot sync during the convertion

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 72322babd3..71ce16113c 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1349,6 +1349,19 @@ start_standby_server(const struct CreateSubscriberOptions *opt, bool restricted_
 	if (opt->config_file != NULL)
 		appendPQExpBuffer(pg_ctl_cmd, " -o \"-c config_file=%s\"",
 						  opt->config_file);
+
+	/*
+	 * We set up some recovery parameters in the middle part of the
+	 * pg_createsubscriber to ensure all changes are caught up. At that time,
+	 * primary_conninfo will be set without the dbname attribute. Basically, it
+	 * works well, but if the slot sync is enabled, the slot sync worker will
+	 * exit with ERROR because the process requires dbname within
+	 * primary_conninfo. To avoid the error, we override sync_replication_slots
+	 * to off. The synchronization won't happen after the conversion, so it is
+	 * acceptable to stop here as well.
+	 */
+	appendPQExpBuffer(pg_ctl_cmd, " -o \"-c sync_replication_slots=off\"");
+
 	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
 	rc = system(pg_ctl_cmd->data);
 	pg_ctl_status(pg_ctl_cmd->data, rc);
-- 
2.34.1

v3-0001-Improve-the-code-that-checks-if-the-recovery-is-f.patchapplication/octet-stream; name=v3-0001-Improve-the-code-that-checks-if-the-recovery-is-f.patchDownload
From 1a4fe8c190ae6f08d881c1e423b9f530c122aff8 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 24 May 2024 14:08:07 +0530
Subject: [PATCH v3 1/5] Improve the code that checks if the recovery is
 finishing

The recovery process has a window between the walreceiver shutdown and
the pg_is_in_recovery function returns false. It means that the
pg_stat_wal_receiver checks can cause the server to finish the recovery
(even if it already reaches the recovery target). So instead of using
the pg_stat_wal_receiver we can use pg_last_wal_receive_lsn to get
last received lsn on standby and check if the consistent_lsn is
already present on standby and check if the recovery is finished.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 63 +++++++++++++--------
 1 file changed, 39 insertions(+), 24 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 90cc580811..d602aac405 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -29,6 +29,7 @@
 #include "getopt_long.h"
 
 #define	DEFAULT_SUB_PORT	"50432"
+#define NUM_ATTEMPTS		60
 
 /* Command-line options */
 struct CreateSubscriberOptions
@@ -94,7 +95,8 @@ static void start_standby_server(const struct CreateSubscriberOptions *opt,
 								 bool restricted_access);
 static void stop_standby_server(const char *datadir);
 static void wait_for_end_recovery(const char *conninfo,
-								  const struct CreateSubscriberOptions *opt);
+								  const struct CreateSubscriberOptions *opt,
+								  const char *consistent_lsn);
 static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
 static void drop_publication(PGconn *conn, struct LogicalRepInfo *dbinfo);
 static void create_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo);
@@ -1362,57 +1364,70 @@ stop_standby_server(const char *datadir)
  * the recovery process. By default, it waits forever.
  */
 static void
-wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions *opt)
+wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions *opt, const char *consistent_lsn)
 {
 	PGconn	   *conn;
+	int			len1;
 	int			status = POSTMASTER_STILL_STARTING;
 	int			timer = 0;
-	int			count = 0;		/* number of consecutive connection attempts */
-
-#define NUM_CONN_ATTEMPTS	10
+	uint32		id;
+	uint32		off;
+	uint64		target_lsn;
 
 	pg_log_info("waiting for the target server to reach the consistent state");
 
+	/*
+	 * Get consistent_lsn as unsigned 64-bit integer.
+	 */
+	if (!dry_run)
+	{
+		len1 = strspn(consistent_lsn, "0123456789abcdefABCDEF");
+		id = (uint32) strtoul(consistent_lsn, NULL, 16);
+		off = (uint32) strtoul(consistent_lsn + len1 + 1, NULL, 16);
+		target_lsn = ((uint64) id << 32) | off;
+	}
+
 	conn = connect_database(conninfo, true);
 
 	for (;;)
 	{
 		PGresult   *res;
+		uint64	   	lsn;
 		bool		in_recovery = server_is_in_recovery(conn);
 
 		/*
-		 * Does the recovery process finish? In dry run mode, there is no
-		 * recovery mode. Bail out as the recovery process has ended.
+		 * In dry run mode, there is no recovery mode.
 		 */
-		if (!in_recovery || dry_run)
-		{
+		if (dry_run) {
 			status = POSTMASTER_READY;
 			recovery_ended = true;
 			break;
 		}
 
 		/*
-		 * If it is still in recovery, make sure the target server is
-		 * connected to the primary so it can receive the required WAL to
-		 * finish the recovery process. If it is disconnected try
-		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
+		 * If the consistent_lsn is already present on standby and the
+		 * recovery is finished then bail out.
 		 */
 		res = PQexec(conn,
-					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
-		if (PQntuples(res) == 0)
+					 "SELECT pg_catalog.pg_last_wal_receive_lsn() - '0/0'");
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		{
-			if (++count > NUM_CONN_ATTEMPTS)
-			{
-				stop_standby_server(subscriber_dir);
-				pg_log_error("standby server disconnected from the primary");
-				break;
-			}
+			pg_log_error("could not get last wal recieved on standby: %s",
+						 PQresultErrorMessage(res));
+			PQclear(res);
+			break;
 		}
-		else
-			count = 0;			/* reset counter if it connects again */
 
+		lsn = strtoul(PQgetvalue(res, 0, 0), NULL, 10);
 		PQclear(res);
 
+		if(lsn >= target_lsn && !in_recovery)
+		{
+			status = POSTMASTER_READY;
+			recovery_ended = true;
+			break;
+		}
+
 		/* Bail out after recovery_timeout seconds if this option is set */
 		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
 		{
@@ -2121,7 +2136,7 @@ main(int argc, char **argv)
 	start_standby_server(&opt, true);
 
 	/* Waiting the subscriber to be promoted */
-	wait_for_end_recovery(dbinfo[0].subconninfo, &opt);
+	wait_for_end_recovery(dbinfo[0].subconninfo, &opt, consistent_lsn);
 
 	/*
 	 * Create the subscription for each database on subscriber. It does not
-- 
2.34.1

v3-0004-Drop-replication-slots-which-had-been-synchronize.patchapplication/octet-stream; name=v3-0004-Drop-replication-slots-which-had-been-synchronize.patchDownload
From 02658c31ec81eaeccc8a46d55f7f96303857ec53 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Thu, 9 May 2024 12:53:36 +0000
Subject: [PATCH v3 4/5] Drop replication slots which had been synchronized

After the transformation, the slotsync worker won't be available on the standby.
If synchronized slots are created, they do not consume changes, it may lead
PANIC. To avoid the issue, drop such slots at the end of the command.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 64 +++++++++++++++++++++
 1 file changed, 64 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 71ce16113c..e07327cc52 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -86,6 +86,7 @@ static void setup_recovery(const struct LogicalRepInfo *dbinfo, const char *data
 						   const char *lsn);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  const char *slotname);
+static void drop_replication_slot_on_subscriber(struct LogicalRepInfo *dbinfo);
 static char *create_logical_replication_slot(PGconn *conn,
 											 struct LogicalRepInfo *dbinfo);
 static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
@@ -1195,6 +1196,61 @@ drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, const char *slotnam
 	}
 }
 
+/*
+ * Drop logical replication slot on subscriber if they had been synchronized.
+ * After the transformation, they won't be updated.
+ */
+static void
+drop_replication_slot_on_subscriber(struct LogicalRepInfo *dbinfo)
+{
+	for (int i = 0; i < num_dbs; i++)
+	{
+		PGconn	   *conn;
+		PGresult   *res;
+
+		/* Connect to the subscriber */
+		conn = connect_database(dbinfo[i].subconninfo, false);
+
+		if (conn == NULL)
+		{
+			pg_log_warning("could not connect to the database \"%s\" on standby",
+						   dbinfo[i].dbname);
+			pg_log_warning_hint("You may have to drop replication slots which had been synchronized.");
+			continue;
+		}
+
+		/* Extract slots which had been synchronized. */
+		res = PQexec(conn,
+					 "SELECT slot_name FROM pg_catalog.pg_replication_slots "
+					 "WHERE failover;");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_warning("could not obtain synchronized slots information: %s",
+						   PQresultErrorMessage(res));
+			pg_log_warning_hint("You may have to drop replication slots which had been synchronized.");
+			disconnect_database(conn, false);
+			continue;
+		}
+
+		/* Skip if not found */
+		if (PQntuples(res) == 0)
+		{
+			PQclear(res);
+			disconnect_database(conn, false);
+			continue;
+		}
+
+		/* Drop all replication slots extracted by the query */
+		for (int j = 0; j < PQntuples(res); j++)
+			drop_replication_slot(conn, &dbinfo[i], PQgetvalue(res, j, 0));
+
+		/* Cleanup */
+		PQclear(res);
+		disconnect_database(conn, false);
+	}
+}
+
 /*
  * Create a logical replication slot and returns a LSN.
  *
@@ -2177,6 +2233,14 @@ main(int argc, char **argv)
 	/* Remove primary_slot_name if it exists on primary */
 	drop_primary_replication_slot(dbinfo, primary_slot_name);
 
+	/*
+	 * After the transformation, the slotsync worker won't be available on the
+	 * standby. If synchronized slots are created, they do not consume changes,
+	 * it may lead PANIC. To avoid the issue, drop such slots at the end of the
+	 * command.
+	 */
+	drop_replication_slot_on_subscriber(dbinfo);
+
 	/* Stop the subscriber */
 	pg_log_info("stopping the subscriber");
 	stop_standby_server(subscriber_dir);
-- 
2.34.1

#264Euler Taveira
euler@eulerto.com
In reply to: Euler Taveira (#259)
4 attachment(s)
Re: speed up a logical replica setup

On Wed, May 22, 2024, at 12:16 PM, Euler Taveira wrote:

I'll summarize all issues as soon as I finish the review of sync slot support. I
think we should avoid new development if we judge that the item can be
documented as a limitation for this version. Nevertheless, I will share patches
so you can give your opinion on whether it is an open item or new development.

Here it is a patch series to fix the issues reported in recent discussions. The
patches 0001 and 0003 aim to fix the buildfarm issues. The patch 0002 removes
synchronized failover slots on subscriber since it has no use. I also included
an optional patch 0004 that improves the usability by checking both servers if
it already failed in any subscriber check.

As I said in this previous email I decided to remove the logic that reacts for
an issue on primary. We can reintroduce another code later if/when we have a
better way to check the recovery progress. It will rely on the recovery_timeout
and it adds recovery_timeout equals to PG_TEST_TIMEOUT_DEFAULT to let the
animals control how long it should wait for the recovery.

Since some animals reported some issues in the check_publisher routine that
checks if the primary_slot_name is in use on primary, this logic was removed
too (patch 0003). We could introduce a way to keep trying this test but the
conclusion is that it is not worth it and if the primary_slot_name does not
exist (due to a setup error), pg_createsubscriber will log an error message and
continue.

The 0002 removes any failover slots that remains on subscriber. Talking about
terminology, I noticed that slotsync.c uses "logical failover slots" and
"failover logical slots", I think the latter sounds better but I'm not a native
speaker. I also don't know if we use a short terminology like "failover slots"
"failover replication slots" or "failover logical replication slots". IMO we
can omit "logical" because "failover" infers it is a logical replication slot.
I'm also not sure about omitting "replication". It is not descriptive enough. I
prefer "failover replication slots".

Before sending this email I realized that I did nothing about physical
replication slots on the standby. I think we should also remove them too
unconditionally.

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

0001-Only-the-recovery_timeout-controls-the-end-of-recove.patchtext/x-patch; name="=?UTF-8?Q?0001-Only-the-recovery=5Ftimeout-controls-the-end-of-recove.pa?= =?UTF-8?Q?tch?="Download
From 5d8b4781e6e9bcb00564f45c25e575a8abab6ae8 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Tue, 21 May 2024 23:04:57 -0300
Subject: [PATCH 1/4] Only the recovery_timeout controls the end of recovery
 process

It used to check if the target server is connected to the primary server
(send required WAL) to rapidly react when the process won't succeed.
This code is not enough to guarantee that the recovery process will
complete. There is a window between the walreceiver shutdown and the
pg_is_in_recovery() returns false that can reach NUM_CONN_ATTEMPTS
attempts and fails.
Instead, rely only on recovery_timeout option to give up the process
after the specified number of seconds.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  7 -----
 src/bin/pg_basebackup/pg_createsubscriber.c   | 29 ++-----------------
 .../t/040_pg_createsubscriber.pl              |  2 ++
 3 files changed, 5 insertions(+), 33 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 142bffff02..a700697f88 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -325,13 +325,6 @@ PostgreSQL documentation
     connections to the target server should fail.
    </para>
 
-   <para>
-    During the recovery process, if the target server disconnects from the
-    source server, <application>pg_createsubscriber</application> will check a
-    few times if the connection has been reestablished to stream the required
-    WAL.  After a few attempts, it terminates with an error.
-   </para>
-
    <para>
     Since DDL commands are not replicated by logical replication, avoid
     executing DDL commands that change the database schema while running
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 90cc580811..f62f34b1a7 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1360,6 +1360,9 @@ stop_standby_server(const char *datadir)
  *
  * If recovery_timeout option is set, terminate abnormally without finishing
  * the recovery process. By default, it waits forever.
+ *
+ * XXX Is the recovery process still in progress? When recovery process has a
+ * better progress reporting mechanism, it should be added here.
  */
 static void
 wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions *opt)
@@ -1367,9 +1370,6 @@ wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions
 	PGconn	   *conn;
 	int			status = POSTMASTER_STILL_STARTING;
 	int			timer = 0;
-	int			count = 0;		/* number of consecutive connection attempts */
-
-#define NUM_CONN_ATTEMPTS	10
 
 	pg_log_info("waiting for the target server to reach the consistent state");
 
@@ -1377,7 +1377,6 @@ wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions
 
 	for (;;)
 	{
-		PGresult   *res;
 		bool		in_recovery = server_is_in_recovery(conn);
 
 		/*
@@ -1391,28 +1390,6 @@ wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions
 			break;
 		}
 
-		/*
-		 * If it is still in recovery, make sure the target server is
-		 * connected to the primary so it can receive the required WAL to
-		 * finish the recovery process. If it is disconnected try
-		 * NUM_CONN_ATTEMPTS in a row and bail out if not succeed.
-		 */
-		res = PQexec(conn,
-					 "SELECT 1 FROM pg_catalog.pg_stat_wal_receiver");
-		if (PQntuples(res) == 0)
-		{
-			if (++count > NUM_CONN_ATTEMPTS)
-			{
-				stop_standby_server(subscriber_dir);
-				pg_log_error("standby server disconnected from the primary");
-				break;
-			}
-		}
-		else
-			count = 0;			/* reset counter if it connects again */
-
-		PQclear(res);
-
 		/* Bail out after recovery_timeout seconds if this option is set */
 		if (opt->recovery_timeout > 0 && timer >= opt->recovery_timeout)
 		{
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 2b883e6910..a677fefa94 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -264,6 +264,7 @@ $node_p->restart;
 command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
+		'--recovery-timeout', "$PostgreSQL::Test::Utils::timeout_default",
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
 		$node_p->connstr('pg1'), '--socket-directory',
@@ -301,6 +302,7 @@ command_ok(
 command_ok(
 	[
 		'pg_createsubscriber', '--verbose',
+		'--recovery-timeout', "$PostgreSQL::Test::Utils::timeout_default",
 		'--verbose', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
 		$node_p->connstr('pg1'), '--socket-directory',
-- 
2.30.2

0002-Remove-failover-replication-slots-on-subscriber.patchtext/x-patch; name="=?UTF-8?Q?0002-Remove-failover-replication-slots-on-subscriber.patch?="Download
From a10edc03a6636d6fef9e013a507a8258096a6fd3 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Wed, 22 May 2024 12:43:13 -0300
Subject: [PATCH 2/4] Remove failover replication slots on subscriber

After running pg_createsubscriber, these replication slots has no use on
subscriber.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  8 +++
 src/bin/pg_basebackup/pg_createsubscriber.c   | 49 ++++++++++++++++++-
 .../t/040_pg_createsubscriber.pl              | 20 +++++++-
 3 files changed, 75 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index a700697f88..ac56c0cce9 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -482,6 +482,14 @@ PostgreSQL documentation
      </para>
     </step>
 
+    <step>
+     <para>
+      If the standby server contains
+      <link linkend="logicaldecoding-replication-slots-synchronization">failover replication slots</link>,
+      they cannot be synchronized anymore so drop them.
+     </para>
+    </step>
+
     <step>
      <para>
       Update the system identifier on the target server. The
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index f62f34b1a7..b42160c6c6 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -85,6 +85,7 @@ static void setup_recovery(const struct LogicalRepInfo *dbinfo, const char *data
 						   const char *lsn);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  const char *slotname);
+static void drop_failover_replication_slots(struct LogicalRepInfo *dbinfo);
 static char *create_logical_replication_slot(PGconn *conn,
 											 struct LogicalRepInfo *dbinfo);
 static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
@@ -1178,6 +1179,49 @@ drop_primary_replication_slot(struct LogicalRepInfo *dbinfo, const char *slotnam
 	}
 }
 
+/*
+ * Drop failover replication slots on subscriber. After the transformation, it
+ * has no use.
+ *
+ * XXX we might not fail here. Instead, we provide a warning so the user
+ * eventually drops this replication slot later.
+ */
+static void
+drop_failover_replication_slots(struct LogicalRepInfo *dbinfo)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	conn = connect_database(dbinfo[0].subconninfo, false);
+	if (conn != NULL)
+	{
+		/* Get failover replication slot names. */
+		res = PQexec(conn,
+					 "SELECT slot_name FROM pg_catalog.pg_replication_slots WHERE failover");
+
+		if (PQresultStatus(res) == PGRES_TUPLES_OK)
+		{
+			/* Remove failover replication slots from subscriber. */
+			for (int i = 0; i < PQntuples(res); i++)
+				drop_replication_slot(conn, &dbinfo[0], PQgetvalue(res, i, 0));
+		}
+		else
+		{
+			pg_log_warning("could not obtain failover replication slot information: %s",
+						   PQresultErrorMessage(res));
+			pg_log_warning_hint("Drop the failover replication slots on subscriber soon to avoid retention of WAL files.");
+		}
+
+		PQclear(res);
+		disconnect_database(conn, false);
+	}
+	else
+	{
+		pg_log_warning("could not drop failover replication slot");
+		pg_log_warning_hint("Drop the failover replication slots on subscriber soon to avoid retention of WAL files.");
+	}
+}
+
 /*
  * Create a logical replication slot and returns a LSN.
  *
@@ -1309,7 +1353,7 @@ start_standby_server(const struct CreateSubscriberOptions *opt, bool restricted_
 	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
 	int			rc;
 
-	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s -o \"-c sync_replication_slots=off\"",
 					  pg_ctl_path, subscriber_dir);
 	if (restricted_access)
 	{
@@ -2111,6 +2155,9 @@ main(int argc, char **argv)
 	/* Remove primary_slot_name if it exists on primary */
 	drop_primary_replication_slot(dbinfo, primary_slot_name);
 
+	/* Remove failover  replication slots if it exists on subscriber */
+	drop_failover_replication_slots(dbinfo);
+
 	/* Stop the subscriber */
 	pg_log_info("stopping the subscriber");
 	stop_standby_server(subscriber_dir);
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index a677fefa94..0516d4e17e 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -88,6 +88,7 @@ command_fails(
 
 # Set up node P as primary
 my $node_p = PostgreSQL::Test::Cluster->new('node_p');
+my $pconnstr = $node_p->connstr;
 $node_p->init(allows_streaming => 'logical');
 $node_p->start;
 
@@ -122,6 +123,8 @@ $node_s->init_from_backup($node_p, 'backup_1', has_streaming => 1);
 $node_s->append_conf(
 	'postgresql.conf', qq[
 primary_slot_name = '$slotname'
+primary_conninfo = '$pconnstr dbname=postgres'
+hot_standby_feedback = on
 ]);
 $node_s->set_standby_mode();
 $node_s->start;
@@ -260,6 +263,16 @@ max_worker_processes = 8
 # Restore default settings on both servers
 $node_p->restart;
 
+# Create failover slot to test its removal
+my $fslotname = 'failover_slot';
+$node_p->safe_psql('pg1',
+	"SELECT pg_create_logical_replication_slot('$fslotname', 'pgoutput', false, false, true)");
+$node_s->start;
+$node_s->safe_psql('postgres', "SELECT pg_sync_replication_slots()");
+my $result = $node_s->safe_psql('postgres', "SELECT slot_name FROM pg_replication_slots WHERE slot_name = '$fslotname' AND synced AND NOT temporary");
+is($result, 'failover_slot', 'failover slot is synced');
+$node_s->stop;
+
 # dry run mode on node S
 command_ok(
 	[
@@ -318,7 +331,7 @@ command_ok(
 	'run pg_createsubscriber on node S');
 
 # Confirm the physical replication slot has been removed
-my $result = $node_p->safe_psql('pg1',
+$result = $node_p->safe_psql('pg1',
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
 );
 is($result, qq(0),
@@ -343,6 +356,11 @@ my @subnames = split("\n", $result);
 $node_s->wait_for_subscription_sync($node_p, $subnames[0]);
 $node_s->wait_for_subscription_sync($node_p, $subnames[1]);
 
+# Confirm the failover slot has been removed
+$result = $node_s->safe_psql('pg1',
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$fslotname'");
+is($result, qq(0), 'failover slot was removed');
+
 # Check result on database pg1
 $result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
 is( $result, qq(first row
-- 
2.30.2

0003-Remove-replication-slot-check-on-primary.patchtext/x-patch; name="=?UTF-8?Q?0003-Remove-replication-slot-check-on-primary.patch?="Download
From f085fbef4ecd31aed470d858e57e9ea3b8014493 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Fri, 24 May 2024 14:18:15 -0300
Subject: [PATCH 3/4] Remove replication slot check on primary

It used to check if the replication slot exists and is active on
primary. This check might fail on slow hosts because the replication
slot might not be active at the time of this check.

The current code obtains the replication slot name from the
primary_slot_name on standby and assumes the replication slot exists and
is active on primary. If it doesn't exist, this tool will log an error
and continue.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 53 ++-------------------
 1 file changed, 5 insertions(+), 48 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index b42160c6c6..6d5127c45e 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -880,47 +880,6 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
 	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
 
-	/*
-	 * If standby sets primary_slot_name, check if this replication slot is in
-	 * use on primary for WAL retention purposes. This replication slot has no
-	 * use after the transformation, hence, it will be removed at the end of
-	 * this process.
-	 */
-	if (primary_slot_name)
-	{
-		PQExpBuffer str = createPQExpBuffer();
-		char	   *psn_esc = PQescapeLiteral(conn, primary_slot_name, strlen(primary_slot_name));
-
-		appendPQExpBuffer(str,
-						  "SELECT 1 FROM pg_catalog.pg_replication_slots "
-						  "WHERE active AND slot_name = %s",
-						  psn_esc);
-
-		pg_free(psn_esc);
-
-		pg_log_debug("command is: %s", str->data);
-
-		res = PQexec(conn, str->data);
-		if (PQresultStatus(res) != PGRES_TUPLES_OK)
-		{
-			pg_log_error("could not obtain replication slot information: %s",
-						 PQresultErrorMessage(res));
-			disconnect_database(conn, true);
-		}
-
-		if (PQntuples(res) != 1)
-		{
-			pg_log_error("could not obtain replication slot information: got %d rows, expected %d row",
-						 PQntuples(res), 1);
-			disconnect_database(conn, true);
-		}
-		else
-			pg_log_info("primary has replication slot \"%s\"",
-						primary_slot_name);
-
-		PQclear(res);
-	}
-
 	disconnect_database(conn, false);
 
 	if (strcmp(wal_level, "logical") != 0)
@@ -2105,12 +2064,7 @@ main(int argc, char **argv)
 	/* Check if the standby server is ready for logical replication */
 	check_subscriber(dbinfo);
 
-	/*
-	 * Check if the primary server is ready for logical replication. This
-	 * routine checks if a replication slot is in use on primary so it relies
-	 * on check_subscriber() to obtain the primary_slot_name. That's why it is
-	 * called after it.
-	 */
+	/* Check if the primary server is ready for logical replication */
 	check_publisher(dbinfo);
 
 	/*
@@ -2152,7 +2106,10 @@ main(int argc, char **argv)
 	 */
 	setup_subscriber(dbinfo, consistent_lsn);
 
-	/* Remove primary_slot_name if it exists on primary */
+	/*
+	 * Remove primary_slot_name if it exists on primary.  This replication slot
+	 * has no use after the transformation.
+	 */
 	drop_primary_replication_slot(dbinfo, primary_slot_name);
 
 	/* Remove failover  replication slots if it exists on subscriber */
-- 
2.30.2

0004-Check-both-servers-before-exiting.patchtext/x-patch; name=0004-Check-both-servers-before-exiting.patchDownload
From a4f10c849f7584c4fb12005cec04b5d09701752f Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Fri, 24 May 2024 14:29:06 -0300
Subject: [PATCH 4/4] Check both servers before exiting

The current code provides multiple errors for each server. If any
subscriber condition is not met, it terminates without checking the
publisher conditions.
This change allows it to check both servers before terminating if any
condition is not met. It saves at least one dry run execution.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 24 ++++++++++++---------
 1 file changed, 14 insertions(+), 10 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 6d5127c45e..b0233786d4 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -76,9 +76,9 @@ static uint64 get_standby_sysid(const char *datadir);
 static void modify_subscriber_sysid(const struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static char *generate_object_name(PGconn *conn);
-static void check_publisher(const struct LogicalRepInfo *dbinfo);
+static bool check_publisher(const struct LogicalRepInfo *dbinfo);
 static char *setup_publisher(struct LogicalRepInfo *dbinfo);
-static void check_subscriber(const struct LogicalRepInfo *dbinfo);
+static bool check_subscriber(const struct LogicalRepInfo *dbinfo);
 static void setup_subscriber(struct LogicalRepInfo *dbinfo,
 							 const char *consistent_lsn);
 static void setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir,
@@ -803,7 +803,7 @@ server_is_in_recovery(PGconn *conn)
  *
  * XXX Does it not allow a synchronous replica?
  */
-static void
+static bool
 check_publisher(const struct LogicalRepInfo *dbinfo)
 {
 	PGconn	   *conn;
@@ -908,8 +908,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 
 	pg_free(wal_level);
 
-	if (failed)
-		exit(1);
+	return failed;
 }
 
 /*
@@ -923,7 +922,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
  * there is not a reliable way to provide a suitable error saying the server C
  * will be broken at the end of this process (due to pg_resetwal).
  */
-static void
+static bool
 check_subscriber(const struct LogicalRepInfo *dbinfo)
 {
 	PGconn	   *conn;
@@ -1014,8 +1013,7 @@ check_subscriber(const struct LogicalRepInfo *dbinfo)
 		failed = true;
 	}
 
-	if (failed)
-		exit(1);
+	return failed;
 }
 
 /*
@@ -1771,6 +1769,9 @@ main(int argc, char **argv)
 
 	char		pidfile[MAXPGPATH];
 
+	bool		pub_failed = false;
+	bool		sub_failed = false;
+
 	pg_logging_init(argv[0]);
 	pg_logging_set_level(PG_LOG_WARNING);
 	progname = get_progname(argv[0]);
@@ -2062,10 +2063,13 @@ main(int argc, char **argv)
 	start_standby_server(&opt, true);
 
 	/* Check if the standby server is ready for logical replication */
-	check_subscriber(dbinfo);
+	sub_failed = check_subscriber(dbinfo);
 
 	/* Check if the primary server is ready for logical replication */
-	check_publisher(dbinfo);
+	pub_failed = check_publisher(dbinfo);
+
+	if (pub_failed || sub_failed)
+		exit(1);
 
 	/*
 	 * Stop the target server. The recovery process requires that the server
-- 
2.30.2

#265Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#264)
1 attachment(s)
RE: speed up a logical replica setup

Dear Euler,

Thanks for making the follow-up patch! I was looking forward to your updates.
I think this patch set is the solution for the found buildfarm error. However,
there are remained claims raised by others. You should reply what you think for
them. At least:

1) There are some misleading messages [1]/messages/by-id/CAA4eK1J2fAvsJ2HihbWJ_GxETd6sdqSMrZdCVJEutRZRpm1MEQ@mail.gmail.com. I think v3-0005 patch set can solve
the issue.
2) pg_createsubscriber may fail If the primary has subscriptions [2]/messages/by-id/CANhcyEWvimA1-f6hSrA=9qkfR5SonFb56b36M++vT=LiFj=76g@mail.gmail.com. IIUC
possible approaches are A)"keep subscriptions disabled at the end",
B)"by default drop the pre-existing subscriptions",
C) "do nothing, just document the risk".

Before sending this email I realized that I did nothing about physical
replication slots on the standby. I think we should also remove them too
unconditionally.

I also considered around here, but it might be difficult to predict the
expectation by users. Can we surely say that it's not intentional one? Regarding
the failover slot, it is OK because that's meaningful only on the standby,
but not sure other slots. I personally think we can keep current spec, but
how do other think?

Below parts are comments for each patches.

0001
Basically LGTM. I was bit confused because the default timeout is not set, but
it seemed to follow the suggestion by Tomas [3]/messages/by-id/5d5dd4cd-6359-4109-88e8-c8e13035ae16@enterprisedb.com.

0002
If you want to improve the commit message, please add that sync_replication_slots
is disabled during the conversion.

0003
Confirmed it followed the discussion [4]/messages/by-id/CAA4eK1LZxYxcbeiOn3Q5hjXVtZKhJWj-fQtndAeTCvZrPev8BA@mail.gmail.com.

0004
Basically LGTM.

Other minor comments are included by the attached diff file. It contains changes
to follow conventions and pgindent/pgperltidy.

[1]: /messages/by-id/CAA4eK1J2fAvsJ2HihbWJ_GxETd6sdqSMrZdCVJEutRZRpm1MEQ@mail.gmail.com
[2]: /messages/by-id/CANhcyEWvimA1-f6hSrA=9qkfR5SonFb56b36M++vT=LiFj=76g@mail.gmail.com
[3]: /messages/by-id/5d5dd4cd-6359-4109-88e8-c8e13035ae16@enterprisedb.com
[4]: /messages/by-id/CAA4eK1LZxYxcbeiOn3Q5hjXVtZKhJWj-fQtndAeTCvZrPev8BA@mail.gmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/global/

Attachments:

minor_fix_by_kuroda.diffapplication/octet-stream; name=minor_fix_by_kuroda.diffDownload
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index b0233786d4..77ae17073a 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -1152,13 +1152,13 @@ drop_failover_replication_slots(struct LogicalRepInfo *dbinfo)
 	conn = connect_database(dbinfo[0].subconninfo, false);
 	if (conn != NULL)
 	{
-		/* Get failover replication slot names. */
+		/* Get failover replication slot names */
 		res = PQexec(conn,
 					 "SELECT slot_name FROM pg_catalog.pg_replication_slots WHERE failover");
 
 		if (PQresultStatus(res) == PGRES_TUPLES_OK)
 		{
-			/* Remove failover replication slots from subscriber. */
+			/* Remove failover replication slots from subscriber */
 			for (int i = 0; i < PQntuples(res); i++)
 				drop_replication_slot(conn, &dbinfo[0], PQgetvalue(res, i, 0));
 		}
@@ -1310,7 +1310,11 @@ start_standby_server(const struct CreateSubscriberOptions *opt, bool restricted_
 	PQExpBuffer pg_ctl_cmd = createPQExpBuffer();
 	int			rc;
 
-	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s -o \"-c sync_replication_slots=off\"",
+	/*
+	 * FIXME: appending "sync_replication_slots=off" is not trivial for reader.
+	 * Please separate it into the later part with comments.
+	 */
+	appendPQExpBuffer(pg_ctl_cmd, "\"%s\" start -D \"%s\" -s",
 					  pg_ctl_path, subscriber_dir);
 	if (restricted_access)
 	{
@@ -1333,6 +1337,24 @@ start_standby_server(const struct CreateSubscriberOptions *opt, bool restricted_
 	if (opt->config_file != NULL)
 		appendPQExpBuffer(pg_ctl_cmd, " -o \"-c config_file=%s\"",
 						  opt->config_file);
+
+	/*
+	 * FIXME: please describe why the sync_replication_slots is set to false.
+	 * I.e.,
+	 */
+
+	/*
+	 * We set up some recovery parameters in the middle part of the
+	 * pg_createsubscriber to ensure all changes are caught up. At that time,
+	 * primary_conninfo will be set without the dbname attribute. Basically, it
+	 * works well, but if the slot sync is enabled, the slot sync worker will
+	 * exit with ERROR because the process requires dbname within
+	 * primary_conninfo. To avoid the error, we override sync_replication_slots
+	 * to off. The synchronization won't happen after the conversion, so it is
+	 * acceptable to stop here as well.
+	 */
+	appendPQExpBuffer(pg_ctl_cmd, " -o \"-c sync_replication_slots=off\"");
+
 	pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data);
 	rc = system(pg_ctl_cmd->data);
 	pg_ctl_status(pg_ctl_cmd->data, rc);
@@ -2068,6 +2090,7 @@ main(int argc, char **argv)
 	/* Check if the primary server is ready for logical replication */
 	pub_failed = check_publisher(dbinfo);
 
+	/* Exit if either of them is not ready */
 	if (pub_failed || sub_failed)
 		exit(1);
 
@@ -2111,8 +2134,8 @@ main(int argc, char **argv)
 	setup_subscriber(dbinfo, consistent_lsn);
 
 	/*
-	 * Remove primary_slot_name if it exists on primary.  This replication slot
-	 * has no use after the transformation.
+	 * Remove primary_slot_name if it exists on primary.  This replication
+	 * slot has no use after the transformation.
 	 */
 	drop_primary_replication_slot(dbinfo, primary_slot_name);
 
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 0516d4e17e..cc65942dc4 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -266,28 +266,43 @@ $node_p->restart;
 # Create failover slot to test its removal
 my $fslotname = 'failover_slot';
 $node_p->safe_psql('pg1',
-	"SELECT pg_create_logical_replication_slot('$fslotname', 'pgoutput', false, false, true)");
+	"SELECT pg_create_logical_replication_slot('$fslotname', 'pgoutput', false, false, true)"
+);
 $node_s->start;
 $node_s->safe_psql('postgres', "SELECT pg_sync_replication_slots()");
-my $result = $node_s->safe_psql('postgres', "SELECT slot_name FROM pg_replication_slots WHERE slot_name = '$fslotname' AND synced AND NOT temporary");
+my $result = $node_s->safe_psql('postgres',
+	"SELECT slot_name FROM pg_replication_slots WHERE slot_name = '$fslotname' AND synced AND NOT temporary"
+);
 is($result, 'failover_slot', 'failover slot is synced');
 $node_s->stop;
 
 # dry run mode on node S
 command_ok(
 	[
-		'pg_createsubscriber', '--verbose',
-		'--recovery-timeout', "$PostgreSQL::Test::Utils::timeout_default",
-		'--dry-run', '--pgdata',
-		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
-		$node_s->host, '--subscriber-port',
-		$node_s->port, '--publication',
-		'pub1', '--publication',
-		'pub2', '--subscription',
-		'sub1', '--subscription',
-		'sub2', '--database',
-		'pg1', '--database',
+		'pg_createsubscriber',
+		'--verbose',
+		'--recovery-timeout',
+		"$PostgreSQL::Test::Utils::timeout_default",
+		'--dry-run',
+		'--pgdata',
+		$node_s->data_dir,
+		'--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory',
+		$node_s->host,
+		'--subscriber-port',
+		$node_s->port,
+		'--publication',
+		'pub1',
+		'--publication',
+		'pub2',
+		'--subscription',
+		'sub1',
+		'--subscription',
+		'sub2',
+		'--database',
+		'pg1',
+		'--database',
 		'pg2'
 	],
 	'run pg_createsubscriber --dry-run on node S');
@@ -314,18 +329,30 @@ command_ok(
 # Run pg_createsubscriber on node S
 command_ok(
 	[
-		'pg_createsubscriber', '--verbose',
-		'--recovery-timeout', "$PostgreSQL::Test::Utils::timeout_default",
-		'--verbose', '--pgdata',
-		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
-		$node_s->host, '--subscriber-port',
-		$node_s->port, '--publication',
-		'pub1', '--publication',
-		'Pub2', '--replication-slot',
-		'replslot1', '--replication-slot',
-		'replslot2', '--database',
-		'pg1', '--database',
+		'pg_createsubscriber',
+		'--verbose',
+		'--recovery-timeout',
+		"$PostgreSQL::Test::Utils::timeout_default",
+		'--verbose',
+		'--pgdata',
+		$node_s->data_dir,
+		'--publisher-server',
+		$node_p->connstr('pg1'),
+		'--socket-directory',
+		$node_s->host,
+		'--subscriber-port',
+		$node_s->port,
+		'--publication',
+		'pub1',
+		'--publication',
+		'Pub2',
+		'--replication-slot',
+		'replslot1',
+		'--replication-slot',
+		'replslot2',
+		'--database',
+		'pg1',
+		'--database',
 		'pg2'
 	],
 	'run pg_createsubscriber on node S');
@@ -358,7 +385,8 @@ $node_s->wait_for_subscription_sync($node_p, $subnames[1]);
 
 # Confirm the failover slot has been removed
 $result = $node_s->safe_psql('pg1',
-	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$fslotname'");
+	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$fslotname'"
+);
 is($result, qq(0), 'failover slot was removed');
 
 # Check result on database pg1
#266Peter Eisentraut
peter@eisentraut.org
In reply to: Euler Taveira (#264)
Re: speed up a logical replica setup

On 07.06.24 05:49, Euler Taveira wrote:

Here it is a patch series to fix the issues reported in recent
discussions. The
patches 0001 and 0003 aim to fix the buildfarm issues. The patch 0002
removes
synchronized failover slots on subscriber since it has no use. I also
included
an optional patch 0004 that improves the usability by checking both
servers if
it already failed in any subscriber check.

I have committed 0001, 0002, and 0003. Let's keep an eye on the
buildfarm to see if that stabilizes things. So far it looks good.

For 0004, I suggest inverting the result values from check_publisher()
and create_subscriber() so that it returns true if the check is ok.

#267Peter Eisentraut
peter@eisentraut.org
In reply to: Hayato Kuroda (Fujitsu) (#265)
Re: speed up a logical replica setup

On 07.06.24 11:17, Hayato Kuroda (Fujitsu) wrote:

Other minor comments are included by the attached diff file. It contains changes
to follow conventions and pgindent/pgperltidy.

I have included some of your changes in the patches from Euler that I
committed today. The 0004 patch might get rerolled by Euler, so he
could include your relevant changes there.

I'm not sure about moving the sync_replication_slots setting around. It
seems to add a lot of work for no discernible gain?

The pgperltidy changes are probably better done separately, because they
otherwise make quite a mess of the patch. Maybe we'll just do that once
more before we branch.

#268Euler Taveira
euler@eulerto.com
In reply to: Peter Eisentraut (#266)
1 attachment(s)
Re: speed up a logical replica setup

On Mon, Jun 17, 2024, at 8:04 AM, Peter Eisentraut wrote:

On 07.06.24 05:49, Euler Taveira wrote:

Here it is a patch series to fix the issues reported in recent
discussions. The
patches 0001 and 0003 aim to fix the buildfarm issues. The patch 0002
removes
synchronized failover slots on subscriber since it has no use. I also
included
an optional patch 0004 that improves the usability by checking both
servers if
it already failed in any subscriber check.

I have committed 0001, 0002, and 0003. Let's keep an eye on the
buildfarm to see if that stabilizes things. So far it looks good.

Thanks.

For 0004, I suggest inverting the result values from check_publisher()
and create_subscriber() so that it returns true if the check is ok.

Yeah, the order doesn't matter after the commit b963913826. I thought about
changing the order but I didn't; I provided a minimal patch. Since you think
it is an improvement, I'm attaching another patch with this change.

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

0001-Check-both-servers-before-exiting.patchtext/x-patch; name=0001-Check-both-servers-before-exiting.patchDownload
From ec96586fa35be3f855fb8844a197f374ac82a622 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler.taveira@enterprisedb.com>
Date: Fri, 24 May 2024 14:29:06 -0300
Subject: [PATCH] Check both servers before exiting

The current code provides multiple errors for each server. If any
subscriber condition is not met, it terminates without checking the
publisher conditions.
This change allows it to check both servers before terminating if any
condition is not met. It saves at least one dry run execution.

The order it calls the check functions don't matter after commit
b96391382626306c301b67cbd2d28313d96741f3 (because there is no
primary_slot_name check anymore) so check the publisher first.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 28 ++++++++++++---------
 1 file changed, 16 insertions(+), 12 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 5499e6d96a..26b4087ff7 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -76,9 +76,9 @@ static uint64 get_standby_sysid(const char *datadir);
 static void modify_subscriber_sysid(const struct CreateSubscriberOptions *opt);
 static bool server_is_in_recovery(PGconn *conn);
 static char *generate_object_name(PGconn *conn);
-static void check_publisher(const struct LogicalRepInfo *dbinfo);
+static bool check_publisher(const struct LogicalRepInfo *dbinfo);
 static char *setup_publisher(struct LogicalRepInfo *dbinfo);
-static void check_subscriber(const struct LogicalRepInfo *dbinfo);
+static bool check_subscriber(const struct LogicalRepInfo *dbinfo);
 static void setup_subscriber(struct LogicalRepInfo *dbinfo,
 							 const char *consistent_lsn);
 static void setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir,
@@ -803,7 +803,7 @@ server_is_in_recovery(PGconn *conn)
  *
  * XXX Does it not allow a synchronous replica?
  */
-static void
+static bool
 check_publisher(const struct LogicalRepInfo *dbinfo)
 {
 	PGconn	   *conn;
@@ -908,8 +908,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 
 	pg_free(wal_level);
 
-	if (failed)
-		exit(1);
+	return failed;
 }
 
 /*
@@ -923,7 +922,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
  * there is not a reliable way to provide a suitable error saying the server C
  * will be broken at the end of this process (due to pg_resetwal).
  */
-static void
+static bool
 check_subscriber(const struct LogicalRepInfo *dbinfo)
 {
 	PGconn	   *conn;
@@ -1014,8 +1013,7 @@ check_subscriber(const struct LogicalRepInfo *dbinfo)
 		failed = true;
 	}
 
-	if (failed)
-		exit(1);
+	return failed;
 }
 
 /*
@@ -1771,6 +1769,9 @@ main(int argc, char **argv)
 
 	char		pidfile[MAXPGPATH];
 
+	bool		pub_failed = false;
+	bool		sub_failed = false;
+
 	pg_logging_init(argv[0]);
 	pg_logging_set_level(PG_LOG_WARNING);
 	progname = get_progname(argv[0]);
@@ -2061,11 +2062,14 @@ main(int argc, char **argv)
 	pg_log_info("starting the standby with command-line options");
 	start_standby_server(&opt, true);
 
-	/* Check if the standby server is ready for logical replication */
-	check_subscriber(dbinfo);
-
 	/* Check if the primary server is ready for logical replication */
-	check_publisher(dbinfo);
+	pub_failed = check_publisher(dbinfo);
+
+	/* Check if the standby server is ready for logical replication */
+	sub_failed = check_subscriber(dbinfo);
+
+	if (pub_failed || sub_failed)
+		exit(1);
 
 	/*
 	 * Stop the target server. The recovery process requires that the server
-- 
2.30.2

#269Noah Misch
noah@leadboat.com
In reply to: Peter Eisentraut (#235)
Re: speed up a logical replica setup

On Mon, Mar 25, 2024 at 12:55:39PM +0100, Peter Eisentraut wrote:

I have committed your version v33.

commit d44032d

--- /dev/null
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
+static char *
+concat_conninfo_dbname(const char *conninfo, const char *dbname)
+{
+	PQExpBuffer buf = createPQExpBuffer();
+	char	   *ret;
+
+	Assert(conninfo != NULL);
+
+	appendPQExpBufferStr(buf, conninfo);
+	appendPQExpBuffer(buf, " dbname=%s", dbname);

pg_createsubscriber fails on a dbname containing a space. Use
appendConnStrVal() here and for other params in get_sub_conninfo(). See the
CVE-2016-5424 commits for more background. For one way to test this scenario,
see generate_db() in the pg_upgrade test suite.

+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	const char *slot_name = dbinfo->replslotname;
+	char	   *slot_name_esc;
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name));
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot(%s, 'pgoutput', false, false, false)",

This is passing twophase=false, but the patch does not mention prepared
transactions. Is the intent to not support workloads containing prepared
transactions? If so, the documentation should say that, and the tool likely
should warn on startup if max_prepared_transactions != 0.

+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  ipubname_esc);

This tool's documentation says it "guarantees that no transaction will be
lost." I tried to determine whether achieving that will require something
like the fix from
/messages/by-id/flat/de52b282-1166-1180-45a2-8d8917ca74c6@enterprisedb.com.
(Not exactly the fix from that thread, since that thread has not discussed the
FOR ALL TABLES version of its race condition.) I don't know. On the one
hand, pg_createsubscriber benefits from creating a logical slot after creating
the publication. That snapbuild.c process will wait for running XIDs. On the
other hand, an INSERT/UPDATE/DELETE acquires its RowExclusiveLock and builds
its relcache entry before assigning an XID, so perhaps the snapbuild.c process
isn't enough to prevent that thread's race condition. What do you think?

#270Amit Kapila
amit.kapila16@gmail.com
In reply to: Noah Misch (#269)
Re: speed up a logical replica setup

On Sun, Jun 23, 2024 at 11:52 AM Noah Misch <noah@leadboat.com> wrote:

+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+     PQExpBuffer str = createPQExpBuffer();
+     PGresult   *res = NULL;
+     const char *slot_name = dbinfo->replslotname;
+     char       *slot_name_esc;
+     char       *lsn = NULL;
+
+     Assert(conn != NULL);
+
+     pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+                             slot_name, dbinfo->dbname);
+
+     slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name));
+
+     appendPQExpBuffer(str,
+                                       "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot(%s, 'pgoutput', false, false, false)",

This is passing twophase=false, but the patch does not mention prepared
transactions. Is the intent to not support workloads containing prepared
transactions? If so, the documentation should say that, and the tool likely
should warn on startup if max_prepared_transactions != 0.

The other point to note in this regard is that if we don't support
two_phase in the beginning during subscription/slot setup, users won't
be able to change it as we don't yet support changing it via alter
subscription (though the patch to alter two_pc is proposed for PG18).

--
With Regards,
Amit Kapila.

#271Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Noah Misch (#269)
1 attachment(s)
RE: speed up a logical replica setup

Dear Noah,

pg_createsubscriber fails on a dbname containing a space. Use
appendConnStrVal() here and for other params in get_sub_conninfo(). See the
CVE-2016-5424 commits for more background. For one way to test this
scenario,
see generate_db() in the pg_upgrade test suite.

Thanks for pointing out. I made a fix patch. Test code was also modified accordingly.

+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	PQExpBuffer str = createPQExpBuffer();
+	PGresult   *res = NULL;
+	const char *slot_name = dbinfo->replslotname;
+	char	   *slot_name_esc;
+	char	   *lsn = NULL;
+
+	Assert(conn != NULL);
+
+	pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+				slot_name, dbinfo->dbname);
+
+	slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name));
+
+	appendPQExpBuffer(str,
+					  "SELECT lsn FROM

pg_catalog.pg_create_logical_replication_slot(%s, 'pgoutput', false, false, false)",

This is passing twophase=false, but the patch does not mention prepared
transactions. Is the intent to not support workloads containing prepared
transactions? If so, the documentation should say that, and the tool likely
should warn on startup if max_prepared_transactions != 0.

IIUC, We decided because it is a default behavior of logical replication. See [1]/messages/by-id/270ad9b8-9c46-40c3-b6c5-3d25b91d3a7d@app.fastmail.com.
+1 for improving a documentation, but not sure it is helpful for adding output.
I want to know opinions from others.

+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+	appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+					  ipubname_esc);

This tool's documentation says it "guarantees that no transaction will be
lost." I tried to determine whether achieving that will require something
like the fix from
/messages/by-id/flat/de52b282-1166-1180-45a2-8d8917ca74c6@enterprise
db.com.
(Not exactly the fix from that thread, since that thread has not discussed the
FOR ALL TABLES version of its race condition.) I don't know. On the one
hand, pg_createsubscriber benefits from creating a logical slot after creating
the publication. That snapbuild.c process will wait for running XIDs. On the
other hand, an INSERT/UPDATE/DELETE acquires its RowExclusiveLock and
builds
its relcache entry before assigning an XID, so perhaps the snapbuild.c process
isn't enough to prevent that thread's race condition. What do you think?

IIUC, documentation just intended to say that a type of replication will be
switched from stream to logical one, at the certain point. Please give sometime
for analyzing.

[1]: /messages/by-id/270ad9b8-9c46-40c3-b6c5-3d25b91d3a7d@app.fastmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

0001-pg_createsubscriber-Fix-cases-which-connection-param.patchapplication/octet-stream; name=0001-pg_createsubscriber-Fix-cases-which-connection-param.patchDownload
From be447a10990fcd235e8d6361b82ba3a41a4caf0f Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 24 Jun 2024 03:05:12 +0000
Subject: [PATCH 1/2] pg_createsubscriber: Fix cases which connection
 parameters contain a space

appendPQExpBuffer() is used to construct a connection string, but we missed the
case when parameters contain a space. To support such cases, appendConnStrVal()
is used to quote strings appropriately. A test code is also updated.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   |  13 +-
 .../t/040_pg_createsubscriber.pl              | 111 ++++++++++--------
 2 files changed, 74 insertions(+), 50 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1138c20e56..8ed10f010b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -26,6 +26,7 @@
 #include "common/restricted_token.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
+#include "fe_utils/string_utils.h"
 #include "getopt_long.h"
 
 #define	DEFAULT_SUB_PORT	"50432"
@@ -307,10 +308,14 @@ get_sub_conninfo(const struct CreateSubscriberOptions *opt)
 
 	appendPQExpBuffer(buf, "port=%s", opt->sub_port);
 #if !defined(WIN32)
-	appendPQExpBuffer(buf, " host=%s", opt->socket_dir);
+	appendPQExpBuffer(buf, " host=");
+	appendConnStrVal(buf, opt->socket_dir);
 #endif
 	if (opt->sub_username != NULL)
-		appendPQExpBuffer(buf, " user=%s", opt->sub_username);
+	{
+		appendPQExpBuffer(buf, " user=");
+		appendConnStrVal(buf, opt->sub_username);
+	}
 	appendPQExpBuffer(buf, " fallback_application_name=%s", progname);
 
 	ret = pg_strdup(buf->data);
@@ -401,8 +406,8 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
 
 	Assert(conninfo != NULL);
 
-	appendPQExpBufferStr(buf, conninfo);
-	appendPQExpBuffer(buf, " dbname=%s", dbname);
+	appendPQExpBuffer(buf, "%s dbname=", conninfo);
+	appendConnStrVal(buf, dbname);
 
 	ret = pg_strdup(buf->data);
 	destroyPQExpBuffer(buf);
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 0516d4e17e..a50771dbdf 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -9,6 +9,27 @@ use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
 
+# Generate a database with a name made of a range of ASCII characters.
+# Mostly Ported from 002_pg_upgrade.pl, but this returns a generated dbname.
+sub generate_db
+{
+	my ($node, $prefix, $from_char, $to_char, $suffix) = @_;
+
+	my $dbname = $prefix;
+	for my $i ($from_char .. $to_char)
+	{
+		next if $i == 7 || $i == 10 || $i == 13;    # skip BEL, LF, and CR
+		$dbname = $dbname . sprintf('%c', $i);
+	}
+
+	$dbname .= $suffix;
+	$node->command_ok(
+		[ 'createdb', $dbname ],
+		"created database with ASCII characters from $from_char to $to_char");
+
+	return $dbname;
+}
+
 program_help_ok('pg_createsubscriber');
 program_version_ok('pg_createsubscriber');
 program_options_handling_ok('pg_createsubscriber');
@@ -104,16 +125,14 @@ $node_f->init(force_initdb => 1, allows_streaming => 'logical');
 # - create test tables
 # - insert a row
 # - create a physical replication slot
-$node_p->safe_psql(
-	'postgres', q(
-	CREATE DATABASE pg1;
-	CREATE DATABASE pg2;
-));
-$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
-$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $db1 = generate_db($node_p, 'regression\\"\\', 1, 45, '\\\\"\\\\\\');
+my $db2 = generate_db($node_p, 'regression', 46, 90, '');
+
+$node_p->safe_psql($db1, 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql($db2, 'CREATE TABLE tbl2 (a text)');
 my $slotname = 'physical_slot';
-$node_p->safe_psql('pg2',
+$node_p->safe_psql($db2,
 	"SELECT pg_create_physical_replication_slot('$slotname')");
 
 # Set up node S as standby linking to node P
@@ -143,11 +162,11 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_t->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_t->host, '--subscriber-port',
 		$node_t->port, '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'target server is not in recovery');
 
@@ -157,11 +176,11 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'standby is up and running');
 
@@ -170,11 +189,11 @@ command_fails(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
+		'--publisher-server', $node_p->connstr($db1),
 		'--socket-directory', $node_f->host,
 		'--subscriber-port', $node_f->port,
-		'--database', 'pg1',
-		'--database', 'pg2'
+		'--database', $db1,
+		'--database', $db2
 	],
 	'subscriber data directory is not a copy of the source database cluster');
 
@@ -191,16 +210,16 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_c->data_dir, '--publisher-server',
-		$node_s->connstr('pg1'), '--socket-directory',
+		$node_s->connstr($db1), '--socket-directory',
 		$node_c->host, '--subscriber-port',
 		$node_c->port, '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'primary server is in recovery');
 
 # Insert another row on node P and wait node S to catch up
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('second row')");
 $node_p->wait_for_replay_catchup($node_s);
 
 # Check some unmet conditions on node P
@@ -218,11 +237,11 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'primary contains unmet conditions on node P');
 # Restore default settings here but only apply it after testing standby. Some
@@ -247,11 +266,11 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'standby contains unmet conditions on node S');
 $node_s->append_conf(
@@ -265,7 +284,7 @@ $node_p->restart;
 
 # Create failover slot to test its removal
 my $fslotname = 'failover_slot';
-$node_p->safe_psql('pg1',
+$node_p->safe_psql($db1,
 	"SELECT pg_create_logical_replication_slot('$fslotname', 'pgoutput', false, false, true)");
 $node_s->start;
 $node_s->safe_psql('postgres', "SELECT pg_sync_replication_slots()");
@@ -280,15 +299,15 @@ command_ok(
 		'--recovery-timeout', "$PostgreSQL::Test::Utils::timeout_default",
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--publication',
 		'pub1', '--publication',
 		'pub2', '--subscription',
 		'sub1', '--subscription',
 		'sub2', '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'run pg_createsubscriber --dry-run on node S');
 
@@ -304,7 +323,7 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--replication-slot',
 		'replslot1'
@@ -318,20 +337,20 @@ command_ok(
 		'--recovery-timeout', "$PostgreSQL::Test::Utils::timeout_default",
 		'--verbose', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--publication',
 		'pub1', '--publication',
 		'Pub2', '--replication-slot',
 		'replslot1', '--replication-slot',
 		'replslot2', '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'run pg_createsubscriber on node S');
 
 # Confirm the physical replication slot has been removed
-$result = $node_p->safe_psql('pg1',
+$result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
 );
 is($result, qq(0),
@@ -339,8 +358,8 @@ is($result, qq(0),
 );
 
 # Insert rows on P
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
-$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql($db2, "INSERT INTO tbl2 VALUES('row 1')");
 
 # Start subscriber
 $node_s->start;
@@ -357,20 +376,20 @@ $node_s->wait_for_subscription_sync($node_p, $subnames[0]);
 $node_s->wait_for_subscription_sync($node_p, $subnames[1]);
 
 # Confirm the failover slot has been removed
-$result = $node_s->safe_psql('pg1',
+$result = $node_s->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$fslotname'");
 is($result, qq(0), 'failover slot was removed');
 
-# Check result on database pg1
-$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+# Check result on database $db1
+$result = $node_s->safe_psql($db1, 'SELECT * FROM tbl1');
 is( $result, qq(first row
 second row
 third row),
-	'logical replication works on database pg1');
+	'logical replication works on database $db1');
 
-# Check result on database pg2
-$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
-is($result, qq(row 1), 'logical replication works on database pg2');
+# Check result on database $db2
+$result = $node_s->safe_psql($db2, 'SELECT * FROM tbl2');
+is($result, qq(row 1), 'logical replication works on database $db2');
 
 # Different system identifier?
 my $sysid_p = $node_p->safe_psql('postgres',
-- 
2.43.0

#272Amit Kapila
amit.kapila16@gmail.com
In reply to: Noah Misch (#269)
Re: speed up a logical replica setup

On Sun, Jun 23, 2024 at 11:52 AM Noah Misch <noah@leadboat.com> wrote:

+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+     appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+                                       ipubname_esc);

This tool's documentation says it "guarantees that no transaction will be
lost." I tried to determine whether achieving that will require something
like the fix from
/messages/by-id/flat/de52b282-1166-1180-45a2-8d8917ca74c6@enterprisedb.com.
(Not exactly the fix from that thread, since that thread has not discussed the
FOR ALL TABLES version of its race condition.) I don't know. On the one
hand, pg_createsubscriber benefits from creating a logical slot after creating
the publication. That snapbuild.c process will wait for running XIDs. On the
other hand, an INSERT/UPDATE/DELETE acquires its RowExclusiveLock and builds
its relcache entry before assigning an XID, so perhaps the snapbuild.c process
isn't enough to prevent that thread's race condition. What do you think?

I am not able to imagine how the race condition discussed in the
thread you quoted can impact this patch. The problem discussed is
mainly the interaction when we are processing the changes in logical
decoding w.r.t concurrent DDL (Alter Publication ... Add Table). The
problem happens because we use the old cache state. I am missing your
point about the race condition mentioned in the thread you quoted with
snapbuild.c. Can you please elaborate a bit more?

--
With Regards,
Amit Kapila.

#273Noah Misch
noah@leadboat.com
In reply to: Amit Kapila (#272)
Re: speed up a logical replica setup

On Mon, Jun 24, 2024 at 05:20:21PM +0530, Amit Kapila wrote:

On Sun, Jun 23, 2024 at 11:52 AM Noah Misch <noah@leadboat.com> wrote:

+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+     appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+                                       ipubname_esc);

This tool's documentation says it "guarantees that no transaction will be
lost." I tried to determine whether achieving that will require something
like the fix from
/messages/by-id/flat/de52b282-1166-1180-45a2-8d8917ca74c6@enterprisedb.com.
(Not exactly the fix from that thread, since that thread has not discussed the
FOR ALL TABLES version of its race condition.) I don't know. On the one
hand, pg_createsubscriber benefits from creating a logical slot after creating
the publication. That snapbuild.c process will wait for running XIDs. On the
other hand, an INSERT/UPDATE/DELETE acquires its RowExclusiveLock and builds
its relcache entry before assigning an XID, so perhaps the snapbuild.c process

Correction: it doesn't matter how the original INSERT/UPDATE/DELETE builds its
relcache entry, just how pgoutput of the change builds the relcache entry from
the historic snapshot.

isn't enough to prevent that thread's race condition. What do you think?

I am not able to imagine how the race condition discussed in the
thread you quoted can impact this patch. The problem discussed is
mainly the interaction when we are processing the changes in logical
decoding w.r.t concurrent DDL (Alter Publication ... Add Table). The
problem happens because we use the old cache state.

Right. Taking the example from
/messages/by-id/20231119021830.d6t6aaxtrkpn743y@awork3.anarazel.de, LSNs
between what that mail calls 4) and 5) are not safely usable as start points.
pg_createsubscriber evades that thread's problem if the consistent_lsn it
passes to pg_replication_origin_advance() can't be in a bad-start-point LSN
span. I cautiously bet the snapbuild.c process achieves that:

I am missing your
point about the race condition mentioned in the thread you quoted with
snapbuild.c. Can you please elaborate a bit more?

When pg_createsubscriber calls pg_create_logical_replication_slot(), the key
part starts at:

/*
* If caller needs us to determine the decoding start point, do so now.
* This might take a while.
*/
if (find_startpoint)
DecodingContextFindStartpoint(ctx);

Two factors protect pg_createsubscriber. First, (a) CREATE PUBLICATION
committed before pg_create_logical_replication_slot() started. Second, (b)
DecodingContextFindStartpoint() waits for running XIDs to complete, via the
process described at the snapbuild.c "starting up in several stages" diagram.
Hence, the consistent_lsn is not in a bad-start-point LSN span. It's fine
even if the original INSERT populated all caches before CREATE PUBLICATION
started and managed to assign an XID only after consistent_lsn. From the
pgoutput perspective, that's indistinguishable from the transaction starting
at its first WAL record, after consistent_lsn. The linked "long-standing data
loss bug in initial sync of logical replication" thread doesn't have (a),
hence its bug. How close is that to accurate?

Thanks,
nm

#274Amit Kapila
amit.kapila16@gmail.com
In reply to: Noah Misch (#273)
Re: speed up a logical replica setup

On Tue, Jun 25, 2024 at 3:38 AM Noah Misch <noah@leadboat.com> wrote:

On Mon, Jun 24, 2024 at 05:20:21PM +0530, Amit Kapila wrote:

On Sun, Jun 23, 2024 at 11:52 AM Noah Misch <noah@leadboat.com> wrote:

+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+     appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+                                       ipubname_esc);

This tool's documentation says it "guarantees that no transaction will be
lost." I tried to determine whether achieving that will require something
like the fix from
/messages/by-id/flat/de52b282-1166-1180-45a2-8d8917ca74c6@enterprisedb.com.
(Not exactly the fix from that thread, since that thread has not discussed the
FOR ALL TABLES version of its race condition.) I don't know. On the one
hand, pg_createsubscriber benefits from creating a logical slot after creating
the publication. That snapbuild.c process will wait for running XIDs. On the
other hand, an INSERT/UPDATE/DELETE acquires its RowExclusiveLock and builds
its relcache entry before assigning an XID, so perhaps the snapbuild.c process

Correction: it doesn't matter how the original INSERT/UPDATE/DELETE builds its
relcache entry, just how pgoutput of the change builds the relcache entry from
the historic snapshot.

isn't enough to prevent that thread's race condition. What do you think?

I am not able to imagine how the race condition discussed in the
thread you quoted can impact this patch. The problem discussed is
mainly the interaction when we are processing the changes in logical
decoding w.r.t concurrent DDL (Alter Publication ... Add Table). The
problem happens because we use the old cache state.

Right. Taking the example from
/messages/by-id/20231119021830.d6t6aaxtrkpn743y@awork3.anarazel.de, LSNs
between what that mail calls 4) and 5) are not safely usable as start points.
pg_createsubscriber evades that thread's problem if the consistent_lsn it
passes to pg_replication_origin_advance() can't be in a bad-start-point LSN
span. I cautiously bet the snapbuild.c process achieves that:

I am missing your
point about the race condition mentioned in the thread you quoted with
snapbuild.c. Can you please elaborate a bit more?

When pg_createsubscriber calls pg_create_logical_replication_slot(), the key
part starts at:

/*
* If caller needs us to determine the decoding start point, do so now.
* This might take a while.
*/
if (find_startpoint)
DecodingContextFindStartpoint(ctx);

Two factors protect pg_createsubscriber. First, (a) CREATE PUBLICATION
committed before pg_create_logical_replication_slot() started. Second, (b)
DecodingContextFindStartpoint() waits for running XIDs to complete, via the
process described at the snapbuild.c "starting up in several stages" diagram.
Hence, the consistent_lsn is not in a bad-start-point LSN span. It's fine
even if the original INSERT populated all caches before CREATE PUBLICATION
started and managed to assign an XID only after consistent_lsn. From the
pgoutput perspective, that's indistinguishable from the transaction starting
at its first WAL record, after consistent_lsn. The linked "long-standing data
loss bug in initial sync of logical replication" thread doesn't have (a),
hence its bug. How close is that to accurate?

Yeah, this theory sounds right to me. The key point is that no DML
(processing of WAL corresponding to DML) before CREATE PUBLICATION ...
command would have reached pgoutput level because we would have waited
for it during snapbuild.c. Can we conclude that the race condition
discussed in the other thread won't impact this patch?

--
With Regards,
Amit Kapila.

#275Euler Taveira
euler@eulerto.com
In reply to: Amit Kapila (#274)
Re: speed up a logical replica setup

On Tue, Jun 25, 2024, at 3:24 AM, Amit Kapila wrote:

On Tue, Jun 25, 2024 at 3:38 AM Noah Misch <noah@leadboat.com> wrote:

On Mon, Jun 24, 2024 at 05:20:21PM +0530, Amit Kapila wrote:

On Sun, Jun 23, 2024 at 11:52 AM Noah Misch <noah@leadboat.com> wrote:

+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+     appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+                                       ipubname_esc);

This tool's documentation says it "guarantees that no transaction will be
lost." I tried to determine whether achieving that will require something
like the fix from
/messages/by-id/flat/de52b282-1166-1180-45a2-8d8917ca74c6@enterprisedb.com.
(Not exactly the fix from that thread, since that thread has not discussed the
FOR ALL TABLES version of its race condition.) I don't know. On the one
hand, pg_createsubscriber benefits from creating a logical slot after creating
the publication. That snapbuild.c process will wait for running XIDs. On the
other hand, an INSERT/UPDATE/DELETE acquires its RowExclusiveLock and builds
its relcache entry before assigning an XID, so perhaps the snapbuild.c process

Correction: it doesn't matter how the original INSERT/UPDATE/DELETE builds its
relcache entry, just how pgoutput of the change builds the relcache entry from
the historic snapshot.

isn't enough to prevent that thread's race condition. What do you think?

I am not able to imagine how the race condition discussed in the
thread you quoted can impact this patch. The problem discussed is
mainly the interaction when we are processing the changes in logical
decoding w.r.t concurrent DDL (Alter Publication ... Add Table). The
problem happens because we use the old cache state.

Right. Taking the example from
/messages/by-id/20231119021830.d6t6aaxtrkpn743y@awork3.anarazel.de, LSNs
between what that mail calls 4) and 5) are not safely usable as start points.
pg_createsubscriber evades that thread's problem if the consistent_lsn it
passes to pg_replication_origin_advance() can't be in a bad-start-point LSN
span. I cautiously bet the snapbuild.c process achieves that:

I am missing your
point about the race condition mentioned in the thread you quoted with
snapbuild.c. Can you please elaborate a bit more?

When pg_createsubscriber calls pg_create_logical_replication_slot(), the key
part starts at:

/*
* If caller needs us to determine the decoding start point, do so now.
* This might take a while.
*/
if (find_startpoint)
DecodingContextFindStartpoint(ctx);

Two factors protect pg_createsubscriber. First, (a) CREATE PUBLICATION
committed before pg_create_logical_replication_slot() started. Second, (b)
DecodingContextFindStartpoint() waits for running XIDs to complete, via the
process described at the snapbuild.c "starting up in several stages" diagram.
Hence, the consistent_lsn is not in a bad-start-point LSN span. It's fine
even if the original INSERT populated all caches before CREATE PUBLICATION
started and managed to assign an XID only after consistent_lsn. From the
pgoutput perspective, that's indistinguishable from the transaction starting
at its first WAL record, after consistent_lsn. The linked "long-standing data
loss bug in initial sync of logical replication" thread doesn't have (a),
hence its bug. How close is that to accurate?

Yeah, this theory sounds right to me. The key point is that no DML
(processing of WAL corresponding to DML) before CREATE PUBLICATION ...
command would have reached pgoutput level because we would have waited
for it during snapbuild.c. Can we conclude that the race condition
discussed in the other thread won't impact this patch?

As Noah said the key point is the CREATE PUBLICATION *before* creating the
replication slots -- that wait transactions to complete.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#276Euler Taveira
euler@eulerto.com
In reply to: Hayato Kuroda (Fujitsu) (#271)
Re: speed up a logical replica setup

On Mon, Jun 24, 2024, at 3:47 AM, Hayato Kuroda (Fujitsu) wrote:

pg_createsubscriber fails on a dbname containing a space. Use
appendConnStrVal() here and for other params in get_sub_conninfo(). See the
CVE-2016-5424 commits for more background. For one way to test this
scenario,
see generate_db() in the pg_upgrade test suite.

Thanks for pointing out. I made a fix patch. Test code was also modified accordingly.

Patch looks good to me. I have one suggestion

-# Mostly Ported from 002_pg_upgrade.pl, but this returns a generated dbname.
+# Extracted from 002_pg_upgrade.pl.

and 2 small fixes:

-   'logical replication works on database $db1');
+   "logical replication works on database $db1");
-is($result, qq(row 1), 'logical replication works on database $db2');
+is($result, qq(row 1), "logical replication works on database $db2");
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+ PQExpBuffer str = createPQExpBuffer();
+ PGresult   *res = NULL;
+ const char *slot_name = dbinfo->replslotname;
+ char    *slot_name_esc;
+ char    *lsn = NULL;
+
+ Assert(conn != NULL);
+
+ pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+ slot_name, dbinfo->dbname);
+
+ slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name));
+
+ appendPQExpBuffer(str,
+   "SELECT lsn FROM

pg_catalog.pg_create_logical_replication_slot(%s, 'pgoutput', false, false, false)",

This is passing twophase=false, but the patch does not mention prepared
transactions. Is the intent to not support workloads containing prepared
transactions? If so, the documentation should say that, and the tool likely
should warn on startup if max_prepared_transactions != 0.

IIUC, We decided because it is a default behavior of logical replication. See [1].
+1 for improving a documentation, but not sure it is helpful for adding output.
I want to know opinions from others.

Documentation says

When two-phase commit is enabled, prepared transactions are sent to the
subscriber at the time of PREPARE TRANSACTION, and are processed as two-phase
transactions on the subscriber too. Otherwise, prepared transactions are sent to
the subscriber only when committed, and are then processed immediately by the
subscriber.

Hence, the replication should be working for prepared transactions even if it
created the slot with twophase = false. IIRC the user won't be able to change it
later. As Amit said in a previous email, once the command
ALTER SUBSCRIPTION ... SET (two_phase = on) is supported, users can change it
after running pg_createsubscriber. The other option is to add a command-line
option to enable or disable it.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#277Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#276)
Re: speed up a logical replica setup

On Wed, Jun 26, 2024 at 7:21 AM Euler Taveira <euler@eulerto.com> wrote:

On Mon, Jun 24, 2024, at 3:47 AM, Hayato Kuroda (Fujitsu) wrote:

pg_createsubscriber fails on a dbname containing a space. Use
appendConnStrVal() here and for other params in get_sub_conninfo(). See the
CVE-2016-5424 commits for more background. For one way to test this
scenario,
see generate_db() in the pg_upgrade test suite.

Thanks for pointing out. I made a fix patch. Test code was also modified accordingly.

Patch looks good to me. I have one suggestion

-# Mostly Ported from 002_pg_upgrade.pl, but this returns a generated dbname.
+# Extracted from 002_pg_upgrade.pl.

and 2 small fixes:

-   'logical replication works on database $db1');
+   "logical replication works on database $db1");
-is($result, qq(row 1), 'logical replication works on database $db2');
+is($result, qq(row 1), "logical replication works on database $db2");
+static char *
+create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+ PQExpBuffer str = createPQExpBuffer();
+ PGresult   *res = NULL;
+ const char *slot_name = dbinfo->replslotname;
+ char    *slot_name_esc;
+ char    *lsn = NULL;
+
+ Assert(conn != NULL);
+
+ pg_log_info("creating the replication slot \"%s\" on database \"%s\"",
+ slot_name, dbinfo->dbname);
+
+ slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name));
+
+ appendPQExpBuffer(str,
+   "SELECT lsn FROM

pg_catalog.pg_create_logical_replication_slot(%s, 'pgoutput', false, false, false)",

This is passing twophase=false, but the patch does not mention prepared
transactions. Is the intent to not support workloads containing prepared
transactions? If so, the documentation should say that, and the tool likely
should warn on startup if max_prepared_transactions != 0.

IIUC, We decided because it is a default behavior of logical replication. See [1].
+1 for improving a documentation, but not sure it is helpful for adding output.
I want to know opinions from others.

Documentation says

When two-phase commit is enabled, prepared transactions are sent to the
subscriber at the time of PREPARE TRANSACTION, and are processed as two-phase
transactions on the subscriber too. Otherwise, prepared transactions are sent to
the subscriber only when committed, and are then processed immediately by the
subscriber.

Hence, the replication should be working for prepared transactions even if it
created the slot with twophase = false. IIRC the user won't be able to change it
later. As Amit said in a previous email, once the command
ALTER SUBSCRIPTION ... SET (two_phase = on) is supported, users can change it
after running pg_createsubscriber. The other option is to add a command-line
option to enable or disable it.

Yeah, it is a good idea to add a new option for two_phase but that
should be done in the next version. For now, I suggest updating the
docs and probably raising a warning (if max_prepared_transactions !=
0) as suggested by Noah. This WARNING is useful because one could
expect that setting max_prepared_transactions != 0 means apply will
happen at prepare time after the subscriber is created by this tool.
The WARNING will be useful even if we support two_phase option as the
user may have set the non-zero value of max_prepared_transactions but
didn't use two_phase option.

--
With Regards,
Amit Kapila.

#278Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Amit Kapila (#277)
2 attachment(s)
RE: speed up a logical replica setup

Dear Amit, Euler,

Thanks for giving comment! 0001 was modified per suggestions.

Yeah, it is a good idea to add a new option for two_phase but that
should be done in the next version. For now, I suggest updating the
docs and probably raising a warning (if max_prepared_transactions !=
0) as suggested by Noah. This WARNING is useful because one could
expect that setting max_prepared_transactions != 0 means apply will
happen at prepare time after the subscriber is created by this tool.
The WARNING will be useful even if we support two_phase option as the
user may have set the non-zero value of max_prepared_transactions but
didn't use two_phase option.

I also think it should be tunable, in PG18+. 0002 adds a description in doc.
Also, this executable warns if the max_prepared_transactions != 0 on the
publisher. Subscriber-side is not checked now because I don't think it is helpful,
but it can be easily added. I did not add test for that, because current test code
does not check outputs.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

v2-0001-pg_createsubscriber-Fix-cases-which-connection-pa.patchapplication/octet-stream; name=v2-0001-pg_createsubscriber-Fix-cases-which-connection-pa.patchDownload
From 512267a7c1457c16a686c9104dc939395a388bd1 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 24 Jun 2024 03:05:12 +0000
Subject: [PATCH v2 1/2] pg_createsubscriber: Fix cases which connection
 parameters contain a space

appendPQExpBuffer() is used to construct a connection string, but we missed the
case when parameters contain a space. To support such cases, appendConnStrVal()
is used to quote strings appropriately. A test code is also updated.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   |  13 +-
 .../t/040_pg_createsubscriber.pl              | 111 ++++++++++--------
 2 files changed, 74 insertions(+), 50 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1138c20e56..8ed10f010b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -26,6 +26,7 @@
 #include "common/restricted_token.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
+#include "fe_utils/string_utils.h"
 #include "getopt_long.h"
 
 #define	DEFAULT_SUB_PORT	"50432"
@@ -307,10 +308,14 @@ get_sub_conninfo(const struct CreateSubscriberOptions *opt)
 
 	appendPQExpBuffer(buf, "port=%s", opt->sub_port);
 #if !defined(WIN32)
-	appendPQExpBuffer(buf, " host=%s", opt->socket_dir);
+	appendPQExpBuffer(buf, " host=");
+	appendConnStrVal(buf, opt->socket_dir);
 #endif
 	if (opt->sub_username != NULL)
-		appendPQExpBuffer(buf, " user=%s", opt->sub_username);
+	{
+		appendPQExpBuffer(buf, " user=");
+		appendConnStrVal(buf, opt->sub_username);
+	}
 	appendPQExpBuffer(buf, " fallback_application_name=%s", progname);
 
 	ret = pg_strdup(buf->data);
@@ -401,8 +406,8 @@ concat_conninfo_dbname(const char *conninfo, const char *dbname)
 
 	Assert(conninfo != NULL);
 
-	appendPQExpBufferStr(buf, conninfo);
-	appendPQExpBuffer(buf, " dbname=%s", dbname);
+	appendPQExpBuffer(buf, "%s dbname=", conninfo);
+	appendConnStrVal(buf, dbname);
 
 	ret = pg_strdup(buf->data);
 	destroyPQExpBuffer(buf);
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 0516d4e17e..26eb606cc4 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -9,6 +9,27 @@ use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
 
+# Generate a database with a name made of a range of ASCII characters.
+# Extracted from 002_pg_upgrade.pl.
+sub generate_db
+{
+	my ($node, $prefix, $from_char, $to_char, $suffix) = @_;
+
+	my $dbname = $prefix;
+	for my $i ($from_char .. $to_char)
+	{
+		next if $i == 7 || $i == 10 || $i == 13;    # skip BEL, LF, and CR
+		$dbname = $dbname . sprintf('%c', $i);
+	}
+
+	$dbname .= $suffix;
+	$node->command_ok(
+		[ 'createdb', $dbname ],
+		"created database with ASCII characters from $from_char to $to_char");
+
+	return $dbname;
+}
+
 program_help_ok('pg_createsubscriber');
 program_version_ok('pg_createsubscriber');
 program_options_handling_ok('pg_createsubscriber');
@@ -104,16 +125,14 @@ $node_f->init(force_initdb => 1, allows_streaming => 'logical');
 # - create test tables
 # - insert a row
 # - create a physical replication slot
-$node_p->safe_psql(
-	'postgres', q(
-	CREATE DATABASE pg1;
-	CREATE DATABASE pg2;
-));
-$node_p->safe_psql('pg1', 'CREATE TABLE tbl1 (a text)');
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('first row')");
-$node_p->safe_psql('pg2', 'CREATE TABLE tbl2 (a text)');
+my $db1 = generate_db($node_p, 'regression\\"\\', 1, 45, '\\\\"\\\\\\');
+my $db2 = generate_db($node_p, 'regression', 46, 90, '');
+
+$node_p->safe_psql($db1, 'CREATE TABLE tbl1 (a text)');
+$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('first row')");
+$node_p->safe_psql($db2, 'CREATE TABLE tbl2 (a text)');
 my $slotname = 'physical_slot';
-$node_p->safe_psql('pg2',
+$node_p->safe_psql($db2,
 	"SELECT pg_create_physical_replication_slot('$slotname')");
 
 # Set up node S as standby linking to node P
@@ -143,11 +162,11 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_t->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_t->host, '--subscriber-port',
 		$node_t->port, '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'target server is not in recovery');
 
@@ -157,11 +176,11 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'standby is up and running');
 
@@ -170,11 +189,11 @@ command_fails(
 	[
 		'pg_createsubscriber', '--verbose',
 		'--pgdata', $node_f->data_dir,
-		'--publisher-server', $node_p->connstr('pg1'),
+		'--publisher-server', $node_p->connstr($db1),
 		'--socket-directory', $node_f->host,
 		'--subscriber-port', $node_f->port,
-		'--database', 'pg1',
-		'--database', 'pg2'
+		'--database', $db1,
+		'--database', $db2
 	],
 	'subscriber data directory is not a copy of the source database cluster');
 
@@ -191,16 +210,16 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_c->data_dir, '--publisher-server',
-		$node_s->connstr('pg1'), '--socket-directory',
+		$node_s->connstr($db1), '--socket-directory',
 		$node_c->host, '--subscriber-port',
 		$node_c->port, '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'primary server is in recovery');
 
 # Insert another row on node P and wait node S to catch up
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('second row')");
+$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('second row')");
 $node_p->wait_for_replay_catchup($node_s);
 
 # Check some unmet conditions on node P
@@ -218,11 +237,11 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'primary contains unmet conditions on node P');
 # Restore default settings here but only apply it after testing standby. Some
@@ -247,11 +266,11 @@ command_fails(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'standby contains unmet conditions on node S');
 $node_s->append_conf(
@@ -265,7 +284,7 @@ $node_p->restart;
 
 # Create failover slot to test its removal
 my $fslotname = 'failover_slot';
-$node_p->safe_psql('pg1',
+$node_p->safe_psql($db1,
 	"SELECT pg_create_logical_replication_slot('$fslotname', 'pgoutput', false, false, true)");
 $node_s->start;
 $node_s->safe_psql('postgres', "SELECT pg_sync_replication_slots()");
@@ -280,15 +299,15 @@ command_ok(
 		'--recovery-timeout', "$PostgreSQL::Test::Utils::timeout_default",
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--publication',
 		'pub1', '--publication',
 		'pub2', '--subscription',
 		'sub1', '--subscription',
 		'sub2', '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'run pg_createsubscriber --dry-run on node S');
 
@@ -304,7 +323,7 @@ command_ok(
 		'pg_createsubscriber', '--verbose',
 		'--dry-run', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--replication-slot',
 		'replslot1'
@@ -318,20 +337,20 @@ command_ok(
 		'--recovery-timeout', "$PostgreSQL::Test::Utils::timeout_default",
 		'--verbose', '--pgdata',
 		$node_s->data_dir, '--publisher-server',
-		$node_p->connstr('pg1'), '--socket-directory',
+		$node_p->connstr($db1), '--socket-directory',
 		$node_s->host, '--subscriber-port',
 		$node_s->port, '--publication',
 		'pub1', '--publication',
 		'Pub2', '--replication-slot',
 		'replslot1', '--replication-slot',
 		'replslot2', '--database',
-		'pg1', '--database',
-		'pg2'
+		$db1, '--database',
+		$db2
 	],
 	'run pg_createsubscriber on node S');
 
 # Confirm the physical replication slot has been removed
-$result = $node_p->safe_psql('pg1',
+$result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
 );
 is($result, qq(0),
@@ -339,8 +358,8 @@ is($result, qq(0),
 );
 
 # Insert rows on P
-$node_p->safe_psql('pg1', "INSERT INTO tbl1 VALUES('third row')");
-$node_p->safe_psql('pg2', "INSERT INTO tbl2 VALUES('row 1')");
+$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('third row')");
+$node_p->safe_psql($db2, "INSERT INTO tbl2 VALUES('row 1')");
 
 # Start subscriber
 $node_s->start;
@@ -357,20 +376,20 @@ $node_s->wait_for_subscription_sync($node_p, $subnames[0]);
 $node_s->wait_for_subscription_sync($node_p, $subnames[1]);
 
 # Confirm the failover slot has been removed
-$result = $node_s->safe_psql('pg1',
+$result = $node_s->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$fslotname'");
 is($result, qq(0), 'failover slot was removed');
 
-# Check result on database pg1
-$result = $node_s->safe_psql('pg1', 'SELECT * FROM tbl1');
+# Check result on database $db1
+$result = $node_s->safe_psql($db1, 'SELECT * FROM tbl1');
 is( $result, qq(first row
 second row
 third row),
-	'logical replication works on database pg1');
+	"logical replication works on database $db1");
 
-# Check result on database pg2
-$result = $node_s->safe_psql('pg2', 'SELECT * FROM tbl2');
-is($result, qq(row 1), 'logical replication works on database pg2');
+# Check result on database $db2
+$result = $node_s->safe_psql($db2, 'SELECT * FROM tbl2');
+is($result, qq(row 1), "logical replication works on database $db2");
 
 # Different system identifier?
 my $sysid_p = $node_p->safe_psql('postgres',
-- 
2.43.0

v2-0002-pg_createsubscriber-Warn-the-two-phase-is-disable.patchapplication/octet-stream; name=v2-0002-pg_createsubscriber-Warn-the-two-phase-is-disable.patchDownload
From 4e4750713fab4e7f3e1fd43d0bc8945b3797b392 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Mon, 24 Jun 2024 04:21:32 +0000
Subject: [PATCH v2 2/2] pg_createsubscriber: Warn the two-phase is disabled
 for logical replication

For now, pg_createsubscriber sets up with the two-phase commit disabled because
the setting is a default behaivor of logical replication. This commit adds the
description in the doc, and output warning when max_prepared_tranasctions > 0
on the publisher node.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml   |  6 ++++++
 src/bin/pg_basebackup/pg_createsubscriber.c | 23 ++++++++++++++++++---
 2 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 2ee6eee9e3..4cc4b7c741 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -353,6 +353,12 @@ PostgreSQL documentation
     <application>pg_createsubscriber</application>.
    </para>
 
+   <para>
+    For now, <application>pg_createsubscriber</application> sets up logical
+    replication with the two-phase commit disabled.  This restriction may be
+    removed in future versions.
+   </para>
+
    <para>
     <application>pg_createsubscriber</application> changes the system
     identifier using <application>pg_resetwal</application>.  It would avoid
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 8ed10f010b..0230960110 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -820,6 +820,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 	int			cur_repslots;
 	int			max_walsenders;
 	int			cur_walsenders;
+	int			max_prepared_transactions;
 
 	pg_log_info("checking settings on publisher");
 
@@ -860,9 +861,12 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 				 "WHERE name = 'max_wal_senders'), "
 				 "cur_mws AS "
 				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
-				 "WHERE backend_type = 'walsender') "
-				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
-				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+				 "WHERE backend_type = 'walsender'), "
+				 "cur_mpt AS "
+				 "(SELECT setting AS mpt FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_prepared_transactions') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws, mpt "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws, cur_mpt");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
@@ -876,6 +880,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 	cur_repslots = atoi(PQgetvalue(res, 0, 2));
 	max_walsenders = atoi(PQgetvalue(res, 0, 3));
 	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+	max_prepared_transactions = atoi(PQgetvalue(res, 0, 5));
 
 	PQclear(res);
 
@@ -884,6 +889,8 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
 	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
 	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+	pg_log_debug("publisher: max_prepared_transactions: %d",
+				 max_prepared_transactions);
 
 	disconnect_database(conn, false);
 
@@ -911,6 +918,16 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 		failed = true;
 	}
 
+	if (max_prepared_transactions != 0)
+	{
+		pg_log_warning("publisher should set max_prepared_transactions to zero, "
+					   "but now it is set to %d",
+					   max_prepared_transactions);
+		pg_log_warning_detail("Subscriptions will be created with the two_phase disabled. "
+							  "Transactions will not be replicated at PREPARE. "
+							  "They will be done at COMMIT PREPARED.");
+	}
+
 	pg_free(wal_level);
 
 	if (failed)
-- 
2.43.0

#279Noah Misch
noah@leadboat.com
In reply to: Euler Taveira (#275)
Re: speed up a logical replica setup

On Tue, Jun 25, 2024 at 09:50:59PM -0300, Euler Taveira wrote:

On Tue, Jun 25, 2024, at 3:24 AM, Amit Kapila wrote:

On Tue, Jun 25, 2024 at 3:38 AM Noah Misch <noah@leadboat.com> wrote:

On Mon, Jun 24, 2024 at 05:20:21PM +0530, Amit Kapila wrote:

On Sun, Jun 23, 2024 at 11:52 AM Noah Misch <noah@leadboat.com> wrote:

+static void
+create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo)
+{
+     appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES",
+                                       ipubname_esc);

This tool's documentation says it "guarantees that no transaction will be
lost." I tried to determine whether achieving that will require something
like the fix from
/messages/by-id/flat/de52b282-1166-1180-45a2-8d8917ca74c6@enterprisedb.com.
(Not exactly the fix from that thread, since that thread has not discussed the
FOR ALL TABLES version of its race condition.) I don't know. On the one
hand, pg_createsubscriber benefits from creating a logical slot after creating
the publication. That snapbuild.c process will wait for running XIDs. On the
other hand, an INSERT/UPDATE/DELETE acquires its RowExclusiveLock and builds
its relcache entry before assigning an XID, so perhaps the snapbuild.c process

Correction: it doesn't matter how the original INSERT/UPDATE/DELETE builds its
relcache entry, just how pgoutput of the change builds the relcache entry from
the historic snapshot.

isn't enough to prevent that thread's race condition. What do you think?

I am not able to imagine how the race condition discussed in the
thread you quoted can impact this patch. The problem discussed is
mainly the interaction when we are processing the changes in logical
decoding w.r.t concurrent DDL (Alter Publication ... Add Table). The
problem happens because we use the old cache state.

Right. Taking the example from
/messages/by-id/20231119021830.d6t6aaxtrkpn743y@awork3.anarazel.de, LSNs
between what that mail calls 4) and 5) are not safely usable as start points.
pg_createsubscriber evades that thread's problem if the consistent_lsn it
passes to pg_replication_origin_advance() can't be in a bad-start-point LSN
span. I cautiously bet the snapbuild.c process achieves that:

I am missing your
point about the race condition mentioned in the thread you quoted with
snapbuild.c. Can you please elaborate a bit more?

When pg_createsubscriber calls pg_create_logical_replication_slot(), the key
part starts at:

/*
* If caller needs us to determine the decoding start point, do so now.
* This might take a while.
*/
if (find_startpoint)
DecodingContextFindStartpoint(ctx);

Two factors protect pg_createsubscriber. First, (a) CREATE PUBLICATION
committed before pg_create_logical_replication_slot() started. Second, (b)
DecodingContextFindStartpoint() waits for running XIDs to complete, via the
process described at the snapbuild.c "starting up in several stages" diagram.
Hence, the consistent_lsn is not in a bad-start-point LSN span. It's fine
even if the original INSERT populated all caches before CREATE PUBLICATION
started and managed to assign an XID only after consistent_lsn. From the
pgoutput perspective, that's indistinguishable from the transaction starting
at its first WAL record, after consistent_lsn. The linked "long-standing data
loss bug in initial sync of logical replication" thread doesn't have (a),
hence its bug. How close is that to accurate?

Yeah, this theory sounds right to me. The key point is that no DML
(processing of WAL corresponding to DML) before CREATE PUBLICATION ...
command would have reached pgoutput level because we would have waited
for it during snapbuild.c. Can we conclude that the race condition
discussed in the other thread won't impact this patch?

As Noah said the key point is the CREATE PUBLICATION *before* creating the
replication slots -- that wait transactions to complete.

Let's consider the transaction loss topic concluded. Thanks for contemplating
it. I've added an open item for the "dbname containing a space" topic.

#280Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#278)
1 attachment(s)
Re: speed up a logical replica setup

On Wed, Jun 26, 2024 at 11:35 AM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Thanks for giving comment! 0001 was modified per suggestions.

Yeah, it is a good idea to add a new option for two_phase but that
should be done in the next version. For now, I suggest updating the
docs and probably raising a warning (if max_prepared_transactions !=
0) as suggested by Noah. This WARNING is useful because one could
expect that setting max_prepared_transactions != 0 means apply will
happen at prepare time after the subscriber is created by this tool.
The WARNING will be useful even if we support two_phase option as the
user may have set the non-zero value of max_prepared_transactions but
didn't use two_phase option.

I also think it should be tunable, in PG18+. 0002 adds a description in doc.
Also, this executable warns if the max_prepared_transactions != 0 on the
publisher.

I have slightly adjusted the doc update and warning message for the
0002 patch. Please see attached and let me know what do you think.

--
With Regards,
Amit Kapila.

Attachments:

v3-0001-pg_createsubscriber-Warn-the-two-phase-is-disable.patchapplication/octet-stream; name=v3-0001-pg_createsubscriber-Warn-the-two-phase-is-disable.patchDownload
From 925adeed0bc7ae6be0b6e40d6c6879cb3fe6b9c6 Mon Sep 17 00:00:00 2001
From: Amit Kapila <akapila@postgresql.org>
Date: Fri, 28 Jun 2024 10:17:53 +0530
Subject: [PATCH v3] pg_createsubscriber: Warn the two-phase is disabled for
 logical replication

For now, pg_createsubscriber sets up with the two-phase commit disabled because
the setting is a default behaivor of logical replication. This commit adds the
description in the doc, and output warning when max_prepared_tranasctions > 0
on the publisher node.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml   |  9 +++++++++
 src/bin/pg_basebackup/pg_createsubscriber.c | 20 +++++++++++++++++---
 2 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index 2ee6eee9e3..12f48180c7 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -353,6 +353,15 @@ PostgreSQL documentation
     <application>pg_createsubscriber</application>.
    </para>
 
+   <para>
+    <application>pg_createsubscriber</application> sets up logical replication
+    with the two-phase commit disabled.  This means the prepared transactions
+    will be replicated at the commit prepared time. Once the setup is complete,
+    users can manually drop and re-create the subscriptions with two-phase
+    <link linkend="sql-createsubscription-params-with-two-phase"><literal>two_phase</literal></link>
+    enabled.
+   </para>
+
    <para>
     <application>pg_createsubscriber</application> changes the system
     identifier using <application>pg_resetwal</application>.  It would avoid
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1138c20e56..798121af31 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -815,6 +815,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 	int			cur_repslots;
 	int			max_walsenders;
 	int			cur_walsenders;
+	int			max_prepared_transactions;
 
 	pg_log_info("checking settings on publisher");
 
@@ -855,9 +856,12 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 				 "WHERE name = 'max_wal_senders'), "
 				 "cur_mws AS "
 				 "(SELECT count(*) AS cmws FROM pg_catalog.pg_stat_activity "
-				 "WHERE backend_type = 'walsender') "
-				 "SELECT wallevel, tmrs, cmrs, tmws, cmws "
-				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws");
+				 "WHERE backend_type = 'walsender'), "
+				 "cur_mpt AS "
+				 "(SELECT setting AS mpt FROM pg_catalog.pg_settings "
+				 "WHERE name = 'max_prepared_transactions') "
+				 "SELECT wallevel, tmrs, cmrs, tmws, cmws, mpt "
+				 "FROM wl, total_mrs, cur_mrs, total_mws, cur_mws, cur_mpt");
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 	{
@@ -871,6 +875,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 	cur_repslots = atoi(PQgetvalue(res, 0, 2));
 	max_walsenders = atoi(PQgetvalue(res, 0, 3));
 	cur_walsenders = atoi(PQgetvalue(res, 0, 4));
+	max_prepared_transactions = atoi(PQgetvalue(res, 0, 5));
 
 	PQclear(res);
 
@@ -879,6 +884,8 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 	pg_log_debug("publisher: current replication slots: %d", cur_repslots);
 	pg_log_debug("publisher: max_wal_senders: %d", max_walsenders);
 	pg_log_debug("publisher: current wal senders: %d", cur_walsenders);
+	pg_log_debug("publisher: max_prepared_transactions: %d",
+				 max_prepared_transactions);
 
 	disconnect_database(conn, false);
 
@@ -906,6 +913,13 @@ check_publisher(const struct LogicalRepInfo *dbinfo)
 		failed = true;
 	}
 
+	if (max_prepared_transactions != 0)
+	{
+		pg_log_warning("two_phase will not be enabled for slots");
+		pg_log_warning_detail("Subscriptions will be created with the two_phase disabled.  "
+							  "Transactions will be replicated at at COMMIT PREPARED.");
+	}
+
 	pg_free(wal_level);
 
 	if (failed)
-- 
2.28.0.windows.1

#281Alexander Lakhin
exclusion@gmail.com
In reply to: Peter Eisentraut (#266)
Re: speed up a logical replica setup

Hello Peter and Euler,

17.06.2024 14:04, Peter Eisentraut wrote:

On 07.06.24 05:49, Euler Taveira wrote:

Here it is a patch series to fix the issues reported in recent discussions. The
patches 0001 and 0003 aim to fix the buildfarm issues. The patch 0002 removes
synchronized failover slots on subscriber since it has no use. I also included
an optional patch 0004 that improves the usability by checking both servers if
it already failed in any subscriber check.

I have committed 0001, 0002, and 0003.  Let's keep an eye on the buildfarm to see if that stabilizes things.  So far
it looks good.

For 0004, I suggest inverting the result values from check_publisher() and create_subscriber() so that it returns true
if the check is ok.

As a recent buildfarm failure [1]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=piculet&amp;dt=2024-06-28%2004%3A42%3A48 shows, that test addition introduced
new instability:
### Starting node "node_s"
# Running: pg_ctl -w -D
/home/bf/bf-build/piculet/HEAD/pgsql.build/testrun/pg_basebackup/040_pg_createsubscriber/data/t_040_pg_createsubscriber_node_s_data/pgdata
-l
/home/bf/bf-build/piculet/HEAD/pgsql.build/testrun/pg_basebackup/040_pg_createsubscriber/log/040_pg_createsubscriber_node_s.log
-o --cluster-name=node_s start
waiting for server to start.... done
server started
# Postmaster PID for node "node_s" is 416482
error running SQL: 'psql:<stdin>:1: ERROR:  skipping slot synchronization as the received slot sync LSN 0/30047F0 for
slot "failover_slot" is ahead of the standby position 0/3004708'
while running 'psql -XAtq -d port=51506 host=/tmp/pqWohdD5Qj dbname='postgres' -f - -v ON_ERROR_STOP=1' with sql 'SELECT
pg_sync_replication_slots()' at /home/bf/bf-build/piculet/HEAD/pgsql/src/test/perl/PostgreSQL/Test/Cluster.pm line 2126.

I could reproduce this failure with:
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -517,6 +517,7 @@ WalReceiverMain(char *startup_data, size_t startup_data_len)
                       * let the startup process and primary server know about
                       * them.
                       */
+pg_usleep(300000);
                      XLogWalRcvFlush(false, startpointTLI);

make -s check -C src/bin/pg_basebackup/ PROVE_TESTS="t/040*"

# +++ tap check in src/bin/pg_basebackup +++
t/040_pg_createsubscriber.pl .. 22/? # Tests were run but no plan was declared and done_testing() was not seen.
# Looks like your test exited with 29 just after 23.
t/040_pg_createsubscriber.pl .. Dubious, test returned 29 (wstat 7424, 0x1d00)
All 23 subtests passed

Test Summary Report
-------------------
t/040_pg_createsubscriber.pl (Wstat: 7424 Tests: 23 Failed: 0)
  Non-zero exit status: 29
  Parse errors: No plan found in TAP output
Files=1, Tests=23,  4 wallclock secs ( 0.01 usr  0.01 sys +  0.49 cusr  0.44 csys =  0.95 CPU)

Moreover, this test may suffer from autovacuum:
echo "
autovacuum_naptime = 1
autovacuum_analyze_threshold = 1
" > /tmp/temp.config
TEMP_CONFIG=/tmp/temp.config make -s check -C src/bin/pg_basebackup/ PROVE_TESTS="t/040*"

# +++ tap check in src/bin/pg_basebackup +++
t/040_pg_createsubscriber.pl .. 24/?
#   Failed test 'failover slot is synced'
#   at t/040_pg_createsubscriber.pl line 273.
#          got: ''
#     expected: 'failover_slot'
t/040_pg_createsubscriber.pl .. 28/? # Looks like you failed 1 test of 33.
t/040_pg_createsubscriber.pl .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/33 subtests

[1]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=piculet&amp;dt=2024-06-28%2004%3A42%3A48

Best regards,
Alexander

#282Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alexander Lakhin (#281)
Re: speed up a logical replica setup

Alexander Lakhin <exclusion@gmail.com> writes:

As a recent buildfarm failure [1] shows, that test addition introduced
new instability:

I have a different but possibly-related complaint: why is
040_pg_createsubscriber.pl so miserably slow? On my machine it
runs for a bit over 19 seconds, which seems completely out of line
(for comparison, 010_pg_basebackup.pl takes 6 seconds, and the
other test scripts in this directory take much less). It looks
like most of the blame falls on this step:

[12:47:22.292](14.534s) ok 28 - run pg_createsubscriber on node S

AFAICS the amount of data being replicated is completely trivial,
so that it doesn't make any sense for this to take so long --- and
if it does, that suggests that this tool will be impossibly slow
for production use. But I suspect there is a logic flaw causing
this. Speculating wildly, perhaps that is related to the failure
Alexander spotted?

regards, tom lane

#283Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#282)
Re: speed up a logical replica setup

In hopes of moving things along as we approach the v18 branch,
I went ahead and pushed Kuroda-san's patches (with a bit of
further editorialization). AFAICS that allows closing out
the concerns raised by Noah, so I've marked that open item
done. However, I added a new open item about how the
040_pg_createsubscriber.pl test is slow and still unstable.

regards, tom lane

#284Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#283)
Re: speed up a logical replica setup

I wrote:

In hopes of moving things along as we approach the v18 branch,
I went ahead and pushed Kuroda-san's patches (with a bit of
further editorialization).

So b3f5ccebd promptly blew up on fairywren [1]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&amp;dt=2024-06-30%2018%3A03%3A06:

connection error: 'psql: error: unterminated quoted string in connection info string'
while running 'psql -XAtq -d port=52984 host=C:/tools/nmsys64/tmp/hHg_pngw4z dbname='regression\\\\"\\\\  !"#$%&\\'()*+,-\\\\\\\\"\\\\\\\\\\\\' -f - -v ON_ERROR_STOP=1' at C:/tools/nmsys64/home/pgrunner/bf/root/HEAD/pgsql/src/test/perl/PostgreSQL/Test/Cluster.pm line 2124.

This shows that the command-line arguments to psql are not getting
adequately quoted on that platform. AFAICS, we are relying on
IPC::Run::run to perform such quoting, and it seems to be
falling down here. I wonder what version of IPC::Run fairywren
is using.

regards, tom lane

[1]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&amp;dt=2024-06-30%2018%3A03%3A06

#285Noah Misch
noah@leadboat.com
In reply to: Tom Lane (#284)
Re: speed up a logical replica setup

On Sun, Jun 30, 2024 at 02:58:00PM -0400, Tom Lane wrote:

I wrote:

In hopes of moving things along as we approach the v18 branch,
I went ahead and pushed Kuroda-san's patches (with a bit of
further editorialization).

So b3f5ccebd promptly blew up on fairywren [1]:

connection error: 'psql: error: unterminated quoted string in connection info string'
while running 'psql -XAtq -d port=52984 host=C:/tools/nmsys64/tmp/hHg_pngw4z dbname='regression\\\\"\\\\  !"#$%&\\'()*+,-\\\\\\\\"\\\\\\\\\\\\' -f - -v ON_ERROR_STOP=1' at C:/tools/nmsys64/home/pgrunner/bf/root/HEAD/pgsql/src/test/perl/PostgreSQL/Test/Cluster.pm line 2124.

This shows that the command-line arguments to psql are not getting
adequately quoted on that platform. AFAICS, we are relying on
IPC::Run::run to perform such quoting, and it seems to be
falling down here. I wonder what version of IPC::Run fairywren
is using.

It does look consistent with IPC::Run predating v20220807.0, hence lacking the
https://github.com/toddr/IPC-Run/issues/142 fix. I wondered how this animal
was passing 002_pg_upgrade.pl, but it seems the animal has run TAP suites only
occasionally. Passes in the last week were TAP-free.

Show quoted text

[1] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&amp;dt=2024-06-30%2018%3A03%3A06

#286Tom Lane
tgl@sss.pgh.pa.us
In reply to: Noah Misch (#285)
Re: speed up a logical replica setup

Noah Misch <noah@leadboat.com> writes:

On Sun, Jun 30, 2024 at 02:58:00PM -0400, Tom Lane wrote:

So b3f5ccebd promptly blew up on fairywren [1]:

It does look consistent with IPC::Run predating v20220807.0, hence lacking the
https://github.com/toddr/IPC-Run/issues/142 fix. I wondered how this animal
was passing 002_pg_upgrade.pl, but it seems the animal has run TAP suites only
occasionally. Passes in the last week were TAP-free.

Hmm, drongo just failed in the same way. (It seems to likewise
run TAP tests only some of the time, which I don't understand
because the $Script_Config output looks the same between runs
with TAP tests and runs without.)

I'm tempted to lobotomize the new test case on Windows until
we have that resolved.

regards, tom lane

#287Noah Misch
noah@leadboat.com
In reply to: Tom Lane (#286)
Re: speed up a logical replica setup

On Sun, Jun 30, 2024 at 05:01:52PM -0400, Tom Lane wrote:

Noah Misch <noah@leadboat.com> writes:

On Sun, Jun 30, 2024 at 02:58:00PM -0400, Tom Lane wrote:

So b3f5ccebd promptly blew up on fairywren [1]:

It does look consistent with IPC::Run predating v20220807.0, hence lacking the
https://github.com/toddr/IPC-Run/issues/142 fix. I wondered how this animal
was passing 002_pg_upgrade.pl, but it seems the animal has run TAP suites only
occasionally. Passes in the last week were TAP-free.

Hmm, drongo just failed in the same way. (It seems to likewise
run TAP tests only some of the time, which I don't understand
because the $Script_Config output looks the same between runs
with TAP tests and runs without.)

On further reflection, 002_pg_upgrade.pl may not fail with old IPC::Run. It
may just create a database with an unintended name, losing some test coverage.

I'm tempted to lobotomize the new test case on Windows until
we have that resolved.

Sounds fine. The pg_upgrade suite adequately tests appendShellString() and
appendConnStrVal() with the larger character repertoire, so it's enough for
pg_createsubscriber to test just a space or so.

#288Tom Lane
tgl@sss.pgh.pa.us
In reply to: Noah Misch (#287)
Re: speed up a logical replica setup

Noah Misch <noah@leadboat.com> writes:

On Sun, Jun 30, 2024 at 05:01:52PM -0400, Tom Lane wrote:

I'm tempted to lobotomize the new test case on Windows until
we have that resolved.

Sounds fine. The pg_upgrade suite adequately tests appendShellString() and
appendConnStrVal() with the larger character repertoire, so it's enough for
pg_createsubscriber to test just a space or so.

Hmph ... according to [1]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&amp;dt=2024-07-01%2000%3A03%3A05, 545082091 was not enough to fix this.
I guess that old version of IPC::Run also misbehaves for cases
involving backslash, single quote, and/or some other special
characters?

regards, tom lane

[1]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&amp;dt=2024-07-01%2000%3A03%3A05

#289Noah Misch
noah@leadboat.com
In reply to: Tom Lane (#288)
Re: speed up a logical replica setup

On Sun, Jun 30, 2024 at 09:32:57PM -0400, Tom Lane wrote:

Noah Misch <noah@leadboat.com> writes:

On Sun, Jun 30, 2024 at 05:01:52PM -0400, Tom Lane wrote:

I'm tempted to lobotomize the new test case on Windows until
we have that resolved.

Sounds fine. The pg_upgrade suite adequately tests appendShellString() and
appendConnStrVal() with the larger character repertoire, so it's enough for
pg_createsubscriber to test just a space or so.

Hmph ... according to [1], 545082091 was not enough to fix this.
I guess that old version of IPC::Run also misbehaves for cases
involving backslash, single quote, and/or some other special
characters?

The database name has backslash toward the end, which likely creates a
problematic backslash-double-quote sequence under win32 command line rules.
You can see that in the node log, comparing the CREATE DATABASE log line
(shows dquote at end of dbname) to the FATAL log line (no dquote at end). I'm
not aware of trouble with characters other than backslash or dquote.

Show quoted text

[1] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&amp;dt=2024-07-01%2000%3A03%3A05

#290Tom Lane
tgl@sss.pgh.pa.us
In reply to: Noah Misch (#289)
Re: speed up a logical replica setup

Noah Misch <noah@leadboat.com> writes:

On Sun, Jun 30, 2024 at 09:32:57PM -0400, Tom Lane wrote:

Hmph ... according to [1], 545082091 was not enough to fix this.
I guess that old version of IPC::Run also misbehaves for cases
involving backslash, single quote, and/or some other special
characters?

The database name has backslash toward the end, which likely creates a
problematic backslash-double-quote sequence under win32 command line rules.

OK, I've made it strip backslashes too. Thanks for confirming.

regards, tom lane

#291Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Tom Lane (#282)
1 attachment(s)
RE: speed up a logical replica setup

Dear Tom,

I have a different but possibly-related complaint: why is
040_pg_createsubscriber.pl so miserably slow? On my machine it
runs for a bit over 19 seconds, which seems completely out of line
(for comparison, 010_pg_basebackup.pl takes 6 seconds, and the
other test scripts in this directory take much less). It looks
like most of the blame falls on this step:

[12:47:22.292](14.534s) ok 28 - run pg_createsubscriber on node S

AFAICS the amount of data being replicated is completely trivial,
so that it doesn't make any sense for this to take so long --- and
if it does, that suggests that this tool will be impossibly slow
for production use. But I suspect there is a logic flaw causing
this.

I analyzed the issue. My elog() debugging said that wait_for_end_recovery() was
wasted some time. This was caused by the recovery target seeming unsatisfactory.

We are setting recovery_target_lsn by the return value of pg_create_logical_replication_slot(),
which returns the end of the RUNNING_XACT record. If we use the returned value as
recovery_target_lsn as-is, however, we must wait for additional WAL generation
because the parameter requires that the replicated WAL overtake a certain point.
On my env, the function waited until the bgwriter emitted the XLOG_RUNNING_XACTS record.

One simple solution is to add an additional WAL record at the end of the publisher
setup. IIUC, an arbitrary WAL insertion can reduce the waiting time. The attached
patch inserts a small XLOG_LOGICAL_MESSAGE record, which could reduce much execution
time on my environment.

```
BEFORE
(13.751s) ok 30 - run pg_createsubscriber on node S
AFTER
(0.749s) ok 30 - run pg_createsubscriber on node S
```

However, even after the modification, the reported failure [1]/messages/by-id/0dffca12-bf17-4a7a-334d-225569de5e6e@gmail.com could not be resolved on my env.

How do you think?

[1]: /messages/by-id/0dffca12-bf17-4a7a-334d-225569de5e6e@gmail.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

emit_dummy_message.diffapplication/octet-stream; name=emit_dummy_message.diffDownload
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index be679ebdff..8afaf7a12b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -782,6 +782,32 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		disconnect_database(conn, false);
 	}
 
+	/*
+	 * pg_create_logical_replication_slot () returns the end of the
+	 * RUNNING_XACT record. If we use the returned value as recovery_target_lsn
+	 * as-is, however, we must wait for additional WAL generation because the
+	 * parameter requires that the replicated WAL overtake a certain point.
+	 * Insert a dummy WAL record to avoid unnecessary waits.
+	 */
+	if (!dry_run)
+	{
+		PGconn	   *conn;
+		PGresult   *res = NULL;
+
+		conn = connect_database(dbinfo[0].pubconninfo, true);
+
+		/* Insert dummy data to WAL to move forward the WAL record */
+		res = PQexec(conn,
+					 "SELECT pg_catalog.pg_logical_emit_message(true, 'pg_createsubscriber', 'dummy data', true);");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not run CHECKPOINT: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+	}
+
 	return lsn;
 }
 
#292Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#291)
Re: speed up a logical replica setup

On Mon, Jul 1, 2024 at 8:22 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

I have a different but possibly-related complaint: why is
040_pg_createsubscriber.pl so miserably slow? On my machine it
runs for a bit over 19 seconds, which seems completely out of line
(for comparison, 010_pg_basebackup.pl takes 6 seconds, and the
other test scripts in this directory take much less). It looks
like most of the blame falls on this step:

[12:47:22.292](14.534s) ok 28 - run pg_createsubscriber on node S

AFAICS the amount of data being replicated is completely trivial,
so that it doesn't make any sense for this to take so long --- and
if it does, that suggests that this tool will be impossibly slow
for production use. But I suspect there is a logic flaw causing
this.

I analyzed the issue. My elog() debugging said that wait_for_end_recovery() was
wasted some time. This was caused by the recovery target seeming unsatisfactory.

We are setting recovery_target_lsn by the return value of pg_create_logical_replication_slot(),
which returns the end of the RUNNING_XACT record. If we use the returned value as
recovery_target_lsn as-is, however, we must wait for additional WAL generation
because the parameter requires that the replicated WAL overtake a certain point.
On my env, the function waited until the bgwriter emitted the XLOG_RUNNING_XACTS record.

IIUC, the problem is that the consistent_lsn value returned by
setup_publisher() is the "end +1" location of the required LSN whereas
the recovery_target_lsn used in wait_for_end_recovery() expects the
LSN value to be "start" location of required LSN.

One simple solution is to add an additional WAL record at the end of the publisher
setup. IIUC, an arbitrary WAL insertion can reduce the waiting time. The attached
patch inserts a small XLOG_LOGICAL_MESSAGE record, which could reduce much execution
time on my environment.

This sounds like an ugly hack to me and don't know if we can use it.
The ideal way to fix this is to get the start_lsn from the
create_logical_slot functionality or have some parameter like
recover_target_end_lsn but I don't know if this is a good time to
extend such a functionality.

--
With Regards,
Amit Kapila.

#293Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Amit Kapila (#292)
RE: speed up a logical replica setup

Dear Amit,

IIUC, the problem is that the consistent_lsn value returned by
setup_publisher() is the "end +1" location of the required LSN whereas
the recovery_target_lsn used in wait_for_end_recovery() expects the
LSN value to be "start" location of required LSN.

Yeah, right. It is same as my understanding.

This sounds like an ugly hack to me and don't know if we can use it.

I also think it is hacky, but I could not find better solutions.

The ideal way to fix this is to get the start_lsn from the
create_logical_slot functionality or have some parameter like
recover_target_end_lsn but I don't know if this is a good time to
extend such a functionality.

I felt that such approach might be used for HEAD, but not suitable for PG17.
Alternative approach I came up with was to insert a tuple while waiting the
promotion. It can generate a WAL record so that standby can finish after the
application. But I'm not sure how do we do and it seems to lead an additional
timing issue. Also, this does not improve the behavior of the command - normal
user may have to wait some time by the command.

Do you have any other idea?

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

#294Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Alexander Lakhin (#281)
4 attachment(s)
RE: speed up a logical replica setup

Dear Alexander and other hackers,

As a recent buildfarm failure [1] shows, that test addition introduced
new instability:
### Starting node "node_s"
# Running: pg_ctl -w -D
/home/bf/bf-build/piculet/HEAD/pgsql.build/testrun/pg_basebackup/040_pg_c
reatesubscriber/data/t_040_pg_createsubscriber_node_s_data/pgdata
-l
/home/bf/bf-build/piculet/HEAD/pgsql.build/testrun/pg_basebackup/040_pg_c
reatesubscriber/log/040_pg_createsubscriber_node_s.log
-o --cluster-name=node_s start
waiting for server to start.... done
server started
# Postmaster PID for node "node_s" is 416482
error running SQL: 'psql:<stdin>:1: ERROR: skipping slot synchronization as the
received slot sync LSN 0/30047F0 for
slot "failover_slot" is ahead of the standby position 0/3004708'
while running 'psql -XAtq -d port=51506 host=/tmp/pqWohdD5Qj
dbname='postgres' -f - -v ON_ERROR_STOP=1' with sql 'SELECT
pg_sync_replication_slots()' at
/home/bf/bf-build/piculet/HEAD/pgsql/src/test/perl/PostgreSQL/Test/Cluster
.pm line 2126.

I could reproduce this failure with:
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -517,6 +517,7 @@ WalReceiverMain(char *startup_data, size_t
startup_data_len)
* let the startup process and primary server know
about
* them.
*/
+pg_usleep(300000);
XLogWalRcvFlush(false, startpointTLI);

make -s check -C src/bin/pg_basebackup/ PROVE_TESTS="t/040*"

# +++ tap check in src/bin/pg_basebackup +++
t/040_pg_createsubscriber.pl .. 22/? # Tests were run but no plan was declared
and done_testing() was not seen.
# Looks like your test exited with 29 just after 23.
t/040_pg_createsubscriber.pl .. Dubious, test returned 29 (wstat 7424, 0x1d00)
All 23 subtests passed

Test Summary Report
-------------------
t/040_pg_createsubscriber.pl (Wstat: 7424 Tests: 23 Failed: 0)
Non-zero exit status: 29
Parse errors: No plan found in TAP output
Files=1, Tests=23, 4 wallclock secs ( 0.01 usr 0.01 sys + 0.49 cusr 0.44
csys = 0.95 CPU)

I thought it was not related with the feature of pg_createsubscriber. It was
related with the slotsync function. In the first place, the error message was
output with the below backtrace.

```
synchronize_one_slot
synchronize_slots
pg_sync_replication_slots
```

And I found the error could be reproduced by the attached script with the source
modification by Alexander [1]/messages/by-id/0dffca12-bf17-4a7a-334d-225569de5e6e@gmail.com. According to my analysis, this could be caused when
1) a replication slot is created with failover = true and XLOG_RUNNING_XACT record is generated, and
2) pg_sync_replication_slots() is called *before* the record is flushed on the
standby. To stabilize the test, we can call wait_for_replay_catchup() after the slot
creation on the primary.

Moreover, this test may suffer from autovacuum:
echo "
autovacuum_naptime = 1
autovacuum_analyze_threshold = 1
" > /tmp/temp.config
TEMP_CONFIG=/tmp/temp.config make -s check -C src/bin/pg_basebackup/
PROVE_TESTS="t/040*"

# +++ tap check in src/bin/pg_basebackup +++
t/040_pg_createsubscriber.pl .. 24/?
# Failed test 'failover slot is synced'
# at t/040_pg_createsubscriber.pl line 273.
# got: ''
# expected: 'failover_slot'
t/040_pg_createsubscriber.pl .. 28/? # Looks like you failed 1 test of 33.
t/040_pg_createsubscriber.pl .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/33 subtests

Thanks for reporting. I could reproduce the failure, and some BF animals said NG.
According to them, the root cause seemed that slot synchronization was failed.

```
LOG: statement: SELECT pg_sync_replication_slots()
LOG: could not sync slot "failover_slot" as remote slot precedes local slot
DETAIL: Remote slot has LSN 0/3006890 and catalog xmin 743, but local slot has LSN 0/3006890 and catalog xmin 744.
```

Based on that, I considered a scenario why the slot could not be synchronized.
I felt this was not caused by the pg_createsubscriber.

1. At initial stage, the xmin of the physical slot is 743, and nextXid of the
primary is also 743.
2. Autovacuum worker starts a new transaction. nextXid is incremented to 744.
3. Tries to creates a logical replication slot with failover=true *before the
transaction at step2 is replicated to the standby*.
4. While creating the slot, the catalog_xmin must be determined.
The initial candidate is nextXid (= 744), but the oldest xmin of replication
slots (=743) is used if it is older than nextXid. So 743 is chosen in this case.
This operaion is done in CreateInitDecodingContext()->GetOldestSafeDecodingContext().
5. After that, the transaction at step2 is reached to the standby node and it
updates the nextXid.
6. Finally runs pg pg_sync_replication_slots() on the standby. It finds a failover
slot on the primary and tries to create on the standby. However, the
catalog_xmin on the primary (743) is older than the nextXid of the standby (744)
so that it skips to create a slot.

To avoid the issue, we can disable the autovacuuming while testing.

# Descriptions for attached files

An attached script can be used to reproduce the first failure without pg_createsubscriber.
It requires to modify the code like [1]/messages/by-id/0dffca12-bf17-4a7a-334d-225569de5e6e@gmail.com.

0001 patch can reduce the wait time of test. See [3]/messages/by-id/OSBPR01MB25521B15BF950D2523BBE143F5D32@OSBPR01MB2552.jpnprd01.prod.outlook.com. I know the approach is bit hacky but
it worked.
0002 patch waits until the WAL record is replicated. This fixes a first failure.
0003 patch disables autovacuum for node_p and node_s. I think node_p is enough, but did
like that just in case. This fixes a second failure.

[1]: /messages/by-id/0dffca12-bf17-4a7a-334d-225569de5e6e@gmail.com
[2]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=adder&amp;dt=2024-07-02%2008%3A45%3A39
[3]: /messages/by-id/OSBPR01MB25521B15BF950D2523BBE143F5D32@OSBPR01MB2552.jpnprd01.prod.outlook.com

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

repro_skipping_slot_sync.shapplication/octet-stream; name=repro_skipping_slot_sync.shDownload
v2-0001-emit-dummy-message-while-setting-up-the-publisher.patchapplication/octet-stream; name=v2-0001-emit-dummy-message-while-setting-up-the-publisher.patchDownload
From e66878760f87687c072a3ff902cba3bfed298b15 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 3 Jul 2024 04:53:26 +0000
Subject: [PATCH v2 1/3] emit dummy message while setting up the publisher

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 26 +++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 21dd50f808..499f26cf38 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -781,6 +781,32 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		disconnect_database(conn, false);
 	}
 
+	/*
+	 * pg_create_logical_replication_slot () returns the end of the
+	 * RUNNING_XACT record. If we use the returned value as recovery_target_lsn
+	 * as-is, however, we must wait for additional WAL generation because the
+	 * parameter requires that the replicated WAL overtake a certain point.
+	 * Insert a dummy WAL record to avoid unnecessary waits.
+	 */
+	if (!dry_run)
+	{
+		PGconn	   *conn;
+		PGresult   *res = NULL;
+
+		conn = connect_database(dbinfo[0].pubconninfo, true);
+
+		/* Insert dummy data to WAL to move forward the WAL record */
+		res = PQexec(conn,
+					 "SELECT pg_catalog.pg_logical_emit_message(true, 'pg_createsubscriber', 'dummy data', true);");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not run CHECKPOINT: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+	}
+
 	return lsn;
 }
 
-- 
2.43.0

v2-0002-wait-until-RUNNING_XACT-is-replicated.patchapplication/octet-stream; name=v2-0002-wait-until-RUNNING_XACT-is-replicated.patchDownload
From 87129dad670100482d8a514dd4a502cade544ac7 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 3 Jul 2024 05:03:11 +0000
Subject: [PATCH v2 2/3] wait until RUNNING_XACT is replicated

---
 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 80002c5a17..ed5ccf2bd7 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -293,6 +293,7 @@ $node_p->safe_psql($db1,
 	"SELECT pg_create_logical_replication_slot('$fslotname', 'pgoutput', false, false, true)"
 );
 $node_s->start;
+$node_p->wait_for_replay_catchup($node_s);
 $node_s->safe_psql('postgres', "SELECT pg_sync_replication_slots()");
 my $result = $node_s->safe_psql('postgres',
 	"SELECT slot_name FROM pg_replication_slots WHERE slot_name = '$fslotname' AND synced AND NOT temporary"
-- 
2.43.0

v2-0003-disable-autovacuum-while-testing.patchapplication/octet-stream; name=v2-0003-disable-autovacuum-while-testing.patchDownload
From 5f4fe7b85f54823094aa0b7c72bac83d31f54c8f Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 3 Jul 2024 05:07:18 +0000
Subject: [PATCH v2 3/3] disable autovacuum while testing

---
 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index ed5ccf2bd7..877a540d7a 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -116,6 +116,7 @@ command_fails(
 my $node_p = PostgreSQL::Test::Cluster->new('node_p');
 my $pconnstr = $node_p->connstr;
 $node_p->init(allows_streaming => 'logical');
+$node_p->append_conf('postgresql.conf', 'autovacuum = off');
 $node_p->start;
 
 # Set up node F as about-to-fail node
@@ -149,6 +150,7 @@ $node_s->append_conf(
 primary_slot_name = '$slotname'
 primary_conninfo = '$pconnstr dbname=postgres'
 hot_standby_feedback = on
+autovacuum = off
 ]);
 $node_s->set_standby_mode();
 $node_s->start;
-- 
2.43.0

#295Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#293)
Re: speed up a logical replica setup

On Wed, Jul 3, 2024 at 9:21 AM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Dear Amit,

IIUC, the problem is that the consistent_lsn value returned by
setup_publisher() is the "end +1" location of the required LSN whereas
the recovery_target_lsn used in wait_for_end_recovery() expects the
LSN value to be "start" location of required LSN.

Yeah, right. It is same as my understanding.

This sounds like an ugly hack to me and don't know if we can use it.

I also think it is hacky, but I could not find better solutions.

The ideal way to fix this is to get the start_lsn from the
create_logical_slot functionality or have some parameter like
recover_target_end_lsn but I don't know if this is a good time to
extend such a functionality.

I felt that such approach might be used for HEAD, but not suitable for PG17.
Alternative approach I came up with was to insert a tuple while waiting the
promotion. It can generate a WAL record so that standby can finish after the
application. But I'm not sure how do we do and it seems to lead an additional
timing issue. Also, this does not improve the behavior of the command - normal
user may have to wait some time by the command.

BTW, I think the time required by standby to reach a consistent state
after startup is any way unpredictable. For example, if we consider
that in real-world scenarios between the time we have stopped standby
and restarted it, there could be many transactions on primary that
need to be replicated before we reach recover_target_lsn.

I don't think adding additional (dummy) WAL records is a good solution
but it is better to hear from others.

Do you have any other idea?

The other idea could be that we use the minimum restart_lsn of all the
slots created by this tool as a consistent_lsn. We can probably get
that value by using pg_get_replication_slots() but this idea needs
further evaluation as to whether it will lead to a consistent
subscriber.

--
With Regards,
Amit Kapila.

#296Amit Kapila
amit.kapila16@gmail.com
In reply to: Amit Kapila (#295)
Re: speed up a logical replica setup

On Wed, Jul 3, 2024 at 11:27 AM Amit Kapila <amit.kapila16@gmail.com> wrote:

Do you have any other idea?

The other idea could be that we use the minimum restart_lsn of all the
slots created by this tool as a consistent_lsn. We can probably get
that value by using pg_get_replication_slots() but this idea needs
further evaluation as to whether it will lead to a consistent
subscriber.

This may not work because when the confirmed_flush LSN of any slot is
ahead of the consistent_lsn value we derived above, we could miss
receiving some transactions. So, I don't have any better ideas than
what I mentioned yesterday.

--
With Regards,
Amit Kapila.

#297Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#294)
Re: speed up a logical replica setup

On Wed, Jul 3, 2024 at 10:42 AM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Based on that, I considered a scenario why the slot could not be synchronized.
I felt this was not caused by the pg_createsubscriber.

1. At initial stage, the xmin of the physical slot is 743, and nextXid of the
primary is also 743.
2. Autovacuum worker starts a new transaction. nextXid is incremented to 744.
3. Tries to creates a logical replication slot with failover=true *before the
transaction at step2 is replicated to the standby*.
4. While creating the slot, the catalog_xmin must be determined.
The initial candidate is nextXid (= 744), but the oldest xmin of replication
slots (=743) is used if it is older than nextXid. So 743 is chosen in this case.
This operaion is done in CreateInitDecodingContext()->GetOldestSafeDecodingContext().
5. After that, the transaction at step2 is reached to the standby node and it
updates the nextXid.
6. Finally runs pg pg_sync_replication_slots() on the standby. It finds a failover
slot on the primary and tries to create on the standby. However, the
catalog_xmin on the primary (743) is older than the nextXid of the standby (744)
so that it skips to create a slot.

To avoid the issue, we can disable the autovacuuming while testing.

Your analysis looks correct to me. The test could fail due to
autovacuum. See the following comment in
040_standby_failover_slots_sync.

# Disable autovacuum to avoid generating xid during stats update as otherwise
# the new XID could then be replicated to standby at some random point making
# slots at primary lag behind standby during slot sync.
$publisher->append_conf('postgresql.conf', 'autovacuum = off');

# Descriptions for attached files

An attached script can be used to reproduce the first failure without pg_createsubscriber.
It requires to modify the code like [1].

0003 patch disables autovacuum for node_p and node_s. I think node_p is enough, but did
like that just in case. This fixes a second failure.

Disabling on the primary node should be sufficient. Let's do the
minimum required to stabilize this test.

--
With Regards,
Amit Kapila.

#298Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Amit Kapila (#297)
3 attachment(s)
RE: speed up a logical replica setup

Dear Amit,

Your analysis looks correct to me. The test could fail due to
autovacuum. See the following comment in
040_standby_failover_slots_sync.

# Disable autovacuum to avoid generating xid during stats update as otherwise
# the new XID could then be replicated to standby at some random point making
# slots at primary lag behind standby during slot sync.
$publisher->append_conf('postgresql.conf', 'autovacuum = off');

Oh, I could not find the comment. I felt it should be added even in
040_pg_createsubscriber.pl. Done.

# Descriptions for attached files

An attached script can be used to reproduce the first failure without

pg_createsubscriber.

It requires to modify the code like [1].

0003 patch disables autovacuum for node_p and node_s. I think node_p is

enough, but did

like that just in case. This fixes a second failure.

Disabling on the primary node should be sufficient. Let's do the
minimum required to stabilize this test.

+1, removed.

PSA new version. 0001 has not been changed yet. A comment was added
in 0002 to clarify why we must wait. For 0003, a comment was added and
setting for standby was reverted.

Best Regards,
Hayato Kuroda
FUJITSU LIMITED
https://www.fujitsu.com/

Attachments:

v3-0001-emit-dummy-message-while-setting-up-the-publisher.patchapplication/octet-stream; name=v3-0001-emit-dummy-message-while-setting-up-the-publisher.patchDownload
From e66878760f87687c072a3ff902cba3bfed298b15 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 3 Jul 2024 04:53:26 +0000
Subject: [PATCH v3 1/3] emit dummy message while setting up the publisher

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 26 +++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 21dd50f808..499f26cf38 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -781,6 +781,32 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		disconnect_database(conn, false);
 	}
 
+	/*
+	 * pg_create_logical_replication_slot () returns the end of the
+	 * RUNNING_XACT record. If we use the returned value as recovery_target_lsn
+	 * as-is, however, we must wait for additional WAL generation because the
+	 * parameter requires that the replicated WAL overtake a certain point.
+	 * Insert a dummy WAL record to avoid unnecessary waits.
+	 */
+	if (!dry_run)
+	{
+		PGconn	   *conn;
+		PGresult   *res = NULL;
+
+		conn = connect_database(dbinfo[0].pubconninfo, true);
+
+		/* Insert dummy data to WAL to move forward the WAL record */
+		res = PQexec(conn,
+					 "SELECT pg_catalog.pg_logical_emit_message(true, 'pg_createsubscriber', 'dummy data', true);");
+
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			pg_log_error("could not run CHECKPOINT: %s",
+						 PQresultErrorMessage(res));
+			disconnect_database(conn, true);
+		}
+	}
+
 	return lsn;
 }
 
-- 
2.43.0

v3-0002-wait-until-RUNNING_XACT-is-replicated.patchapplication/octet-stream; name=v3-0002-wait-until-RUNNING_XACT-is-replicated.patchDownload
From b43f599e802d3a92ef520d1ac6fa3df4e8c3746c Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 3 Jul 2024 05:03:11 +0000
Subject: [PATCH v3 2/3] wait until RUNNING_XACT is replicated

---
 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 80002c5a17..f21a253211 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -293,6 +293,9 @@ $node_p->safe_psql($db1,
 	"SELECT pg_create_logical_replication_slot('$fslotname', 'pgoutput', false, false, true)"
 );
 $node_s->start;
+# Wait for the standby to catch up so that the standby is not lagging behind
+# the failover slot.
+$node_p->wait_for_replay_catchup($node_s);
 $node_s->safe_psql('postgres', "SELECT pg_sync_replication_slots()");
 my $result = $node_s->safe_psql('postgres',
 	"SELECT slot_name FROM pg_replication_slots WHERE slot_name = '$fslotname' AND synced AND NOT temporary"
-- 
2.43.0

v3-0003-disable-autovacuum-while-testing.patchapplication/octet-stream; name=v3-0003-disable-autovacuum-while-testing.patchDownload
From 5a15ad746f22fd49570e3d14f3cfe0c8acfafed6 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 3 Jul 2024 05:07:18 +0000
Subject: [PATCH v3 3/3] disable autovacuum while testing

---
 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index f21a253211..74b90d9a91 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -116,6 +116,10 @@ command_fails(
 my $node_p = PostgreSQL::Test::Cluster->new('node_p');
 my $pconnstr = $node_p->connstr;
 $node_p->init(allows_streaming => 'logical');
+# Disable autovacuum to avoid generating xid during stats update as otherwise
+# the new XID could then be replicated to standby at some random point making
+# slots at primary lag behind standby during slot sync.
+$node_p->append_conf('postgresql.conf', 'autovacuum = off');
 $node_p->start;
 
 # Set up node F as about-to-fail node
-- 
2.43.0

#299Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#298)
Re: speed up a logical replica setup

On Wed, Jul 3, 2024 at 12:42 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Disabling on the primary node should be sufficient. Let's do the
minimum required to stabilize this test.

+1, removed.

PSA new version. 0001 has not been changed yet. A comment was added
in 0002 to clarify why we must wait. For 0003, a comment was added and
setting for standby was reverted.

Pushed 0002 and 0003. Let's wait for a discussion on 0001.

--
With Regards,
Amit Kapila.

#300Alexander Lakhin
exclusion@gmail.com
In reply to: Amit Kapila (#299)
Re: speed up a logical replica setup

Hello Amit and Kuroda-san,

03.07.2024 14:02, Amit Kapila wrote:

Pushed 0002 and 0003. Let's wait for a discussion on 0001.

Please look at another failure of the test [1]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=skink&amp;dt=2024-07-08%2013%3A16%3A35:
[13:28:05.647](2.460s) not ok 26 - failover slot is synced
[13:28:05.648](0.001s) #   Failed test 'failover slot is synced'
#   at /home/bf/bf-build/skink-master/HEAD/pgsql/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl line 307.
[13:28:05.648](0.000s) #          got: ''
#     expected: 'failover_slot'

with 040_pg_createsubscriber_node_s.log containing:
2024-07-08 13:28:05.369 UTC [3985464][client backend][0/2:0] LOG: statement: SELECT pg_sync_replication_slots()
2024-07-08 13:28:05.557 UTC [3985464][client backend][0/2:0] LOG: could not sync slot "failover_slot" as remote slot
precedes local slot
2024-07-08 13:28:05.557 UTC [3985464][client backend][0/2:0] DETAIL:  Remote slot has LSN 0/30047B8 and catalog xmin
743, but local slot has LSN 0/30047B8 and catalog xmin 744.

I could not reproduce it locally, but I've discovered that that subtest
somehow depends on pg_createsubscriber executed for the
'primary contains unmet conditions on node P' check. For example with this
test modification:
@@ -249,7 +249,7 @@ command_fails(
         $node_p->connstr($db1), '--socket-directory',
         $node_s->host, '--subscriber-port',
         $node_s->port, '--database',
-        $db1, '--database',
+        'XXX', '--database',
         $db2
     ],
     'primary contains unmet conditions on node P');

I see the same failure:
2024-07-09 10:19:43.284 UTC [938890] 040_pg_createsubscriber.pl LOG:  statement: SELECT pg_sync_replication_slots()
2024-07-09 10:19:43.292 UTC [938890] 040_pg_createsubscriber.pl LOG:  could not sync slot "failover_slot" as remote slot
precedes local slot
2024-07-09 10:19:43.292 UTC [938890] 040_pg_createsubscriber.pl DETAIL:  Remote slot has LSN 0/3004780 and catalog xmin
743, but local slot has LSN 0/3004780 and catalog xmin 744.

Thus maybe even a normal pg_createsubscriber run can affect the primary
server (it's catalog xmin) differently?

One difference I found in the logs, is that the skink failure's
regress_log_040_pg_createsubscriber contains:
pg_createsubscriber: error: publisher requires 2 wal sender processes, but only 1 remain

Though for a successful run I see locally (I can't find logs of
successful test runs on skink):
pg_createsubscriber: error: publisher requires 2 wal sender processes, but only 0 remain

[1]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=skink&amp;dt=2024-07-08%2013%3A16%3A35

Best regards,
Alexander

#301Euler Taveira
euler@eulerto.com
In reply to: Alexander Lakhin (#300)
1 attachment(s)
Re: speed up a logical replica setup

On Tue, Jul 9, 2024, at 8:00 AM, Alexander Lakhin wrote:

Hello Amit and Kuroda-san,

03.07.2024 14:02, Amit Kapila wrote:

Pushed 0002 and 0003. Let's wait for a discussion on 0001.

Please look at another failure of the test [1]:
[13:28:05.647](2.460s) not ok 26 - failover slot is synced
[13:28:05.648](0.001s) # Failed test 'failover slot is synced'
# at /home/bf/bf-build/skink-master/HEAD/pgsql/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl line 307.
[13:28:05.648](0.000s) # got: ''
# expected: 'failover_slot'

I'm wondering if the attached patch is sufficient to move the restart_lsn
forward. I experimented several lightweight ideas but none works. BTW the steps
to create the failover slot here is similar 040_standby_failover_slots_sync.pl.
I don't have a clue why it is failing for this one.

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

fix.difftext/x-patch; name=fix.diffDownload
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 74b90d9a913..232dbbbc55e 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -297,6 +297,17 @@ $node_p->safe_psql($db1,
 	"SELECT pg_create_logical_replication_slot('$fslotname', 'pgoutput', false, false, true)"
 );
 $node_s->start;
+$node_p->safe_psql($db1, qq(
+	SELECT pg_logical_emit_message(true, 'a', '1');
+	CHECKPOINT;
+));
+$node_p->safe_psql($db1, qq(
+	SELECT *
+	FROM pg_logical_slot_get_binary_changes('$fslotname', NULL, NULL,
+		'proto_version', '1',
+		'publication_names', 'dummy',
+		'messages', 'true');
+));
 # Wait for the standby to catch up so that the standby is not lagging behind
 # the failover slot.
 $node_p->wait_for_replay_catchup($node_s);
#302Amit Kapila
amit.kapila16@gmail.com
In reply to: Alexander Lakhin (#300)
Re: speed up a logical replica setup

On Tue, Jul 9, 2024 at 4:30 PM Alexander Lakhin <exclusion@gmail.com> wrote:

Please look at another failure of the test [1]:
[13:28:05.647](2.460s) not ok 26 - failover slot is synced
[13:28:05.648](0.001s) # Failed test 'failover slot is synced'
# at /home/bf/bf-build/skink-master/HEAD/pgsql/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl line 307.
[13:28:05.648](0.000s) # got: ''
# expected: 'failover_slot'

with 040_pg_createsubscriber_node_s.log containing:
2024-07-08 13:28:05.369 UTC [3985464][client backend][0/2:0] LOG: statement: SELECT pg_sync_replication_slots()
2024-07-08 13:28:05.557 UTC [3985464][client backend][0/2:0] LOG: could not sync slot "failover_slot" as remote slot
precedes local slot
2024-07-08 13:28:05.557 UTC [3985464][client backend][0/2:0] DETAIL: Remote slot has LSN 0/30047B8 and catalog xmin
743, but local slot has LSN 0/30047B8 and catalog xmin 744.

I could not reproduce it locally, but I've discovered that that subtest
somehow depends on pg_createsubscriber executed for the
'primary contains unmet conditions on node P' check. For example with this
test modification:
@@ -249,7 +249,7 @@ command_fails(
$node_p->connstr($db1), '--socket-directory',
$node_s->host, '--subscriber-port',
$node_s->port, '--database',
- $db1, '--database',
+ 'XXX', '--database',
$db2
],
'primary contains unmet conditions on node P');

I see the same failure:
2024-07-09 10:19:43.284 UTC [938890] 040_pg_createsubscriber.pl LOG: statement: SELECT pg_sync_replication_slots()
2024-07-09 10:19:43.292 UTC [938890] 040_pg_createsubscriber.pl LOG: could not sync slot "failover_slot" as remote slot
precedes local slot
2024-07-09 10:19:43.292 UTC [938890] 040_pg_createsubscriber.pl DETAIL: Remote slot has LSN 0/3004780 and catalog xmin
743, but local slot has LSN 0/3004780 and catalog xmin 744.

Thus maybe even a normal pg_createsubscriber run can affect the primary
server (it's catalog xmin) differently?

Yes, pg_createsubscriber can affect the primary server's catalog xmin
because it starts the standby server that can send HSFeedback (See
XLogWalRcvSendHSFeedback()), which can advance the physical slot's
xmin corresponding the following Insert in the test:

# Insert another row on node P and wait node S to catch up
$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('second row')");
$node_p->wait_for_replay_catchup($node_s);

In the success case, pg_createsubscriber is able to send HSFeedback
and in the failure case, it won't. We can see the following logs in
040_pg_createsubscriber_node_p.log:

2024-07-08 13:28:00.872 UTC [3982331][walsender][:0] FATAL: the
database system is starting up
2024-07-08 13:28:00.875 UTC [3982328][startup][:0] LOG: database
system was shut down at 2024-07-08 13:28:00 UTC
2024-07-08 13:28:01.105 UTC [3981996][postmaster][:0] LOG: database
system is ready to accept connections

This shows that when the test 'primary contains unmet conditions on
node P' starts the standby server the corresponding primary node was
not ready because we just restarted node_p before that test and didn't
ensure that the node_p is up and ready to accept connections before
starting the pg_createsubscriber test.

Even in the successful cases where the standby is able to connect to
primary for test 'primary contains unmet conditions on node P', there
is no guarantee that xmin of the physical slot will be updated at
least, we don't have anything in the test to ensure the same.

Now as before creating logical replication, we didn't ensure that the
physical slot's xmin has been caught up to the latest value, the test
can lead to failure like: "Remote slot has LSN 0/3004780 and catalog
xmin 743, but local slot has LSN 0/3004780 and catalog xmin 744".

The xmin on standby could have been advanced due to the following Insert:
# Insert another row on node P and wait node S to catch up
$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('second row')");
$node_p->wait_for_replay_catchup($node_s);

We don't wait for the xmin to catch up corresponding to this insert
and I don't know if there is a way to do that. So, we should move this
Insert to after the call to pg_sync_replication_slots(). It won't
impact the general test of pg_createsubscriber.

Thanks to Hou-San for helping me in the analysis of this BF failure.

--
With Regards,
Amit Kapila.

#303Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#301)
Re: speed up a logical replica setup

On Wed, Jul 10, 2024 at 4:51 PM Euler Taveira <euler@eulerto.com> wrote:

On Tue, Jul 9, 2024, at 8:00 AM, Alexander Lakhin wrote:

Hello Amit and Kuroda-san,

03.07.2024 14:02, Amit Kapila wrote:

Pushed 0002 and 0003. Let's wait for a discussion on 0001.

Please look at another failure of the test [1]:
[13:28:05.647](2.460s) not ok 26 - failover slot is synced
[13:28:05.648](0.001s) # Failed test 'failover slot is synced'
# at /home/bf/bf-build/skink-master/HEAD/pgsql/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl line 307.
[13:28:05.648](0.000s) # got: ''
# expected: 'failover_slot'

I'm wondering if the attached patch is sufficient to move the restart_lsn
forward.

This won't guarantee that xmin of the logical slot on the primary has
moved ahead or at least we haven't ensured the same. So, we should try
not to perform statements that can increase the xmin on standby before
creating a logical slot. See the analysis sent in the previous email.

--
With Regards,
Amit Kapila.

#304Zhijie Hou (Fujitsu)
houzj.fnst@fujitsu.com
In reply to: Amit Kapila (#302)
1 attachment(s)
RE: speed up a logical replica setup

On Thursday, July 11, 2024 6:21 PM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Tue, Jul 9, 2024 at 4:30 PM Alexander Lakhin <exclusion@gmail.com>
wrote:

Please look at another failure of the test [1]:
[13:28:05.647](2.460s) not ok 26 - failover slot is synced
[13:28:05.648](0.001s) # Failed test 'failover slot is synced'
# at

/home/bf/bf-build/skink-master/HEAD/pgsql/src/bin/pg_basebackup/t/04
0_pg_createsubscriber.pl line 307.

[13:28:05.648](0.000s) # got: ''
# expected: 'failover_slot'

with 040_pg_createsubscriber_node_s.log containing:
2024-07-08 13:28:05.369 UTC [3985464][client backend][0/2:0] LOG:
statement: SELECT pg_sync_replication_slots()
2024-07-08 13:28:05.557 UTC [3985464][client backend][0/2:0] LOG:
could not sync slot "failover_slot" as remote slot precedes local slot
2024-07-08 13:28:05.557 UTC [3985464][client backend][0/2:0] DETAIL:
Remote slot has LSN 0/30047B8 and catalog xmin 743, but local slot has LSN

0/30047B8 and catalog xmin 744.

I could not reproduce it locally, but I've discovered that that
subtest somehow depends on pg_createsubscriber executed for the
'primary contains unmet conditions on node P' check. For example with
this test modification:
@@ -249,7 +249,7 @@ command_fails(
$node_p->connstr($db1), '--socket-directory',
$node_s->host, '--subscriber-port',
$node_s->port, '--database',
- $db1, '--database',
+ 'XXX', '--database',
$db2
],
'primary contains unmet conditions on node P');

I see the same failure:
2024-07-09 10:19:43.284 UTC [938890] 040_pg_createsubscriber.pl LOG:
statement: SELECT pg_sync_replication_slots()
2024-07-09 10:19:43.292 UTC [938890] 040_pg_createsubscriber.pl LOG:
could not sync slot "failover_slot" as remote slot precedes local slot
2024-07-09 10:19:43.292 UTC [938890] 040_pg_createsubscriber.pl
DETAIL: Remote slot has LSN 0/3004780 and catalog xmin 743, but local

slot has LSN 0/3004780 and catalog xmin 744.

Thus maybe even a normal pg_createsubscriber run can affect the
primary server (it's catalog xmin) differently?

Yes, pg_createsubscriber can affect the primary server's catalog xmin because
it starts the standby server that can send HSFeedback (See
XLogWalRcvSendHSFeedback()), which can advance the physical slot's xmin
corresponding the following Insert in the test:

# Insert another row on node P and wait node S to catch up
$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('second row')");
$node_p->wait_for_replay_catchup($node_s);

In the success case, pg_createsubscriber is able to send HSFeedback and in
the failure case, it won't. We can see the following logs in
040_pg_createsubscriber_node_p.log:

2024-07-08 13:28:00.872 UTC [3982331][walsender][:0] FATAL: the database
system is starting up
2024-07-08 13:28:00.875 UTC [3982328][startup][:0] LOG: database system
was shut down at 2024-07-08 13:28:00 UTC
2024-07-08 13:28:01.105 UTC [3981996][postmaster][:0] LOG: database
system is ready to accept connections

This shows that when the test 'primary contains unmet conditions on node P'
starts the standby server the corresponding primary node was not ready
because we just restarted node_p before that test and didn't ensure that the
node_p is up and ready to accept connections before starting the
pg_createsubscriber test.

Even in the successful cases where the standby is able to connect to primary
for test 'primary contains unmet conditions on node P', there is no guarantee
that xmin of the physical slot will be updated at least, we don't have anything in
the test to ensure the same.

Now as before creating logical replication, we didn't ensure that the physical
slot's xmin has been caught up to the latest value, the test can lead to failure
like: "Remote slot has LSN 0/3004780 and catalog xmin 743, but local slot has
LSN 0/3004780 and catalog xmin 744".

The xmin on standby could have been advanced due to the following Insert:
# Insert another row on node P and wait node S to catch up
$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('second row')");
$node_p->wait_for_replay_catchup($node_s);

We don't wait for the xmin to catch up corresponding to this insert and I don't
know if there is a way to do that. So, we should move this Insert to after the call
to pg_sync_replication_slots(). It won't impact the general test of
pg_createsubscriber.

The analysis and suggestion look reasonable to me.
Here is a small patch which does the same.

Thanks to Hou-San for helping me in the analysis of this BF failure.

Best Regards,
Hou zj

Attachments:

0001-fix-unstable-test-in-040_pg_createsubscriber.patchapplication/octet-stream; name=0001-fix-unstable-test-in-040_pg_createsubscriber.patchDownload
From e141bbe2598117aabd76730cca109bd3470b5018 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <houzj.fnst@cn.fujitsu.com>
Date: Thu, 11 Jul 2024 18:59:13 +0800
Subject: [PATCH] fix unstable test in 040_pg_createsubscriber

In this test, the INSERT before slot synchronization results in the
generation of a new xid. The new xid could be replicated to the standby
before the xmin of physical slot on the primary catches up, as the xmin is
updated independently through hot standby feedback. To address this issue
and ensure slot synchronization reliability, the INSERT operation has been
moved to occur after the logical slot has been successfully synchronized.

---
 src/bin/pg_basebackup/t/040_pg_createsubscriber.pl | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 74b90d9a91..cdac75768b 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -227,10 +227,6 @@ command_fails(
 	],
 	'primary server is in recovery');
 
-# Insert another row on node P and wait node S to catch up
-$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('second row')");
-$node_p->wait_for_replay_catchup($node_s);
-
 # Check some unmet conditions on node P
 $node_p->append_conf(
 	'postgresql.conf', q{
@@ -306,6 +302,10 @@ my $result = $node_s->safe_psql('postgres',
 );
 is($result, 'failover_slot', 'failover slot is synced');
 
+# Insert another row on node P and wait node S to catch up
+$node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('second row')");
+$node_p->wait_for_replay_catchup($node_s);
+
 # Create subscription to test its removal
 my $dummy_sub = 'regress_sub_dummy';
 $node_p->safe_psql($db1,
-- 
2.30.0.windows.2

#305Euler Taveira
euler@eulerto.com
In reply to: Zhijie Hou (Fujitsu) (#304)
Re: speed up a logical replica setup

On Thu, Jul 11, 2024, at 8:22 AM, Zhijie Hou (Fujitsu) wrote:

The analysis and suggestion look reasonable to me.
Here is a small patch which does the same.

LGTM. However, I would add a comment above the INSERT you moved around to remind
you that a transaction cannot be added there because it will break this test.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#306Alexander Lakhin
exclusion@gmail.com
In reply to: Amit Kapila (#302)
Re: speed up a logical replica setup

Hello Amit and Hou-San,

11.07.2024 13:21, Amit Kapila wrote:

We don't wait for the xmin to catch up corresponding to this insert
and I don't know if there is a way to do that. So, we should move this
Insert to after the call to pg_sync_replication_slots(). It won't
impact the general test of pg_createsubscriber.

Thanks to Hou-San for helping me in the analysis of this BF failure.

Thank you for investigating that issue!

May I ask you to look at another failure of the test occurred today [1]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=olingo&amp;dt=2024-07-11%2007%3A25%3A12?
The failure log contains:
recovery_target_lsn = '0/30098D0'
pg_createsubscriber: starting the subscriber
...
pg_createsubscriber: server was started
pg_createsubscriber: waiting for the target server to reach the consistent state
...
2024-07-11 07:40:10.837 UTC [2948531][postmaster][:0] LOG:  received fast shutdown request

Though what I'm seeing after a successful run is:
recovery_target_lsn = '0/3009860'
pg_createsubscriber: starting the subscriber
...
pg_createsubscriber: server was started
pg_createsubscriber: waiting for the target server to reach the consistent state
...
2024-07-11 15:19:40.733 UTC [207517] 040_pg_createsubscriber.pl LOG:  statement: SELECT pg_catalog.pg_is_in_recovery()
2024-07-11 15:19:41.635 UTC [207514] LOG:  recovery stopping after WAL location (LSN) "0/3009860"
2024-07-11 15:19:41.635 UTC [207514] LOG:  redo done at 0/3009860 system usage: CPU: user: 0.00 s, system: 0.00 s,
elapsed: 21.00 s

I've managed to reproduce the failure locally. With the bgwriter mod:
-#define LOG_SNAPSHOT_INTERVAL_MS 15000
+#define LOG_SNAPSHOT_INTERVAL_MS 150

and wal_debug=on, when running 5 test instances with parallel, I get the
failure with the following log:
recovery_target_lsn = '0/3009A20'
pg_createsubscriber: starting the subscriber

2024-07-11 14:40:04.551 UTC [205589:72][startup][33/0:0] LOG:  REDO @ 0/30099E8; LSN 0/3009A20: prev 0/30099B0; xid 0;
len 24 - Standby/RUNNING_XACTS: nextXid 747 latestCompletedXid 746 oldestRunningXid 747
# ^^^ the last REDO record in the log
...
pg_createsubscriber: server was started
pg_createsubscriber: waiting for the target server to reach the consistent state
...
pg_createsubscriber: server was stopped
pg_createsubscriber: error: recovery timed out
...
[14:43:06.011](181.800s) not ok 30 - run pg_createsubscriber on node S
[14:43:06.012](0.000s)
[14:43:06.012](0.000s) #   Failed test 'run pg_createsubscriber on node S'
#   at t/040_pg_createsubscriber.pl line 356.

$ pg_waldump -p src/bin/pg_basebackup_1/tmp_check/t_040_pg_createsubscriber_node_s_data/pgdata/pg_wal/
000000010000000000000003 000000010000000000000003 | tail -2
rmgr: Standby     len (rec/tot):     50/    50, tx:          0, lsn: 0/030099B0, prev 0/03009948, desc: RUNNING_XACTS
nextXid 747 latestCompletedXid 746 oldestRunningXid 747
rmgr: Standby     len (rec/tot):     50/    50, tx:          0, lsn: 0/030099E8, prev 0/030099B0, desc: RUNNING_XACTS
nextXid 747 latestCompletedXid 746 oldestRunningXid 747

Whilst
$ pg_waldump -p src/bin/pg_basebackup_1/tmp_check/t_040_pg_createsubscriber_node_p_data/pgdata/pg_wal/
000000010000000000000003 000000010000000000000003 | tail
rmgr: Standby     len (rec/tot):     50/    50, tx:          0, lsn: 0/030099B0, prev 0/03009948, desc: RUNNING_XACTS
nextXid 747 latestCompletedXid 746 oldestRunningXid 747
rmgr: Standby     len (rec/tot):     50/    50, tx:          0, lsn: 0/030099E8, prev 0/030099B0, desc: RUNNING_XACTS
nextXid 747 latestCompletedXid 746 oldestRunningXid 747
rmgr: Heap2       len (rec/tot):     60/    60, tx:        747, lsn: 0/03009A20, prev 0/030099E8, desc: NEW_CID rel:
1663/16384/6104, tid: 0/1, cmin: 4294967295, cmax: 0, combo: 4294967295
rmgr: Heap        len (rec/tot):     54/    54, tx:        747, lsn: 0/03009A60, prev 0/03009A20, desc: DELETE xmax:
747, off: 1, infobits: [KEYS_UPDATED], flags: 0x00, blkref #0: rel 1663/16384/6104 blk 0
rmgr: Transaction len (rec/tot):     78/    78, tx:        747, lsn: 0/03009A98, prev 0/03009A60, desc: INVALIDATION ;
inval msgs: catcache 49 catcache 46 relcache 0
rmgr: Transaction len (rec/tot):     98/    98, tx:        747, lsn: 0/03009AE8, prev 0/03009A98, desc: COMMIT
2024-07-11 14:43:05.994561 UTC; relcache init file inval dbid 16384 tsid 1663; inval msgs: catcache 49 catcache 46
relcache 0
rmgr: Heap2       len (rec/tot):     60/    60, tx:        748, lsn: 0/03009B50, prev 0/03009AE8, desc: NEW_CID rel:
1663/16385/6104, tid: 0/1, cmin: 4294967295, cmax: 0, combo: 4294967295
rmgr: Heap        len (rec/tot):     54/    54, tx:        748, lsn: 0/03009B90, prev 0/03009B50, desc: DELETE xmax:
748, off: 1, infobits: [KEYS_UPDATED], flags: 0x00, blkref #0: rel 1663/16385/6104 blk 0
rmgr: Transaction len (rec/tot):     78/    78, tx:        748, lsn: 0/03009BC8, prev 0/03009B90, desc: INVALIDATION ;
inval msgs: catcache 49 catcache 46 relcache 0
rmgr: Transaction len (rec/tot):     98/    98, tx:        748, lsn: 0/03009C18, prev 0/03009BC8, desc: COMMIT
2024-07-11 14:43:06.008619 UTC; relcache init file inval dbid 16385 tsid 1663; inval msgs: catcache 49 catcache 46
relcache 0

[1]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=olingo&amp;dt=2024-07-11%2007%3A25%3A12

Best regards,
Alexander

#307Euler Taveira
euler@eulerto.com
In reply to: Alexander Lakhin (#306)
1 attachment(s)
Re: speed up a logical replica setup

On Thu, Jul 11, 2024, at 2:00 PM, Alexander Lakhin wrote:

Hello Amit and Hou-San,

11.07.2024 13:21, Amit Kapila wrote:

We don't wait for the xmin to catch up corresponding to this insert
and I don't know if there is a way to do that. So, we should move this
Insert to after the call to pg_sync_replication_slots(). It won't
impact the general test of pg_createsubscriber.

Thanks to Hou-San for helping me in the analysis of this BF failure.

Thank you for investigating that issue!

May I ask you to look at another failure of the test occurred today [1]?

Thanks for the report!

You are observing the same issue that Amit explained in [1]/messages/by-id/CAA4eK1+p+7Ag6nqdFRdqowK1EmJ6bG-MtZQ_54dnFBi=_OO5RQ@mail.gmail.com. The
pg_create_logical_replication_slot returns the EndRecPtr (see
slot->data.confirmed_flush in DecodingContextFindStartpoint()). EndRecPtr points
to the next record and it is a future position for an idle server. That's why
the recovery takes some time to finish because it is waiting for an activity to
increase the LSN position. Since you modified LOG_SNAPSHOT_INTERVAL_MS to create
additional WAL records soon, the EndRecPtr position is reached rapidly and the
recovery ends quickly.

Hayato proposes a patch [2]/messages/by-id/OSBPR01MB25521B15BF950D2523BBE143F5D32@OSBPR01MB2552.jpnprd01.prod.outlook.com to create an additional WAL record that has the same
effect from you little hack: increase the LSN position to allow the recovery
finishes soon. I don't like the solution although it seems simple to implement.
As Amit said if we know the ReadRecPtr, we could use it as consistent LSN. The
problem is that it is used by logical decoding but it is not exposed. [reading
the code...] When the logical replication slot is created, restart_lsn points to
the lastReplayedEndRecPtr (see ReplicationSlotReserveWal()) that is the last
record replayed. Since the replication slots aren't in use, we could use the
restart_lsn from the last replication slot as a consistent LSN.

I'm attaching a patch that implements it.It runs in 6s instead of 26s.

[1]: /messages/by-id/CAA4eK1+p+7Ag6nqdFRdqowK1EmJ6bG-MtZQ_54dnFBi=_OO5RQ@mail.gmail.com
[2]: /messages/by-id/OSBPR01MB25521B15BF950D2523BBE143F5D32@OSBPR01MB2552.jpnprd01.prod.outlook.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

0001-pg_createsubscriber-fix-slow-recovery.patchtext/x-patch; name="=?UTF-8?Q?0001-pg=5Fcreatesubscriber-fix-slow-recovery.patch?="Download
From a3be65c9bea08d3c7ffd575342a879d37b28b9a8 Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Thu, 11 Jul 2024 19:59:56 -0300
Subject: [PATCH] pg_createsubscriber: fix slow recovery

If the primary server is idle when you are running pg_createsubscriber,
it used to take some time during recovery. The reason is that it was
using the LSN returned by pg_create_logical_replication_slot as
recovery_target_lsn. This LSN points to "end + 1" record that might not
be available at WAL, hence, the recovery routine waits until some
activity from auxiliary processes write WAL records and once it reaches
the recovery_target_lsn position.

Instead, use restart_lsn from the last replication slot. It points to
the last WAL record that was replayed. Hence, the recovery finishes
soon.

create_logical_replication_slot() does not return LSN so change its
signature.

Discussion: https://www.postgresql.org/message-id/2377319.1719766794%40sss.pgh.pa.us
Discussion: https://www.postgresql.org/message-id/68de6498-0449-a113-dd03-e198dded0bac%40gmail.com
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 56 ++++++++++++++++-----
 1 file changed, 43 insertions(+), 13 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 21dd50f8089..5ef3f751a5b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -86,8 +86,8 @@ static void setup_recovery(const struct LogicalRepInfo *dbinfo, const char *data
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  const char *slotname);
 static void drop_failover_replication_slots(struct LogicalRepInfo *dbinfo);
-static char *create_logical_replication_slot(PGconn *conn,
-											 struct LogicalRepInfo *dbinfo);
+static bool create_logical_replication_slot(PGconn *conn,
+											struct LogicalRepInfo *dbinfo);
 static void drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo,
 								  const char *slot_name);
 static void pg_ctl_status(const char *pg_ctl_cmd, int rc);
@@ -769,15 +769,47 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		create_publication(conn, &dbinfo[i]);
 
 		/* Create replication slot on publisher */
-		if (lsn)
-			pg_free(lsn);
-		lsn = create_logical_replication_slot(conn, &dbinfo[i]);
-		if (lsn != NULL || dry_run)
+		if (create_logical_replication_slot(conn, &dbinfo[i]) || dry_run)
 			pg_log_info("create replication slot \"%s\" on publisher",
 						dbinfo[i].replslotname);
 		else
 			exit(1);
 
+		/*
+		 * Get restart_lsn from the last replication slot. It points to the
+		 * last record replayed. The LSN returned by
+		 * pg_create_logical_replication_slot() should not be used because it
+		 * points to a future position. In case the server is idle while
+		 * running this tool, the recovery will not finish soon because the
+		 * future position was not reached. Hence, obtain the restart_lsn that
+		 * points to a LSN already available at WAL. The recovery parameters
+		 * guarantee that this last replication slot will be applied.
+		 */
+		if ((i == num_dbs - 1) && !dry_run)
+		{
+			PQExpBuffer str = createPQExpBuffer();
+			PGresult   *res;
+
+			appendPQExpBuffer(str,
+							  "SELECT restart_lsn FROM pg_catalog.pg_replication_slots "
+							  "WHERE slot_name = '%s'",
+							  dbinfo[i].replslotname);
+			res = PQexec(conn, str->data);
+			if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			{
+				pg_log_error("could not obtain consistent LSN: %s",
+							 PQresultErrorMessage(res));
+				disconnect_database(conn, true);
+			}
+			else
+			{
+				lsn = pg_strdup(PQgetvalue(res, 0, 0));
+			}
+
+			PQclear(res);
+			destroyPQExpBuffer(str);
+		}
+
 		disconnect_database(conn, false);
 	}
 
@@ -1283,19 +1315,18 @@ drop_failover_replication_slots(struct LogicalRepInfo *dbinfo)
 }
 
 /*
- * Create a logical replication slot and returns a LSN.
+ * Create a logical replication slot.
  *
  * CreateReplicationSlot() is not used because it does not provide the one-row
  * result set that contains the LSN.
  */
-static char *
+static bool
 create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
 {
 	PQExpBuffer str = createPQExpBuffer();
 	PGresult   *res = NULL;
 	const char *slot_name = dbinfo->replslotname;
 	char	   *slot_name_esc;
-	char	   *lsn = NULL;
 
 	Assert(conn != NULL);
 
@@ -1305,7 +1336,7 @@ create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
 	slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name));
 
 	appendPQExpBuffer(str,
-					  "SELECT lsn FROM pg_catalog.pg_create_logical_replication_slot(%s, 'pgoutput', false, false, false)",
+					  "SELECT * FROM pg_catalog.pg_create_logical_replication_slot(%s, 'pgoutput', false, false, false)",
 					  slot_name_esc);
 
 	pg_free(slot_name_esc);
@@ -1322,10 +1353,9 @@ create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
 						 PQresultErrorMessage(res));
 			PQclear(res);
 			destroyPQExpBuffer(str);
-			return NULL;
+			return false;
 		}
 
-		lsn = pg_strdup(PQgetvalue(res, 0, 0));
 		PQclear(res);
 	}
 
@@ -1334,7 +1364,7 @@ create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo)
 
 	destroyPQExpBuffer(str);
 
-	return lsn;
+	return true;
 }
 
 static void
-- 
2.30.2

#308Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#305)
Re: speed up a logical replica setup

On Thu, Jul 11, 2024 at 7:04 PM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Jul 11, 2024, at 8:22 AM, Zhijie Hou (Fujitsu) wrote:

The analysis and suggestion look reasonable to me.
Here is a small patch which does the same.

LGTM. However, I would add a comment above the INSERT you moved around to remind
you that a transaction cannot be added there because it will break this test.

Pushed after adding a comment.

--
With Regards,
Amit Kapila.

#309Amit Kapila
amit.kapila16@gmail.com
In reply to: Euler Taveira (#307)
Re: speed up a logical replica setup

On Fri, Jul 12, 2024 at 4:54 AM Euler Taveira <euler@eulerto.com> wrote:

On Thu, Jul 11, 2024, at 2:00 PM, Alexander Lakhin wrote:

May I ask you to look at another failure of the test occurred today [1]?

Thanks for the report!

You are observing the same issue that Amit explained in [1]. The
pg_create_logical_replication_slot returns the EndRecPtr (see
slot->data.confirmed_flush in DecodingContextFindStartpoint()). EndRecPtr points
to the next record and it is a future position for an idle server. That's why
the recovery takes some time to finish because it is waiting for an activity to
increase the LSN position. Since you modified LOG_SNAPSHOT_INTERVAL_MS to create
additional WAL records soon, the EndRecPtr position is reached rapidly and the
recovery ends quickly.

If the recovery ends quickly (which is expected due to reduced
LOG_SNAPSHOT_INTERVAL_MS ) then why do we see "error: recovery timed
out"?

Hayato proposes a patch [2] to create an additional WAL record that has the same
effect from you little hack: increase the LSN position to allow the recovery
finishes soon. I don't like the solution although it seems simple to implement.
As Amit said if we know the ReadRecPtr, we could use it as consistent LSN. The
problem is that it is used by logical decoding but it is not exposed. [reading
the code...] When the logical replication slot is created, restart_lsn points to
the lastReplayedEndRecPtr (see ReplicationSlotReserveWal()) that is the last
record replayed.

The last 'lastReplayedEndRecPtr' should be the value of restart_lsn on
standby (when RecoveryInProgress is true) but here we are creating
slots on the publisher/primary, so shouldn't restart_lsn point to
"latest WAL insert pointer"?

--
With Regards,
Amit Kapila.

#310Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#307)
RE: speed up a logical replica setup

Dear Alexander, Euler, Amit,

I also analyzed this failure, let me share it. Here, I think events in below
ordering were occurred.

1. Backend created a publication on $db2,
2. BGWriter generated RUNNING_XACT record, then
3. Backend created a replication slot on $db2.

In this case, the recovery_target_lsn is ahead of the RUNNING_XACT record generated
at step 3. Also, since both bgwriter and slot creation mark the record as
*UNIMPORTANT* one, the writer won't start again even after the
LOG_SNAPSHOT_INTERVAL_MS. The rule is written in BackgroundWriterMain():

```
/*
* Only log if enough time has passed and interesting records have
* been inserted since the last snapshot. Have to compare with <=
* instead of < because GetLastImportantRecPtr() points at the
* start of a record, whereas last_snapshot_lsn points just past
* the end of the record.
*/
if (now >= timeout &&
last_snapshot_lsn <= GetLastImportantRecPtr())
{
last_snapshot_lsn = LogStandbySnapshot();
last_snapshot_ts = now;
}
```

Therefore, pg_createsubscriber waited until a new record was replicated, but no
activities were recorded, causing a timeout. Since this is a timing issue, Alexander
could reproduce the failure with shorter time duration and parallel running.

IIUC, the root cause is that pg_create_logical_replication_slot() returns a LSN
which is not generated yet. So, I think both mine [1]/messages/by-id/OSBPR01MB25521B15BF950D2523BBE143F5D32@OSBPR01MB2552.jpnprd01.prod.outlook.com and Euler's approach [2]/messages/by-id/b1f0f8c7-8f01-4950-af77-339df3dc4684@app.fastmail.com
can solve the issue. My proposal was to add an extra WAL record after the final
slot creation, and Euler's one was to use a restart_lsn as the recovery_target_lsn.
In case of primary server, restart_lsn is set to the latest WAL insert position and
then RUNNING_XACT record is generated later.

How do you think?

[1]: /messages/by-id/OSBPR01MB25521B15BF950D2523BBE143F5D32@OSBPR01MB2552.jpnprd01.prod.outlook.com
[2]: /messages/by-id/b1f0f8c7-8f01-4950-af77-339df3dc4684@app.fastmail.com

Best regards,
Hayato Kuroda
FUJITSU LIMITED

#311Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#310)
Re: speed up a logical replica setup

On Wed, Jul 17, 2024 at 1:23 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

I also analyzed this failure, let me share it. Here, I think events in below
ordering were occurred.

1. Backend created a publication on $db2,
2. BGWriter generated RUNNING_XACT record, then
3. Backend created a replication slot on $db2.

In this case, the recovery_target_lsn is ahead of the RUNNING_XACT record generated
at step 3. Also, since both bgwriter and slot creation mark the record as
*UNIMPORTANT* one, the writer won't start again even after the
LOG_SNAPSHOT_INTERVAL_MS. The rule is written in BackgroundWriterMain():

```
/*
* Only log if enough time has passed and interesting records have
* been inserted since the last snapshot. Have to compare with <=
* instead of < because GetLastImportantRecPtr() points at the
* start of a record, whereas last_snapshot_lsn points just past
* the end of the record.
*/
if (now >= timeout &&
last_snapshot_lsn <= GetLastImportantRecPtr())
{
last_snapshot_lsn = LogStandbySnapshot();
last_snapshot_ts = now;
}
```

Therefore, pg_createsubscriber waited until a new record was replicated, but no
activities were recorded, causing a timeout. Since this is a timing issue, Alexander
could reproduce the failure with shorter time duration and parallel running.

Your analysis sounds correct to me.

IIUC, the root cause is that pg_create_logical_replication_slot() returns a LSN
which is not generated yet. So, I think both mine [1] and Euler's approach [2]
can solve the issue. My proposal was to add an extra WAL record after the final
slot creation, and Euler's one was to use a restart_lsn as the recovery_target_lsn.

I don't think it is correct to set restart_lsn as consistent_lsn point
because the same is used to set replication origin progress. Later
when we start the subscriber, the system will use that LSN as a
start_decoding_at point which is the point after which all the commits
will be replicated. So, we will end up incorrectly using restart_lsn
(LSN from where we start reading the WAL) as start_decoding_at point.
How could that be correct?

Now, even if we use restart_lsn as recovery_target_lsn and the LSN
returned by pg_create_logical_replication_slot() as consistent LSN to
set replication progress, that also could lead to data loss because
the subscriber may never get data between restart_lsn value and
consistent LSN value.

--
With Regards,
Amit Kapila.

#312Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Amit Kapila (#311)
RE: speed up a logical replica setup

Dear Amit,

Your analysis sounds correct to me.

Okay, so we could have a same picture...

IIUC, the root cause is that pg_create_logical_replication_slot() returns a LSN
which is not generated yet. So, I think both mine [1] and Euler's approach [2]
can solve the issue. My proposal was to add an extra WAL record after the final
slot creation, and Euler's one was to use a restart_lsn as the

recovery_target_lsn.

I don't think it is correct to set restart_lsn as consistent_lsn point
because the same is used to set replication origin progress. Later
when we start the subscriber, the system will use that LSN as a
start_decoding_at point which is the point after which all the commits
will be replicated. So, we will end up incorrectly using restart_lsn
(LSN from where we start reading the WAL) as start_decoding_at point.
How could that be correct?

I didn't say we could use restart_lsn as consistent point of logical replication,
but I could agree the approach has issues.

Now, even if we use restart_lsn as recovery_target_lsn and the LSN
returned by pg_create_logical_replication_slot() as consistent LSN to
set replication progress, that also could lead to data loss because
the subscriber may never get data between restart_lsn value and
consistent LSN value.

You considered the case, e.g., tuples were inserted just after the restart_lsn
but before the RUNNING_XACT record? In this case, yes, the streaming replication
finishes before replicating tuples but logical replication will skip them.
Euler's approach cannot be used as-is.

Best regards,
Hayato Kuroda
FUJITSU LIMITED

#313Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#312)
Re: speed up a logical replica setup

On Wed, Jul 17, 2024 at 5:28 PM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Your analysis sounds correct to me.

Okay, so we could have a same picture...

IIUC, the root cause is that pg_create_logical_replication_slot() returns a LSN
which is not generated yet. So, I think both mine [1] and Euler's approach [2]
can solve the issue. My proposal was to add an extra WAL record after the final
slot creation, and Euler's one was to use a restart_lsn as the

recovery_target_lsn.

I don't think it is correct to set restart_lsn as consistent_lsn point
because the same is used to set replication origin progress. Later
when we start the subscriber, the system will use that LSN as a
start_decoding_at point which is the point after which all the commits
will be replicated. So, we will end up incorrectly using restart_lsn
(LSN from where we start reading the WAL) as start_decoding_at point.
How could that be correct?

I didn't say we could use restart_lsn as consistent point of logical replication,
but I could agree the approach has issues.

Now, even if we use restart_lsn as recovery_target_lsn and the LSN
returned by pg_create_logical_replication_slot() as consistent LSN to
set replication progress, that also could lead to data loss because
the subscriber may never get data between restart_lsn value and
consistent LSN value.

You considered the case, e.g., tuples were inserted just after the restart_lsn
but before the RUNNING_XACT record?

I am thinking of transactions between restart_lsn and "consistent
point lsn" (aka the point after which all commits will be replicated).
You conclusion seems correct to me that such transactions won't be
replicated by streaming replication and would be skipped by logical
replication. Now, if we can avoid that anyway, we can consider that.

--
With Regards,
Amit Kapila.

#314Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#283)
040_pg_createsubscriber.pl is slow and unstable (was Re: speed up a logical replica setup)

On Sun, Jun 30, 2024 at 2:40 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

In hopes of moving things along as we approach the v18 branch,
I went ahead and pushed Kuroda-san's patches (with a bit of
further editorialization). AFAICS that allows closing out
the concerns raised by Noah, so I've marked that open item
done. However, I added a new open item about how the
040_pg_createsubscriber.pl test is slow and still unstable.

This open item is now the only open item. It seems to have been open
for a month with no response from Peter which is, from my point of
view, far from ideal. However, another thing that is not ideal is that
we've been using the same thread to discuss every issue related to
this patch for 2.5 years. The thread spans hundreds of messages and it
is by no means obvious to what extent the messages posted after this
one addressed the underlying concern. Perhaps it would have been an
idea to start new threads when we started discussing post-commit
issues, instead of just tagging onto the same one.

But that said, I see no commits in the commit history which purport to
improve performance, so I guess the performance is probably still not
what you want, though I am not clear on the details. And as far as
stability is concerned, I peered through the dense haze of
027_stream_regress-related buildfarm failures for long enough to
discover that the stability issues with 040_pg_createsubscriber aren't
fixed either, because we have these recent buildfarm reports:

https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=olingo&amp;dt=2024-07-26%2016%3A02%3A40
https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=adder&amp;dt=2024-07-26%2009%3A20%3A15
https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=canebrake&amp;dt=2024-07-25%2002%3A39%3A02
https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=tamandua&amp;dt=2024-07-22%2002%3A31%3A32

So, Peter, as the committer responsible for pg_createsubscriber, what
are we going to do about this?

--
Robert Haas
EDB: http://www.enterprisedb.com

#315Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#314)
Re: 040_pg_createsubscriber.pl is slow and unstable (was Re: speed up a logical replica setup)

Robert Haas <robertmhaas@gmail.com> writes:

On Sun, Jun 30, 2024 at 2:40 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

... However, I added a new open item about how the
040_pg_createsubscriber.pl test is slow and still unstable.

But that said, I see no commits in the commit history which purport to
improve performance, so I guess the performance is probably still not
what you want, though I am not clear on the details.

My concern is described at [1]/messages/by-id/2377319.1719766794@sss.pgh.pa.us:

I have a different but possibly-related complaint: why is
040_pg_createsubscriber.pl so miserably slow? On my machine it
runs for a bit over 19 seconds, which seems completely out of line
(for comparison, 010_pg_basebackup.pl takes 6 seconds, and the
other test scripts in this directory take much less). It looks
like most of the blame falls on this step:

[12:47:22.292](14.534s) ok 28 - run pg_createsubscriber on node S

AFAICS the amount of data being replicated is completely trivial,
so that it doesn't make any sense for this to take so long --- and
if it does, that suggests that this tool will be impossibly slow
for production use. But I suspect there is a logic flaw causing
this. Speculating wildly, perhaps that is related to the failure
Alexander spotted?

The followup discussion in that thread made it sound like there's
some fairly fundamental deficiency in how wait_for_end_recovery()
detects end-of-recovery. I'm not too conversant with the details
though, and it's possible that pg_createsubscriber is just falling
foul of a pre-existing infelicity.

If the problem can be correctly described as "pg_createsubscriber
takes 10 seconds or so to detect end-of-stream", then it's probably
only an annoyance for testing and not something that would be fatal
in the real world. I'm not quite sure if that's accurate, though.

regards, tom lane

[1]: /messages/by-id/2377319.1719766794@sss.pgh.pa.us

#316Euler Taveira
euler@eulerto.com
In reply to: Amit Kapila (#313)
Re: speed up a logical replica setup

On Wed, Jul 17, 2024, at 11:37 PM, Amit Kapila wrote:

I am thinking of transactions between restart_lsn and "consistent
point lsn" (aka the point after which all commits will be replicated).
You conclusion seems correct to me that such transactions won't be
replicated by streaming replication and would be skipped by logical
replication. Now, if we can avoid that anyway, we can consider that.

Under reflection what I proposed [1]/messages/by-id/b1f0f8c7-8f01-4950-af77-339df3dc4684@app.fastmail.com seems more complex and possibly
error prone than other available solutions. The recovery step was slow
if the server is idle (that's the case for the test). An idle server
usually doesn't have another WAL record after creating the replication
slots. Since pg_createsubscriber is using the LSN returned by the last
replication slot as recovery_target_lsn, this LSN is ahead of the
current WAL position and the recovery waits until something writes a
WAL record to reach the target and ends the recovery.

Hayato already mentioned one of the solution in a previous email [2]/messages/by-id/OSBPR01MB25521B15BF950D2523BBE143F5D32@OSBPR01MB2552.jpnprd01.prod.outlook.com.
AFAICS we can use any solution that creates a WAL record. I tested the
following options:

\timing
select * from pg_create_logical_replication_slot('pg_createsubscriber', 'pgoutput', true);
select pg_logical_emit_message(false, 'pg_createsubscriber', 'dummy');
select pg_log_standby_snapshot();
select pg_create_restore_point('pg_createsubscriber');

that results in the following output:

slot_name | lsn
---------------------+-----------
pg_createsubscriber | 0/942DD28
(1 row)

Time: 200.571 ms
pg_logical_emit_message
-------------------------
0/942DD78
(1 row)

Time: 0.938 ms
pg_log_standby_snapshot
-------------------------
0/942DDB0
(1 row)

Time: 0.741 ms
pg_create_restore_point
-------------------------
0/942DE18
(1 row)

Time: 0.870 ms

and generates the following WAL records:

rmgr: Standby len (rec/tot): 50/ 50, tx: 0, lsn: 0/0942DCF0, prev 0/0942DCB8, desc: RUNNING_XACTS nextXid 3939 latestCompletedXid 3938 oldestRunningXid 3939
rmgr: LogicalMessage len (rec/tot): 75/ 75, tx: 0, lsn: 0/0942DD28, prev 0/0942DCF0, desc: MESSAGE non-transactional, prefix "pg_createsubscriber"; payload (5 bytes): 64 75 6D 6D 79
rmgr: Standby len (rec/tot): 50/ 50, tx: 0, lsn: 0/0942DD78, prev 0/0942DD28, desc: RUNNING_XACTS nextXid 3939 latestCompletedXid 3938 oldestRunningXid 3939
rmgr: XLOG len (rec/tot): 98/ 98, tx: 0, lsn: 0/0942DDB0, prev 0/0942DD78, desc: RESTORE_POINT pg_createsubscriber

The options are:

(a) temporary replication slot: requires an additional replication slot.
small payload. it is extremely slow in comparison with the other
options.
(b) logical message: can be consumed by logical replication when/if it
is supported some day. big payload. fast.
(c) snapshot of running txn: small payload. fast.
(d) named restore point: biggest payload. fast.

I don't have a strong preference but if I need to pick one I would
choose option (c) or option (d). The option (a) is out of the question.

Opinions?

[1]: /messages/by-id/b1f0f8c7-8f01-4950-af77-339df3dc4684@app.fastmail.com
[2]: /messages/by-id/OSBPR01MB25521B15BF950D2523BBE143F5D32@OSBPR01MB2552.jpnprd01.prod.outlook.com

--
Euler Taveira
EDB https://www.enterprisedb.com/

#317Euler Taveira
euler@eulerto.com
In reply to: Euler Taveira (#316)
2 attachment(s)
Re: speed up a logical replica setup

On Mon, Jul 29, 2024, at 6:11 PM, Euler Taveira wrote:

The options are:

(a) temporary replication slot: requires an additional replication slot.
small payload. it is extremely slow in comparison with the other
options.
(b) logical message: can be consumed by logical replication when/if it
is supported some day. big payload. fast.
(c) snapshot of running txn: small payload. fast.
(d) named restore point: biggest payload. fast.

I don't have a strong preference but if I need to pick one I would
choose option (c) or option (d). The option (a) is out of the question.

I'm attaching a patch that implements option (c). While reading the code
I noticed that I left a comment that should be removed by commit
b9639138262. 0002 removes it.

--
Euler Taveira
EDB https://www.enterprisedb.com/

Attachments:

0001-pg_createsubscriber-fix-slow-recovery.patchtext/x-patch; name="=?UTF-8?Q?0001-pg=5Fcreatesubscriber-fix-slow-recovery.patch?="Download
From f4afe05fc7e73c5c23bcdeba4fc65a538c83b8ba Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Mon, 29 Jul 2024 19:44:16 -0300
Subject: [PATCH 1/2] pg_createsubscriber: fix slow recovery

If the primary server is idle when you are running pg_createsubscriber,
it used to take some time during recovery. The reason is that it was
using the LSN returned by pg_create_logical_replication_slot as
recovery_target_lsn. This LSN points to the next WAL record that might
not be available at WAL, hence, the recovery routine waits until some
activity writes a WAL record to end the recovery. Inject a new WAL
record after the last replication slot to avoid slowness.

Discussion: https://www.postgresql.org/message-id/2377319.1719766794%40sss.pgh.pa.us
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 23 +++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index b02318782a6..00976c643a1 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -778,6 +778,29 @@ setup_publisher(struct LogicalRepInfo *dbinfo)
 		else
 			exit(1);
 
+		/*
+		 * An idle server might not write a new WAL record until the recovery
+		 * is about to end. Since pg_createsubscriber is using the LSN
+		 * returned by the last replication slot as recovery_target_lsn, this
+		 * LSN is ahead of the current WAL position and the recovery waits
+		 * until something writes a WAL record to reach the target and ends
+		 * the recovery. To avoid the recovery slowness in this case, injects
+		 * a new WAL record here.
+		 */
+		if (i == num_dbs - 1 && !dry_run)
+		{
+			PGresult   *res;
+
+			res = PQexec(conn, "SELECT pg_log_standby_snapshot()");
+			if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			{
+				pg_log_error("could not write an additional WAL record: %s",
+							 PQresultErrorMessage(res));
+				disconnect_database(conn, true);
+			}
+			PQclear(res);
+		}
+
 		disconnect_database(conn, false);
 	}
 
-- 
2.30.2

0002-pg_createsubscriber-remove-obsolete-comment.patchtext/x-patch; name="=?UTF-8?Q?0002-pg=5Fcreatesubscriber-remove-obsolete-comment.patch?="Download
From 75558e8379abae3a642583f31b21e0ca5db80d2b Mon Sep 17 00:00:00 2001
From: Euler Taveira <euler@eulerto.com>
Date: Mon, 29 Jul 2024 20:59:32 -0300
Subject: [PATCH 2/2] pg_createsubscriber: remove obsolete comment

This comment should have been removed by commit b9639138262. There is no
replication slot check on primary anymore.
---
 src/bin/pg_basebackup/pg_createsubscriber.c | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 00976c643a1..87668640f78 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -2209,10 +2209,7 @@ main(int argc, char **argv)
 	stop_standby_server(subscriber_dir);
 
 	/*
-	 * Create the required objects for each database on publisher. This step
-	 * is here mainly because if we stop the standby we cannot verify if the
-	 * primary slot is in use. We could use an extra connection for it but it
-	 * doesn't seem worth.
+	 * Create the required objects for each database on publisher.
 	 */
 	consistent_lsn = setup_publisher(dbinfo);
 
-- 
2.30.2

#318Amit Kapila
amit.kapila16@gmail.com
In reply to: Tom Lane (#315)
Re: 040_pg_createsubscriber.pl is slow and unstable (was Re: speed up a logical replica setup)

On Tue, Jul 30, 2024 at 1:48 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

On Sun, Jun 30, 2024 at 2:40 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

... However, I added a new open item about how the
040_pg_createsubscriber.pl test is slow and still unstable.

But that said, I see no commits in the commit history which purport to
improve performance, so I guess the performance is probably still not
what you want, though I am not clear on the details.

My concern is described at [1]:

I have a different but possibly-related complaint: why is
040_pg_createsubscriber.pl so miserably slow? On my machine it
runs for a bit over 19 seconds, which seems completely out of line
(for comparison, 010_pg_basebackup.pl takes 6 seconds, and the
other test scripts in this directory take much less). It looks
like most of the blame falls on this step:

[12:47:22.292](14.534s) ok 28 - run pg_createsubscriber on node S

AFAICS the amount of data being replicated is completely trivial,
so that it doesn't make any sense for this to take so long --- and
if it does, that suggests that this tool will be impossibly slow
for production use. But I suspect there is a logic flaw causing
this. Speculating wildly, perhaps that is related to the failure
Alexander spotted?

The followup discussion in that thread made it sound like there's
some fairly fundamental deficiency in how wait_for_end_recovery()
detects end-of-recovery. I'm not too conversant with the details
though, and it's possible that pg_createsubscriber is just falling
foul of a pre-existing infelicity.

If the problem can be correctly described as "pg_createsubscriber
takes 10 seconds or so to detect end-of-stream",

The problem can be defined as: "pg_createsubscriber waits for an
additional (new) WAL record to be generated on primary before it
considers the standby is ready for becoming a subscriber". Now, on
busy systems, this shouldn't be a problem but for idle systems, the
time to detect end-of-stream can't be easily defined.

One of the proposed solutions is that pg_createsubscriber generate a
dummy WAL record on the publisher/primary by using something like
pg_logical_emit_message(), pg_log_standby_snapshot(), etc. This will
fix the problem (BF failures and slow detection for end-of-stream) but
sounds more like a hack. The other ideas that we can consider as
mentioned in [1]/messages/by-id/CAA4eK1+p+7Ag6nqdFRdqowK1EmJ6bG-MtZQ_54dnFBi=_OO5RQ@mail.gmail.com require API/design change which is not preferable at
this point. So, the only way seems to be to accept the generation of
dummy WAL records to bring predictability in the tests or otherwise in
the usage of the tool.

[1]: /messages/by-id/CAA4eK1+p+7Ag6nqdFRdqowK1EmJ6bG-MtZQ_54dnFBi=_OO5RQ@mail.gmail.com

--
With Regards,
Amit Kapila.

#319Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#316)
RE: speed up a logical replica setup

Dear Euler,

Hayato already mentioned one of the solution in a previous email [2].
AFAICS we can use any solution that creates a WAL record. I tested the
following options:

Yes, my point was that we should add an arbitrary record after the recovery_target_lsn.

(a) temporary replication slot: requires an additional replication slot.
small payload. it is extremely slow in comparison with the other
options.
(b) logical message: can be consumed by logical replication when/if it
is supported some day. big payload. fast.
(c) snapshot of running txn: small payload. fast.
(d) named restore point: biggest payload. fast.

Another PoV is whether they trigger the flush of the generated WAL record. You've
tested pg_logical_emit_message() with flush=false, but this meant that the WAL does
not flush so that the wait_for_end_recovery() waits a time. IIUC, it may wait 15
seconds, which is the time duration bgwriter works. If flush is set to true, the
execution time will be longer.
pg_create_restore_point() also does not flush the generated record so that it may
be problematic. Moreover, the name of the restore point might be a conflict that
users will use.

Overall, I could agree to use pg_log_standby_snapshot(). This flushes the WAL
asynchronously but ensures the timing is not so delayed.

Best regards,
Hayato Kuroda
FUJITSU LIMITED

#320Tom Lane
tgl@sss.pgh.pa.us
In reply to: Amit Kapila (#318)
Re: 040_pg_createsubscriber.pl is slow and unstable (was Re: speed up a logical replica setup)

Amit Kapila <amit.kapila16@gmail.com> writes:

On Tue, Jul 30, 2024 at 1:48 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

If the problem can be correctly described as "pg_createsubscriber
takes 10 seconds or so to detect end-of-stream",

The problem can be defined as: "pg_createsubscriber waits for an
additional (new) WAL record to be generated on primary before it
considers the standby is ready for becoming a subscriber". Now, on
busy systems, this shouldn't be a problem but for idle systems, the
time to detect end-of-stream can't be easily defined.

Got it. IMO, that absolutely will be a problem for real users,
not only test cases.

One of the proposed solutions is that pg_createsubscriber generate a
dummy WAL record on the publisher/primary by using something like
pg_logical_emit_message(), pg_log_standby_snapshot(), etc. This will
fix the problem (BF failures and slow detection for end-of-stream) but
sounds more like a hack.

It's undoubtedly a hack, but I like it anyway because it's small,
self-contained, and easily removable once we have a better solution.
As you say, it seems a bit late in the v17 cycle to be designing
anything more invasive.

regards, tom lane

#321Amit Kapila
amit.kapila16@gmail.com
In reply to: Tom Lane (#320)
Re: 040_pg_createsubscriber.pl is slow and unstable (was Re: speed up a logical replica setup)

On Tue, Jul 30, 2024 at 9:56 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Amit Kapila <amit.kapila16@gmail.com> writes:

On Tue, Jul 30, 2024 at 1:48 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

If the problem can be correctly described as "pg_createsubscriber
takes 10 seconds or so to detect end-of-stream",

The problem can be defined as: "pg_createsubscriber waits for an
additional (new) WAL record to be generated on primary before it
considers the standby is ready for becoming a subscriber". Now, on
busy systems, this shouldn't be a problem but for idle systems, the
time to detect end-of-stream can't be easily defined.

Got it. IMO, that absolutely will be a problem for real users,
not only test cases.

One of the proposed solutions is that pg_createsubscriber generate a
dummy WAL record on the publisher/primary by using something like
pg_logical_emit_message(), pg_log_standby_snapshot(), etc. This will
fix the problem (BF failures and slow detection for end-of-stream) but
sounds more like a hack.

It's undoubtedly a hack, but I like it anyway because it's small,
self-contained, and easily removable once we have a better solution.
As you say, it seems a bit late in the v17 cycle to be designing
anything more invasive.

Thanks for your feedback. We will proceed in that direction and try to
close this open item.

--
With Regards,
Amit Kapila.

#322Amit Kapila
amit.kapila16@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#319)
Re: speed up a logical replica setup

On Tue, Jul 30, 2024 at 9:26 AM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:

Hayato already mentioned one of the solution in a previous email [2].
AFAICS we can use any solution that creates a WAL record. I tested the
following options:

Yes, my point was that we should add an arbitrary record after the recovery_target_lsn.

(a) temporary replication slot: requires an additional replication slot.
small payload. it is extremely slow in comparison with the other
options.
(b) logical message: can be consumed by logical replication when/if it
is supported some day. big payload. fast.
(c) snapshot of running txn: small payload. fast.
(d) named restore point: biggest payload. fast.

Another PoV is whether they trigger the flush of the generated WAL record. You've
tested pg_logical_emit_message() with flush=false, but this meant that the WAL does
not flush so that the wait_for_end_recovery() waits a time. IIUC, it may wait 15
seconds, which is the time duration bgwriter works. If flush is set to true, the
execution time will be longer.
pg_create_restore_point() also does not flush the generated record so that it may
be problematic. Moreover, the name of the restore point might be a conflict that
users will use.

Overall, I could agree to use pg_log_standby_snapshot(). This flushes the WAL
asynchronously but ensures the timing is not so delayed.

The other minor benefit of using pg_log_standby_snapshot() is that
after the standby is converted to subscriber, the publisher will
process this record to see if the slot machinery can be advanced. So,
overall there won't be any harm in using it. I'll check the latest
patch Euler shared.

--
With Regards,
Amit Kapila.

#323Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Amit Kapila (#318)
Re: 040_pg_createsubscriber.pl is slow and unstable (was Re: speed up a logical replica setup)

On Tue, Jul 30, 2024 at 9:25 AM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Tue, Jul 30, 2024 at 1:48 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

On Sun, Jun 30, 2024 at 2:40 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

... However, I added a new open item about how the
040_pg_createsubscriber.pl test is slow and still unstable.

But that said, I see no commits in the commit history which purport to
improve performance, so I guess the performance is probably still not
what you want, though I am not clear on the details.

My concern is described at [1]:

I have a different but possibly-related complaint: why is
040_pg_createsubscriber.pl so miserably slow? On my machine it
runs for a bit over 19 seconds, which seems completely out of line
(for comparison, 010_pg_basebackup.pl takes 6 seconds, and the
other test scripts in this directory take much less). It looks
like most of the blame falls on this step:

[12:47:22.292](14.534s) ok 28 - run pg_createsubscriber on node S

AFAICS the amount of data being replicated is completely trivial,
so that it doesn't make any sense for this to take so long --- and
if it does, that suggests that this tool will be impossibly slow
for production use. But I suspect there is a logic flaw causing
this. Speculating wildly, perhaps that is related to the failure
Alexander spotted?

The followup discussion in that thread made it sound like there's
some fairly fundamental deficiency in how wait_for_end_recovery()
detects end-of-recovery. I'm not too conversant with the details
though, and it's possible that pg_createsubscriber is just falling
foul of a pre-existing infelicity.

If the problem can be correctly described as "pg_createsubscriber
takes 10 seconds or so to detect end-of-stream",

The problem can be defined as: "pg_createsubscriber waits for an
additional (new) WAL record to be generated on primary before it
considers the standby is ready for becoming a subscriber". Now, on
busy systems, this shouldn't be a problem but for idle systems, the
time to detect end-of-stream can't be easily defined.

AFAIU, the server will emit running transactions WAL record at least
15 seconds. So the subscriber should not have to wait longer than 15
seconds. I understand that it would be a problem for tests, but will
it be a problem for end users? Sorry for repetition, if this has been
discussed.

--
Best Wishes,
Ashutosh Bapat

#324Amit Kapila
amit.kapila16@gmail.com
In reply to: Ashutosh Bapat (#323)
Re: 040_pg_createsubscriber.pl is slow and unstable (was Re: speed up a logical replica setup)

On Tue, Jul 30, 2024 at 11:28 AM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:

On Tue, Jul 30, 2024 at 9:25 AM Amit Kapila <amit.kapila16@gmail.com> wrote:

On Tue, Jul 30, 2024 at 1:48 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

On Sun, Jun 30, 2024 at 2:40 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

... However, I added a new open item about how the
040_pg_createsubscriber.pl test is slow and still unstable.

But that said, I see no commits in the commit history which purport to
improve performance, so I guess the performance is probably still not
what you want, though I am not clear on the details.

My concern is described at [1]:

I have a different but possibly-related complaint: why is
040_pg_createsubscriber.pl so miserably slow? On my machine it
runs for a bit over 19 seconds, which seems completely out of line
(for comparison, 010_pg_basebackup.pl takes 6 seconds, and the
other test scripts in this directory take much less). It looks
like most of the blame falls on this step:

[12:47:22.292](14.534s) ok 28 - run pg_createsubscriber on node S

AFAICS the amount of data being replicated is completely trivial,
so that it doesn't make any sense for this to take so long --- and
if it does, that suggests that this tool will be impossibly slow
for production use. But I suspect there is a logic flaw causing
this. Speculating wildly, perhaps that is related to the failure
Alexander spotted?

The followup discussion in that thread made it sound like there's
some fairly fundamental deficiency in how wait_for_end_recovery()
detects end-of-recovery. I'm not too conversant with the details
though, and it's possible that pg_createsubscriber is just falling
foul of a pre-existing infelicity.

If the problem can be correctly described as "pg_createsubscriber
takes 10 seconds or so to detect end-of-stream",

The problem can be defined as: "pg_createsubscriber waits for an
additional (new) WAL record to be generated on primary before it
considers the standby is ready for becoming a subscriber". Now, on
busy systems, this shouldn't be a problem but for idle systems, the
time to detect end-of-stream can't be easily defined.

AFAIU, the server will emit running transactions WAL record at least
15 seconds.

AFAICU, this is not true because the code suggests that the running
xacts record is inserted by bgwriter only when enough time has passed
and interesting records have been inserted since the last snapshot.
Please take a look at the following comment and code in bgwriter.c
"Only log if enough time has passed and interesting records have been
inserted since the last snapshot...".

--
With Regards,
Amit Kapila.

#325Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Euler Taveira (#317)
RE: speed up a logical replica setup

Dear Euler,

I applied your patch and confirmed it could fix the issue [1]/messages/by-id/68de6498-0449-a113-dd03-e198dded0bac@gmail.com.

METHOD
=======
1. shortened the snapshot interval to 3ms, and
```
-#define LOG_SNAPSHOT_INTERVAL_MS 15000
+#define LOG_SNAPSHOT_INTERVAL_MS 3
```
2. ran test 040_pg_createsubscriber.pl many times.
Before patching, I could reproduce the failure when I ran less than 1hr.
After patching, I ran more than 1hr but I could not reproduce.

Since this is a timing issue which I could not surely reproduce, I wanted others
to test it as well.

[1]: /messages/by-id/68de6498-0449-a113-dd03-e198dded0bac@gmail.com

Best regards,
Hayato Kuroda
FUJITSU LIMITED

#326Peter Eisentraut
peter@eisentraut.org
In reply to: Euler Taveira (#317)
Re: speed up a logical replica setup

On 30.07.24 02:05, Euler Taveira wrote:

I'm attaching a patch that implements option (c). While reading the code
I noticed that I left a comment that should be removed by commit
b9639138262. 0002 removes it.

I have committed your 0002 patch. It looks like Amit has already
committed your 0001 patch.

#327Euler Taveira
euler@eulerto.com
In reply to: Peter Eisentraut (#326)
Re: speed up a logical replica setup

On Tue, Jul 30, 2024, at 7:35 AM, Peter Eisentraut wrote:

On 30.07.24 02:05, Euler Taveira wrote:

I'm attaching a patch that implements option (c). While reading the code
I noticed that I left a comment that should be removed by commit
b9639138262. 0002 removes it.

I have committed your 0002 patch. It looks like Amit has already
committed your 0001 patch.

Peter / Amit, thanks for the quick review and commit.

--
Euler Taveira
EDB https://www.enterprisedb.com/