Resetting recovery target parameters in pg_createsubscriber

Started by D Laaren6 months ago43 messages
#1D Laaren
dlaaren8@gmail.com
2 attachment(s)

Hi Hackers,

I noticed that pg_createsubscriber sets recovery target params for
correct recovery before converting a physical replica to a logical
one but does not reset them afterward. It can lead to recovery
failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system might incorrectly determine that the
recovery target was never reached because these parameters remain
active.

I’ve attached a TAP test to reproduce the issue.
The proposed patch ensures all recovery parameters are reset after
conversion to prevent such edge cases.

I would appreciate any feedback.
--
Regards,
Alyona Vinter

Attachments:

v1-0001-TAP-test-recovery_fails_after_pg_createsubscriber.plapplication/x-perl; name=v1-0001-TAP-test-recovery_fails_after_pg_createsubscriber.plDownload
v1-0002-reset-recovery-target-params-pg-createsubscriber.patchtext/x-patch; charset=US-ASCII; name=v1-0002-reset-recovery-target-params-pg-createsubscriber.patchDownload
diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 025b893a41e..16fa0f707f0 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -99,6 +99,8 @@ 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 reset_recovery_params(const struct LogicalRepInfo *dbinfo,
+								  const char *datadir);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  const char *slotname);
 static void drop_failover_replication_slots(struct LogicalRepInfo *dbinfo);
@@ -1276,6 +1278,54 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
 }

+/*
+ * Reset the earlier set recovery parameters.
+ */
+static void
+reset_recovery_params(const struct LogicalRepInfo *dbinfo, const char *datadir)
+{
+	PGconn	   *conn;
+	PGresult   *res;
+
+	const char *recovery_params[] = {
+		"recovery_target",
+		"recovery_target_timeline",
+		"recovery_target_inclusive",
+		"recovery_target_action",
+		"recovery_target_name",
+		"recovery_target_time",
+		"recovery_target_xid",
+		"recovery_target_lsn",
+		NULL	/* sentinel */
+	};
+
+	conn = connect_database(dbinfo[0].subconninfo, true);
+
+	for (int i = 0; recovery_params[i] != NULL; i++)
+	{
+		char sql[64];
+
+		/*
+		 * ALTER SYSTEM RESET does not support prepared statements so we use it
+		 * in the old way with sprintf
+		 */
+		snprintf(sql, sizeof(sql), "ALTER SYSTEM RESET %s", recovery_params[i]);
+
+		res = PQexec(conn, sql);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK) {
+			pg_log_error("Prepared execution failed: %s\n", PQerrorMessage(conn));
+			PQclear(res);
+			disconnect_database(conn, true);
+			exit(1);
+		}
+		PQclear(res);
+	}
+
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters were reset");
+}
+
 /*
  * Drop physical replication slot on primary if the standby was using it. After
  * the transformation, it has no use.
@@ -2445,6 +2495,9 @@ main(int argc, char **argv)
 	/* Remove failover replication slots if they exist on subscriber */
 	drop_failover_replication_slots(dbinfos.dbinfo);

+	/* Reset recovery parameters */
+	reset_recovery_params(dbinfos.dbinfo, subscriber_dir);
+
 	/* Stop the subscriber */
 	pg_log_info("stopping the subscriber");
 	stop_standby_server(subscriber_dir);
#2Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: D Laaren (#1)
RE: Resetting recovery target parameters in pg_createsubscriber

Dear Laaren,

I noticed that pg_createsubscriber sets recovery target params for
correct recovery before converting a physical replica to a logical
one but does not reset them afterward. It can lead to recovery
failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system might incorrectly determine that the
recovery target was never reached because these parameters remain
active.

Thanks for reporting.
I have known that parameters won't be overwritten, but I didn't recognize the
case that recovery fails.

I’ve attached a TAP test to reproduce the issue.
The proposed patch ensures all recovery parameters are reset after
conversion to prevent such edge cases.

WriteRecoveryConfig() has been used to setup the recovery parameters. Can we
follow the way to restore them?

Also, can we add a test to 040_pg_createsubscriber? IIUC it is enough to check
one of recovery parameter is reset after the conversion.

Best regards,
Hayato Kuroda
FUJITSU LIMITED

#3Michael Paquier
michael@paquier.xyz
In reply to: Hayato Kuroda (Fujitsu) (#2)
Re: Resetting recovery target parameters in pg_createsubscriber

On Mon, Sep 01, 2025 at 02:06:34AM +0000, Hayato Kuroda (Fujitsu) wrote:

WriteRecoveryConfig() has been used to setup the recovery parameters. Can we
follow the way to restore them?

Also, can we add a test to 040_pg_createsubscriber? IIUC it is enough to check
one of recovery parameter is reset after the conversion.

Yeah, we'd want some tests to check the behaviors and expectations in
this tool. This tool is complex enough that this is going to be
mandatory, and making a test cheaper is always nicer.

FWIW, I find the proposed patch a bit dangerous. It updates
pg_createsubscriber.c so as an ALTER SYSTEM is used to reset the
parameters, but the recovery parameters are updated via
WriteRecoveryConfig() which is the code path holding the knowledge
that postgresql.auto.conf is used to hold the recovery parameters.

I don't like much the fact this creates a duplication with
setup_recovery() for the list of parameters handled. All the recovery
parameters are forced to a hardcoded value, except
recovery_target_lsn. So perhaps it would be better to maintain in
pg_createsubscriber.c a list made of (GUC names, values), with the LSN
part handled as an exception for the value to assign.

GenerateRecoveryConfig() can work with a replication connection,
relying on ALTER SYSTEM would not be able to do that properly, so
perhaps we should just invent a new routine that resets a portion of
the file on disk because recovery_gen.c's code already assumes that it
has write access to a data folder to do its work?
--
Michael

#4Alyona Vinter
dlaaren8@gmail.com
In reply to: Michael Paquier (#3)
2 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

Dear Michael and Hayato,

Thank you both for your valuable feedback on the previous patch version.

I've reworked the patch based on your suggestions - the new version should
address the concerns about ALTER SYSTEM and follows the same patterns as
the 'setup_recovery' code.

I kept primary_conninfo as-is for now since I'm not totally sure if we need
to touch it

I look forward to your feedback! ;)

Best regards,
Alyona Vinter

Attachments:

v2-0001-Reseting-recovery-target-parameters-in-pg_createsubscriber.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Reseting-recovery-target-parameters-in-pg_createsubscriber.patchDownload
From 94ca21fd0b3e683c4b03b552a3c7baa7f376eed3 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 1/2] Reseting recovery target parameters in
 pg_createsubscriber

The utility sets recovery target params for correct recovery before
conversion a physical replica to a logical one but does not reset them
afterward. It may cause recovery failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system may incorrectly determine that the
recovery target was never reached because these parameters remain
active.

This change ensures all recovery parameters are properly reset after
conversion to prevent such edge cases.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 127 +++++++++++++-----
 .../t/040_pg_createsubscriber.pl              |  17 +++
 2 files changed, 113 insertions(+), 31 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1d0fe44b6d3..b34e840b254 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -99,6 +99,8 @@ 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 reset_recovery_params(const struct LogicalRepInfo *dbinfo,
+								  const char *datadir);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  const char *slotname);
 static void drop_failover_replication_slots(struct LogicalRepInfo *dbinfo);
@@ -161,6 +163,43 @@ enum WaitPMResult
 	POSTMASTER_STILL_STARTING
 };

+typedef struct RecoveryParam {
+	const char *name;
+	const char *value;
+} RecoveryParam;
+
+/*
+ * Recovery parameters to be configured during physical replication setup.
+ * Most parameters are initialized, except recovery_target_lsn which is set
+ * separately during setup_recovery().
+ */
+static const RecoveryParam recovery_params[] = {
+	{"recovery_target", "''"},
+	{"recovery_target_timeline", "'latest'"},
+	/*
+	 * Set recovery_target_inclusive = false to avoid reapplying the
+	 * transaction committed at 'lsn' after subscription is enabled. This is
+	 * because the provided 'lsn' is also used as the replication start point
+	 * for the subscription. So, the server can send the transaction committed
+	 * at that 'lsn' after replication is started which can lead to applying
+	 * the same transaction twice if we keep recovery_target_inclusive = true.
+	 */
+	{"recovery_target_inclusive", "false"},
+	{"recovery_target_action", "promote"},
+	{"recovery_target_name", "''"},
+	{"recovery_target_time", "''"},
+	{"recovery_target_xid", "''"},
+	{"recovery_target_lsn", NULL}, /* the value will be specified later in setup_recovery*/
+	{NULL, NULL}, /* sentinel */
+};
+
+/*
+ * Buffer to preserve the original recovery conf contents before modifying
+ * recovery parameters. This allows restoration of the original configuration
+ * after the logical replication process completes, maintaining the system's
+ * previous recovery state.
+ */
+static PQExpBuffer recoveryconfcontents;

 /*
  * Cleanup objects that were created by pg_createsubscriber if there is an
@@ -1227,7 +1266,7 @@ static void
 setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const char *lsn)
 {
 	PGconn	   *conn;
-	PQExpBuffer recoveryconfcontents;
+	PQExpBuffer generatedrecoveryconfcontents;

 	/*
 	 * Despite of the recovery parameters will be written to the subscriber,
@@ -1236,6 +1275,9 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo, true);

+	/* Before setting up the recovery parameters save the original content. */
+	recoveryconfcontents = GetRecoveryConfig(conn, datadir);
+
 	/*
 	 * Write recovery parameters.
 	 *
@@ -1246,43 +1288,63 @@ 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, NULL);
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_timeline = 'latest'\n");
+	generatedrecoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);

-	/*
-	 * Set recovery_target_inclusive = false to avoid reapplying the
-	 * transaction committed at 'lsn' after subscription is enabled. This is
-	 * because the provided 'lsn' is also used as the replication start point
-	 * for the subscription. So, the server can send the transaction committed
-	 * at that 'lsn' after replication is started which can lead to applying
-	 * the same transaction twice if we keep recovery_target_inclusive = true.
-	 */
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_inclusive = false\n");
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_action = promote\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_name = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_time = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_xid = ''\n");
+	for (int i = 0; recovery_params[i].name != NULL; i++)
+	{
+		if (strcmp(recovery_params[i].name, "recovery_target_lsn") == 0)
+		{
+			const char *lsn_str;
+
+			if (dry_run)
+				lsn_str = psprintf("%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+			else
+				lsn_str = lsn;
+
+			appendPQExpBuffer(generatedrecoveryconfcontents, "%s = '%s'\n",
+							  recovery_params[i].name, lsn_str);
+		}
+		else
+		{
+			appendPQExpBuffer(generatedrecoveryconfcontents, "%s = %s\n",
+							  recovery_params[i].name, recovery_params[i].value);
+		}
+	}
+
+	if (dry_run)
+		appendPQExpBufferStr(generatedrecoveryconfcontents, "# dry run mode");
+	else
+		WriteRecoveryConfig(conn, datadir, generatedrecoveryconfcontents);
+
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", generatedrecoveryconfcontents->data);
+}
+
+/*
+ * Reset the previously set recovery parameters.
+ */
+static void
+reset_recovery_params(const struct LogicalRepInfo *dbinfo, const char *datadir)
+{
+	PGconn	   *conn;
+	PQExpBuffer generatedrecoveryconfcontents;
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	generatedrecoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);
+
+	appendPQExpBuffer(recoveryconfcontents, "%s",
+					  generatedrecoveryconfcontents->data);

 	if (dry_run)
-	{
 		appendPQExpBufferStr(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);
-	}
+		ReplaceRecoveryConfig(conn, datadir, recoveryconfcontents);
+
 	disconnect_database(conn, false);

-	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+	pg_log_debug("recovery parameters were reset");
 }

 /*
@@ -2458,6 +2520,9 @@ main(int argc, char **argv)
 	pg_log_info("stopping the subscriber");
 	stop_standby_server(subscriber_dir);

+	/* Reset recovery parameters */
+	reset_recovery_params(dbinfo, subscriber_dir);
+
 	/* Change system identifier from subscriber */
 	modify_subscriber_sysid(&opt);

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 229fef5b3b5..69fb6a3dbef 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -41,6 +41,16 @@ sub generate_db
 	return $dbname;
 }

+sub test_param_absent {
+    my ($node, $param) = @_;
+    my $auto_conf = $node->data_dir . '/postgresql.auto.conf';
+
+    return 1 unless -e $auto_conf;
+
+    my $content = slurp_file($auto_conf);
+    return $content !~ /^\s*$param\s*=/m;
+}
+
 #
 # Test mandatory options
 command_fails(['pg_createsubscriber'],
@@ -467,6 +477,13 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');

+# Verify that recovery parameters have been reset after pg_createsubscriber
+# We check recovery_target_lsn as a representative parameter - since all
+# recovery parameters are managed as a group, the absence of one indicates
+# that the entire set has been properly cleared from the configuration.
+ok(test_param_absent($node_s, 'recovery_target_lsn'),
+   'recovery_target_lsn parameter was removed');
+
 # Confirm the physical replication slot has been removed
 $result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
--
2.51.0

v2-0002-Implements-helper-function-in-recovery_gen.patchtext/x-patch; charset=US-ASCII; name=v2-0002-Implements-helper-function-in-recovery_gen.patchDownload
From baed4d6390737c4274cd4dc1c21d7bf488c673d4 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 2/2] Implements helper function in recovery_gen

These functions support pg_createsubscriber's need to temporarily
configure recovery params and ensure proper cleanup after the conversion
to logical replication is complete.
---
 src/fe_utils/recovery_gen.c         | 77 +++++++++++++++++++++++++++++
 src/include/fe_utils/recovery_gen.h |  3 ++
 2 files changed, 80 insertions(+)

diff --git a/src/fe_utils/recovery_gen.c b/src/fe_utils/recovery_gen.c
index e9023584768..e8e0dde9e00 100644
--- a/src/fe_utils/recovery_gen.c
+++ b/src/fe_utils/recovery_gen.c
@@ -10,6 +10,7 @@
 #include "postgres_fe.h"

 #include "common/logging.h"
+#include "common/file_utils.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/string_utils.h"

@@ -234,3 +235,79 @@ GetDbnameFromConnectionOptions(const char *connstr)
 	PQconninfoFree(conn_opts);
 	return dbname;
 }
+
+PQExpBuffer
+GetRecoveryConfig(PGconn *pgconn, const char *target_dir)
+{
+	PQExpBuffer contents;
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	char data[1024];
+	size_t bytes_read;
+
+	Assert(pgconn != NULL);
+
+	contents = createPQExpBuffer();
+	if (!contents)
+		pg_fatal("out of memory");
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(filename, "r");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", filename);
+
+	while ((bytes_read = fread(data, 1, sizeof(data), cf)) > 0)
+	{
+		data[bytes_read] = '\0';
+		appendPQExpBufferStr(contents, data);
+	}
+
+	if (ferror(cf))
+	{
+		pg_fatal("could not read from file \"%s\": %m", filename);
+	}
+
+	fclose(cf);
+
+	return contents;
+}
+
+void
+ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents)
+{
+	char		tmp_filename[MAXPGPATH];
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	Assert(pgconn != NULL);
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(tmp_filename, MAXPGPATH, "%s/%s.tmp", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(tmp_filename, "w");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", tmp_filename);
+
+	if (fwrite(contents->data, contents->len, 1, cf) != 1)
+		pg_fatal("could not write to file \"%s\": %m", tmp_filename);
+
+	fclose(cf);
+
+	if (durable_rename(tmp_filename, filename) != 0)
+		pg_fatal("could not rename file \"%s\" to \"%s\": %m",
+				 tmp_filename, filename);
+}
diff --git a/src/include/fe_utils/recovery_gen.h b/src/include/fe_utils/recovery_gen.h
index c13f2263bcd..18219af966b 100644
--- a/src/include/fe_utils/recovery_gen.h
+++ b/src/include/fe_utils/recovery_gen.h
@@ -27,4 +27,7 @@ extern void WriteRecoveryConfig(PGconn *pgconn, const char *target_dir,
 								PQExpBuffer contents);
 extern char *GetDbnameFromConnectionOptions(const char *connstr);

+extern PQExpBuffer GetRecoveryConfig(PGconn *pgconn, const char *target_dir);
+extern void ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents);
+
 #endif							/* RECOVERY_GEN_H */
--
2.51.0

#5Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Alyona Vinter (#4)
RE: Resetting recovery target parameters in pg_createsubscriber

Dear Alyona,

Thanks for updating the patch!
Sadly, your patch cannot be applied cleanly. Even after the manual merge, it could not
be built. Maybe `dbinfo` should be `dbinfos.dbinfo`. Obtained message is written in [1]``` ../postgres/src/bin/pg_basebackup/pg_createsubscriber.c: In function ‘main’: ../postgres/src/bin/pg_basebackup/pg_createsubscriber.c:2526:31: error: ‘dbinfo’ undeclared (first use in this function); did you mean ‘dbinfos’? 2526 | reset_recovery_params(dbinfo, subscriber_dir); | ^~~~~~ | dbinfos ```.
(cfbot seemed not to run correctly)

Regarding patch content, your patch restores the postgresql.auto.conf after the
command runs. Initially I felt that it is enough to set below GUCs becasue only
they are changed from the default. Is there a reason why you fully restore them?

```
recovery_target_inclusive true
recovery_target_action pause
recovery_target_lsn ""
```

[1]: ``` ../postgres/src/bin/pg_basebackup/pg_createsubscriber.c: In function ‘main’: ../postgres/src/bin/pg_basebackup/pg_createsubscriber.c:2526:31: error: ‘dbinfo’ undeclared (first use in this function); did you mean ‘dbinfos’? 2526 | reset_recovery_params(dbinfo, subscriber_dir); | ^~~~~~ | dbinfos ```
```
../postgres/src/bin/pg_basebackup/pg_createsubscriber.c: In function ‘main’:
../postgres/src/bin/pg_basebackup/pg_createsubscriber.c:2526:31: error: ‘dbinfo’ undeclared (first use in this function); did you mean ‘dbinfos’?
2526 | reset_recovery_params(dbinfo, subscriber_dir);
| ^~~~~~
| dbinfos
```

Best regards,
Hayato Kuroda
FUJITSU LIMITED

#6Alyona Vinter
dlaaren8@gmail.com
In reply to: Hayato Kuroda (Fujitsu) (#5)
2 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

Dear Hayato,

Thank you for the review! My apologies for the error in the patch -- it
looks like I accidentally modified it before sending =(. I've attached the
fixed versions below.

Regarding patch content, your patch restores the postgresql.auto.conf

after the

command runs. Initially I felt that it is enough to set below GUCs

becasue only

they are changed from the default. Is there a reason why you fully

restore them?

I just found it easier to restore the original state of
'postgresql.auto.conf', as removing parameters from the file resets them to
their default values. This approach achieves the same final state without
having to explicitly set each one.

Attachments:

v2-0002-Implements-helper-function-in-recovery_gen.patchtext/x-patch; charset=US-ASCII; name=v2-0002-Implements-helper-function-in-recovery_gen.patchDownload
From baed4d6390737c4274cd4dc1c21d7bf488c673d4 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 2/2] Implements helper function in recovery_gen

These functions support pg_createsubscriber's need to temporarily
configure recovery params and ensure proper cleanup after the conversion
to logical replication is complete.
---
 src/fe_utils/recovery_gen.c         | 77 +++++++++++++++++++++++++++++
 src/include/fe_utils/recovery_gen.h |  3 ++
 2 files changed, 80 insertions(+)

diff --git a/src/fe_utils/recovery_gen.c b/src/fe_utils/recovery_gen.c
index e9023584768..e8e0dde9e00 100644
--- a/src/fe_utils/recovery_gen.c
+++ b/src/fe_utils/recovery_gen.c
@@ -10,6 +10,7 @@
 #include "postgres_fe.h"

 #include "common/logging.h"
+#include "common/file_utils.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/string_utils.h"

@@ -234,3 +235,79 @@ GetDbnameFromConnectionOptions(const char *connstr)
 	PQconninfoFree(conn_opts);
 	return dbname;
 }
+
+PQExpBuffer
+GetRecoveryConfig(PGconn *pgconn, const char *target_dir)
+{
+	PQExpBuffer contents;
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	char data[1024];
+	size_t bytes_read;
+
+	Assert(pgconn != NULL);
+
+	contents = createPQExpBuffer();
+	if (!contents)
+		pg_fatal("out of memory");
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(filename, "r");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", filename);
+
+	while ((bytes_read = fread(data, 1, sizeof(data), cf)) > 0)
+	{
+		data[bytes_read] = '\0';
+		appendPQExpBufferStr(contents, data);
+	}
+
+	if (ferror(cf))
+	{
+		pg_fatal("could not read from file \"%s\": %m", filename);
+	}
+
+	fclose(cf);
+
+	return contents;
+}
+
+void
+ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents)
+{
+	char		tmp_filename[MAXPGPATH];
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	Assert(pgconn != NULL);
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(tmp_filename, MAXPGPATH, "%s/%s.tmp", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(tmp_filename, "w");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", tmp_filename);
+
+	if (fwrite(contents->data, contents->len, 1, cf) != 1)
+		pg_fatal("could not write to file \"%s\": %m", tmp_filename);
+
+	fclose(cf);
+
+	if (durable_rename(tmp_filename, filename) != 0)
+		pg_fatal("could not rename file \"%s\" to \"%s\": %m",
+				 tmp_filename, filename);
+}
diff --git a/src/include/fe_utils/recovery_gen.h b/src/include/fe_utils/recovery_gen.h
index c13f2263bcd..18219af966b 100644
--- a/src/include/fe_utils/recovery_gen.h
+++ b/src/include/fe_utils/recovery_gen.h
@@ -27,4 +27,7 @@ extern void WriteRecoveryConfig(PGconn *pgconn, const char *target_dir,
 								PQExpBuffer contents);
 extern char *GetDbnameFromConnectionOptions(const char *connstr);

+extern PQExpBuffer GetRecoveryConfig(PGconn *pgconn, const char *target_dir);
+extern void ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents);
+
 #endif							/* RECOVERY_GEN_H */
--
2.51.0

v2-0001-Reseting-recovery-target-parameters-in-pg_createsubscriber.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Reseting-recovery-target-parameters-in-pg_createsubscriber.patchDownload
From 94ca21fd0b3e683c4b03b552a3c7baa7f376eed3 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 15 Jul 2025 15:21:22 +0700
Subject: [PATCH 1/2] Reseting recovery target parameters in
 pg_createsubscriber

The utility sets recovery target params for correct recovery before
conversion a physical replica to a logical one but does not reset them
afterward. It may cause recovery failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system may incorrectly determine that the
recovery target was never reached because these parameters remain
active.

This change ensures all recovery parameters are properly reset after
conversion to prevent such edge cases.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 127 +++++++++++++-----
 .../t/040_pg_createsubscriber.pl              |  17 +++
 2 files changed, 113 insertions(+), 31 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1d0fe44b6d3..8ddb56c641e 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -99,6 +99,8 @@ 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 reset_recovery_params(const struct LogicalRepInfo *dbinfo,
+								  const char *datadir);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  const char *slotname);
 static void drop_failover_replication_slots(struct LogicalRepInfo *dbinfo);
@@ -161,6 +163,43 @@ enum WaitPMResult
 	POSTMASTER_STILL_STARTING
 };

+typedef struct RecoveryParam {
+	const char *name;
+	const char *value;
+} RecoveryParam;
+
+/*
+ * Recovery parameters to be configured during physical replication setup.
+ * Most parameters are initialized, except recovery_target_lsn which is set
+ * separately during setup_recovery().
+ */
+static const RecoveryParam recovery_params[] = {
+	{"recovery_target", "''"},
+	{"recovery_target_timeline", "'latest'"},
+	/*
+	 * Set recovery_target_inclusive = false to avoid reapplying the
+	 * transaction committed at 'lsn' after subscription is enabled. This is
+	 * because the provided 'lsn' is also used as the replication start point
+	 * for the subscription. So, the server can send the transaction committed
+	 * at that 'lsn' after replication is started which can lead to applying
+	 * the same transaction twice if we keep recovery_target_inclusive = true.
+	 */
+	{"recovery_target_inclusive", "false"},
+	{"recovery_target_action", "promote"},
+	{"recovery_target_name", "''"},
+	{"recovery_target_time", "''"},
+	{"recovery_target_xid", "''"},
+	{"recovery_target_lsn", NULL}, /* the value will be specified later in setup_recovery*/
+	{NULL, NULL}, /* sentinel */
+};
+
+/*
+ * Buffer to preserve the original recovery conf contents before modifying
+ * recovery parameters. This allows restoration of the original configuration
+ * after the logical replication process completes, maintaining the system's
+ * previous recovery state.
+ */
+static PQExpBuffer recoveryconfcontents;

 /*
  * Cleanup objects that were created by pg_createsubscriber if there is an
@@ -1227,7 +1266,7 @@ static void
 setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const char *lsn)
 {
 	PGconn	   *conn;
-	PQExpBuffer recoveryconfcontents;
+	PQExpBuffer generatedrecoveryconfcontents;

 	/*
 	 * Despite of the recovery parameters will be written to the subscriber,
@@ -1236,6 +1275,9 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo, true);

+	/* Before setting up the recovery parameters save the original content. */
+	recoveryconfcontents = GetRecoveryConfig(conn, datadir);
+
 	/*
 	 * Write recovery parameters.
 	 *
@@ -1246,43 +1288,63 @@ 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, NULL);
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_timeline = 'latest'\n");
+	generatedrecoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);

-	/*
-	 * Set recovery_target_inclusive = false to avoid reapplying the
-	 * transaction committed at 'lsn' after subscription is enabled. This is
-	 * because the provided 'lsn' is also used as the replication start point
-	 * for the subscription. So, the server can send the transaction committed
-	 * at that 'lsn' after replication is started which can lead to applying
-	 * the same transaction twice if we keep recovery_target_inclusive = true.
-	 */
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_inclusive = false\n");
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_action = promote\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_name = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_time = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_xid = ''\n");
+	for (int i = 0; recovery_params[i].name != NULL; i++)
+	{
+		if (strcmp(recovery_params[i].name, "recovery_target_lsn") == 0)
+		{
+			const char *lsn_str;
+
+			if (dry_run)
+				lsn_str = psprintf("%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+			else
+				lsn_str = lsn;
+
+			appendPQExpBuffer(generatedrecoveryconfcontents, "%s = '%s'\n",
+							  recovery_params[i].name, lsn_str);
+		}
+		else
+		{
+			appendPQExpBuffer(generatedrecoveryconfcontents, "%s = %s\n",
+							  recovery_params[i].name, recovery_params[i].value);
+		}
+	}
+
+	if (dry_run)
+		appendPQExpBufferStr(generatedrecoveryconfcontents, "# dry run mode");
+	else
+		WriteRecoveryConfig(conn, datadir, generatedrecoveryconfcontents);
+
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", generatedrecoveryconfcontents->data);
+}
+
+/*
+ * Reset the previously set recovery parameters.
+ */
+static void
+reset_recovery_params(const struct LogicalRepInfo *dbinfo, const char *datadir)
+{
+	PGconn	   *conn;
+	PQExpBuffer generatedrecoveryconfcontents;
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	generatedrecoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);
+
+	appendPQExpBuffer(recoveryconfcontents, "%s",
+					  generatedrecoveryconfcontents->data);

 	if (dry_run)
-	{
 		appendPQExpBufferStr(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);
-	}
+		ReplaceRecoveryConfig(conn, datadir, recoveryconfcontents);
+
 	disconnect_database(conn, false);

-	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+	pg_log_debug("recovery parameters were reset");
 }

 /*
@@ -2458,6 +2520,9 @@ main(int argc, char **argv)
 	pg_log_info("stopping the subscriber");
 	stop_standby_server(subscriber_dir);

+	/* Reset recovery parameters */
+	reset_recovery_params(dbinfos.dbinfo, subscriber_dir);
+
 	/* Change system identifier from subscriber */
 	modify_subscriber_sysid(&opt);

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 229fef5b3b5..69fb6a3dbef 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -41,6 +41,16 @@ sub generate_db
 	return $dbname;
 }

+sub test_param_absent {
+    my ($node, $param) = @_;
+    my $auto_conf = $node->data_dir . '/postgresql.auto.conf';
+
+    return 1 unless -e $auto_conf;
+
+    my $content = slurp_file($auto_conf);
+    return $content !~ /^\s*$param\s*=/m;
+}
+
 #
 # Test mandatory options
 command_fails(['pg_createsubscriber'],
@@ -467,6 +477,13 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');

+# Verify that recovery parameters have been reset after pg_createsubscriber
+# We check recovery_target_lsn as a representative parameter - since all
+# recovery parameters are managed as a group, the absence of one indicates
+# that the entire set has been properly cleared from the configuration.
+ok(test_param_absent($node_s, 'recovery_target_lsn'),
+   'recovery_target_lsn parameter was removed');
+
 # Confirm the physical replication slot has been removed
 $result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
--
2.51.0

#7Alyona Vinter
dlaaren8@gmail.com
In reply to: Alyona Vinter (#6)
2 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

Hi,

CFbot indicated some issues with the patch. I've attached rebased versions
of the patches, so hopefully everything will be ok this time.

Best regards,
Alyona Vinter

On Fri, 5 Sept 2025 at 12:51, Alyona Vinter <dlaaren8@gmail.com> wrote:

Show quoted text

Dear Hayato,

Thank you for the review! My apologies for the error in the patch -- it
looks like I accidentally modified it before sending =(. I've attached the
fixed versions below.

Regarding patch content, your patch restores the postgresql.auto.conf

after the

command runs. Initially I felt that it is enough to set below GUCs

becasue only

they are changed from the default. Is there a reason why you fully

restore them?

I just found it easier to restore the original state of
'postgresql.auto.conf', as removing parameters from the file resets them to
their default values. This approach achieves the same final state without
having to explicitly set each one.

Attachments:

v3-0001-Reseting-recovery-target-parameters-in-pg_createsubscriber.patchtext/x-patch; charset=US-ASCII; name=v3-0001-Reseting-recovery-target-parameters-in-pg_createsubscriber.patchDownload
From 94ca21fd0b3e683c4b03b552a3c7baa7f376eed3 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 1/2] Reseting recovery target parameters in
 pg_createsubscriber

The utility sets recovery target params for correct recovery before
conversion a physical replica to a logical one but does not reset them
afterward. It may cause recovery failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system may incorrectly determine that the
recovery target was never reached because these parameters remain
active.

This change ensures all recovery parameters are properly reset after
conversion to prevent such edge cases.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 127 +++++++++++++-----
 .../t/040_pg_createsubscriber.pl              |  17 +++
 2 files changed, 113 insertions(+), 31 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1d0fe44b6d3..b34e840b254 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -99,6 +99,8 @@ 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 reset_recovery_params(const struct LogicalRepInfo *dbinfo,
+								  const char *datadir);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  const char *slotname);
 static void drop_failover_replication_slots(struct LogicalRepInfo *dbinfo);
@@ -161,6 +163,43 @@ enum WaitPMResult
 	POSTMASTER_STILL_STARTING
 };

+typedef struct RecoveryParam {
+	const char *name;
+	const char *value;
+} RecoveryParam;
+
+/*
+ * Recovery parameters to be configured during physical replication setup.
+ * Most parameters are initialized, except recovery_target_lsn which is set
+ * separately during setup_recovery().
+ */
+static const RecoveryParam recovery_params[] = {
+	{"recovery_target", "''"},
+	{"recovery_target_timeline", "'latest'"},
+	/*
+	 * Set recovery_target_inclusive = false to avoid reapplying the
+	 * transaction committed at 'lsn' after subscription is enabled. This is
+	 * because the provided 'lsn' is also used as the replication start point
+	 * for the subscription. So, the server can send the transaction committed
+	 * at that 'lsn' after replication is started which can lead to applying
+	 * the same transaction twice if we keep recovery_target_inclusive = true.
+	 */
+	{"recovery_target_inclusive", "false"},
+	{"recovery_target_action", "promote"},
+	{"recovery_target_name", "''"},
+	{"recovery_target_time", "''"},
+	{"recovery_target_xid", "''"},
+	{"recovery_target_lsn", NULL}, /* the value will be specified later in setup_recovery*/
+	{NULL, NULL}, /* sentinel */
+};
+
+/*
+ * Buffer to preserve the original recovery conf contents before modifying
+ * recovery parameters. This allows restoration of the original configuration
+ * after the logical replication process completes, maintaining the system's
+ * previous recovery state.
+ */
+static PQExpBuffer recoveryconfcontents;

 /*
  * Cleanup objects that were created by pg_createsubscriber if there is an
@@ -1227,7 +1266,7 @@ static void
 setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const char *lsn)
 {
 	PGconn	   *conn;
-	PQExpBuffer recoveryconfcontents;
+	PQExpBuffer generatedrecoveryconfcontents;

 	/*
 	 * Despite of the recovery parameters will be written to the subscriber,
@@ -1236,6 +1275,9 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo, true);

+	/* Before setting up the recovery parameters save the original content. */
+	recoveryconfcontents = GetRecoveryConfig(conn, datadir);
+
 	/*
 	 * Write recovery parameters.
 	 *
@@ -1246,43 +1288,63 @@ 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, NULL);
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_timeline = 'latest'\n");
+	generatedrecoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);

-	/*
-	 * Set recovery_target_inclusive = false to avoid reapplying the
-	 * transaction committed at 'lsn' after subscription is enabled. This is
-	 * because the provided 'lsn' is also used as the replication start point
-	 * for the subscription. So, the server can send the transaction committed
-	 * at that 'lsn' after replication is started which can lead to applying
-	 * the same transaction twice if we keep recovery_target_inclusive = true.
-	 */
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_inclusive = false\n");
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_action = promote\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_name = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_time = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_xid = ''\n");
+	for (int i = 0; recovery_params[i].name != NULL; i++)
+	{
+		if (strcmp(recovery_params[i].name, "recovery_target_lsn") == 0)
+		{
+			const char *lsn_str;
+
+			if (dry_run)
+				lsn_str = psprintf("%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+			else
+				lsn_str = lsn;
+
+			appendPQExpBuffer(generatedrecoveryconfcontents, "%s = '%s'\n",
+							  recovery_params[i].name, lsn_str);
+		}
+		else
+		{
+			appendPQExpBuffer(generatedrecoveryconfcontents, "%s = %s\n",
+							  recovery_params[i].name, recovery_params[i].value);
+		}
+	}
+
+	if (dry_run)
+		appendPQExpBufferStr(generatedrecoveryconfcontents, "# dry run mode");
+	else
+		WriteRecoveryConfig(conn, datadir, generatedrecoveryconfcontents);
+
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", generatedrecoveryconfcontents->data);
+}
+
+/*
+ * Reset the previously set recovery parameters.
+ */
+static void
+reset_recovery_params(const struct LogicalRepInfo *dbinfo, const char *datadir)
+{
+	PGconn	   *conn;
+	PQExpBuffer generatedrecoveryconfcontents;
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	generatedrecoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);
+
+	appendPQExpBuffer(recoveryconfcontents, "%s",
+					  generatedrecoveryconfcontents->data);

 	if (dry_run)
-	{
 		appendPQExpBufferStr(recoveryconfcontents, "# dry run mode");
-		appendPQExpBuffer(recoveryconfcontents,
-						  "recovery_target_lsn = '%X/%08X'\n",
-						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
-	}
 	else
-	{
-		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
-						  lsn);
-		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
-	}
+		ReplaceRecoveryConfig(conn, datadir, recoveryconfcontents);
+
 	disconnect_database(conn, false);

-	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+	pg_log_debug("recovery parameters were reset");
 }

 /*
@@ -2458,6 +2520,9 @@ main(int argc, char **argv)
 	pg_log_info("stopping the subscriber");
 	stop_standby_server(subscriber_dir);

+	/* Reset recovery parameters */
+	reset_recovery_params(dbinfo, subscriber_dir);
+
 	/* Change system identifier from subscriber */
 	modify_subscriber_sysid(&opt);

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 229fef5b3b5..69fb6a3dbef 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -41,6 +41,16 @@ sub generate_db
 	return $dbname;
 }

+sub test_param_absent {
+    my ($node, $param) = @_;
+    my $auto_conf = $node->data_dir . '/postgresql.auto.conf';
+
+    return 1 unless -e $auto_conf;
+
+    my $content = slurp_file($auto_conf);
+    return $content !~ /^\s*$param\s*=/m;
+}
+
 #
 # Test mandatory options
 command_fails(['pg_createsubscriber'],
@@ -467,6 +477,13 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');

+# Verify that recovery parameters have been reset after pg_createsubscriber
+# We check recovery_target_lsn as a representative parameter - since all
+# recovery parameters are managed as a group, the absence of one indicates
+# that the entire set has been properly cleared from the configuration.
+ok(test_param_absent($node_s, 'recovery_target_lsn'),
+   'recovery_target_lsn parameter was removed');
+
 # Confirm the physical replication slot has been removed
 $result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
--
2.51.0

v3-0002-Implements-helper-function-in-recovery_gen.patchtext/x-patch; charset=US-ASCII; name=v3-0002-Implements-helper-function-in-recovery_gen.patchDownload
From baed4d6390737c4274cd4dc1c21d7bf488c673d4 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 2/2] Implements helper function in recovery_gen

These functions support pg_createsubscriber's need to temporarily
configure recovery params and ensure proper cleanup after the conversion
to logical replication is complete.
---
 src/fe_utils/recovery_gen.c         | 77 +++++++++++++++++++++++++++++
 src/include/fe_utils/recovery_gen.h |  3 ++
 2 files changed, 80 insertions(+)

diff --git a/src/fe_utils/recovery_gen.c b/src/fe_utils/recovery_gen.c
index e9023584768..e8e0dde9e00 100644
--- a/src/fe_utils/recovery_gen.c
+++ b/src/fe_utils/recovery_gen.c
@@ -10,6 +10,7 @@
 #include "postgres_fe.h"

 #include "common/logging.h"
+#include "common/file_utils.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/string_utils.h"

@@ -234,3 +235,79 @@ GetDbnameFromConnectionOptions(const char *connstr)
 	PQconninfoFree(conn_opts);
 	return dbname;
 }
+
+PQExpBuffer
+GetRecoveryConfig(PGconn *pgconn, const char *target_dir)
+{
+	PQExpBuffer contents;
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	char data[1024];
+	size_t bytes_read;
+
+	Assert(pgconn != NULL);
+
+	contents = createPQExpBuffer();
+	if (!contents)
+		pg_fatal("out of memory");
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(filename, "r");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", filename);
+
+	while ((bytes_read = fread(data, 1, sizeof(data), cf)) > 0)
+	{
+		data[bytes_read] = '\0';
+		appendPQExpBufferStr(contents, data);
+	}
+
+	if (ferror(cf))
+	{
+		pg_fatal("could not read from file \"%s\": %m", filename);
+	}
+
+	fclose(cf);
+
+	return contents;
+}
+
+void
+ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents)
+{
+	char		tmp_filename[MAXPGPATH];
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	Assert(pgconn != NULL);
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(tmp_filename, MAXPGPATH, "%s/%s.tmp", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(tmp_filename, "w");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", tmp_filename);
+
+	if (fwrite(contents->data, contents->len, 1, cf) != 1)
+		pg_fatal("could not write to file \"%s\": %m", tmp_filename);
+
+	fclose(cf);
+
+	if (durable_rename(tmp_filename, filename) != 0)
+		pg_fatal("could not rename file \"%s\" to \"%s\": %m",
+				 tmp_filename, filename);
+}
diff --git a/src/include/fe_utils/recovery_gen.h b/src/include/fe_utils/recovery_gen.h
index c13f2263bcd..18219af966b 100644
--- a/src/include/fe_utils/recovery_gen.h
+++ b/src/include/fe_utils/recovery_gen.h
@@ -27,4 +27,7 @@ extern void WriteRecoveryConfig(PGconn *pgconn, const char *target_dir,
 								PQExpBuffer contents);
 extern char *GetDbnameFromConnectionOptions(const char *connstr);

+extern PQExpBuffer GetRecoveryConfig(PGconn *pgconn, const char *target_dir);
+extern void ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents);
+
 #endif							/* RECOVERY_GEN_H */
--
2.51.0

#8Alyona Vinter
dlaaren8@gmail.com
In reply to: Alyona Vinter (#7)
2 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

Sorry, wrong patches again. Here are the correct ones.

Best regards,
Alyona Vinter

Attachments:

v3-0002-Implements-helper-function-in-recovery_gen.patchtext/x-patch; charset=US-ASCII; name=v3-0002-Implements-helper-function-in-recovery_gen.patchDownload
From baed4d6390737c4274cd4dc1c21d7bf488c673d4 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 2/2] Implements helper function in recovery_gen

These functions support pg_createsubscriber's need to temporarily
configure recovery params and ensure proper cleanup after the conversion
to logical replication is complete.
---
 src/fe_utils/recovery_gen.c         | 77 +++++++++++++++++++++++++++++
 src/include/fe_utils/recovery_gen.h |  3 ++
 2 files changed, 80 insertions(+)

diff --git a/src/fe_utils/recovery_gen.c b/src/fe_utils/recovery_gen.c
index e9023584768..e8e0dde9e00 100644
--- a/src/fe_utils/recovery_gen.c
+++ b/src/fe_utils/recovery_gen.c
@@ -10,6 +10,7 @@
 #include "postgres_fe.h"

 #include "common/logging.h"
+#include "common/file_utils.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/string_utils.h"

@@ -234,3 +235,79 @@ GetDbnameFromConnectionOptions(const char *connstr)
 	PQconninfoFree(conn_opts);
 	return dbname;
 }
+
+PQExpBuffer
+GetRecoveryConfig(PGconn *pgconn, const char *target_dir)
+{
+	PQExpBuffer contents;
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	char data[1024];
+	size_t bytes_read;
+
+	Assert(pgconn != NULL);
+
+	contents = createPQExpBuffer();
+	if (!contents)
+		pg_fatal("out of memory");
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(filename, "r");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", filename);
+
+	while ((bytes_read = fread(data, 1, sizeof(data), cf)) > 0)
+	{
+		data[bytes_read] = '\0';
+		appendPQExpBufferStr(contents, data);
+	}
+
+	if (ferror(cf))
+	{
+		pg_fatal("could not read from file \"%s\": %m", filename);
+	}
+
+	fclose(cf);
+
+	return contents;
+}
+
+void
+ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents)
+{
+	char		tmp_filename[MAXPGPATH];
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	Assert(pgconn != NULL);
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(tmp_filename, MAXPGPATH, "%s/%s.tmp", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(tmp_filename, "w");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", tmp_filename);
+
+	if (fwrite(contents->data, contents->len, 1, cf) != 1)
+		pg_fatal("could not write to file \"%s\": %m", tmp_filename);
+
+	fclose(cf);
+
+	if (durable_rename(tmp_filename, filename) != 0)
+		pg_fatal("could not rename file \"%s\" to \"%s\": %m",
+				 tmp_filename, filename);
+}
diff --git a/src/include/fe_utils/recovery_gen.h b/src/include/fe_utils/recovery_gen.h
index c13f2263bcd..18219af966b 100644
--- a/src/include/fe_utils/recovery_gen.h
+++ b/src/include/fe_utils/recovery_gen.h
@@ -27,4 +27,7 @@ extern void WriteRecoveryConfig(PGconn *pgconn, const char *target_dir,
 								PQExpBuffer contents);
 extern char *GetDbnameFromConnectionOptions(const char *connstr);

+extern PQExpBuffer GetRecoveryConfig(PGconn *pgconn, const char *target_dir);
+extern void ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents);
+
 #endif							/* RECOVERY_GEN_H */
--
2.51.0

v3-0001-Reseting-recovery-target-parameters-in-pg_createsubscriber.patchtext/x-patch; charset=US-ASCII; name=v3-0001-Reseting-recovery-target-parameters-in-pg_createsubscriber.patchDownload
From 94ca21fd0b3e683c4b03b552a3c7baa7f376eed3 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 1/2] Reseting recovery target parameters in
 pg_createsubscriber

The utility sets recovery target params for correct recovery before
conversion a physical replica to a logical one but does not reset them
afterward. It may cause recovery failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system may incorrectly determine that the
recovery target was never reached because these parameters remain
active.

This change ensures all recovery parameters are properly reset after
conversion to prevent such edge cases.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 127 +++++++++++++-----
 .../t/040_pg_createsubscriber.pl              |  17 +++
 2 files changed, 113 insertions(+), 31 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 1d0fe44b6d3..b34e840b254 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -99,6 +99,8 @@ 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 reset_recovery_params(const struct LogicalRepInfo *dbinfo,
+								  const char *datadir);
 static void drop_primary_replication_slot(struct LogicalRepInfo *dbinfo,
 										  const char *slotname);
 static void drop_failover_replication_slots(struct LogicalRepInfo *dbinfo);
@@ -161,6 +163,43 @@ enum WaitPMResult
 	POSTMASTER_STILL_STARTING
 };

+typedef struct RecoveryParam {
+	const char *name;
+	const char *value;
+} RecoveryParam;
+
+/*
+ * Recovery parameters to be configured during physical replication setup.
+ * Most parameters are initialized, except recovery_target_lsn which is set
+ * separately during setup_recovery().
+ */
+static const RecoveryParam recovery_params[] = {
+	{"recovery_target", "''"},
+	{"recovery_target_timeline", "'latest'"},
+	/*
+	 * Set recovery_target_inclusive = false to avoid reapplying the
+	 * transaction committed at 'lsn' after subscription is enabled. This is
+	 * because the provided 'lsn' is also used as the replication start point
+	 * for the subscription. So, the server can send the transaction committed
+	 * at that 'lsn' after replication is started which can lead to applying
+	 * the same transaction twice if we keep recovery_target_inclusive = true.
+	 */
+	{"recovery_target_inclusive", "false"},
+	{"recovery_target_action", "promote"},
+	{"recovery_target_name", "''"},
+	{"recovery_target_time", "''"},
+	{"recovery_target_xid", "''"},
+	{"recovery_target_lsn", NULL}, /* the value will be specified later in setup_recovery*/
+	{NULL, NULL}, /* sentinel */
+};
+
+/*
+ * Buffer to preserve the original recovery conf contents before modifying
+ * recovery parameters. This allows restoration of the original configuration
+ * after the logical replication process completes, maintaining the system's
+ * previous recovery state.
+ */
+static PQExpBuffer recoveryconfcontents;

 /*
  * Cleanup objects that were created by pg_createsubscriber if there is an
@@ -1227,7 +1266,7 @@ static void
 setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const char *lsn)
 {
 	PGconn	   *conn;
-	PQExpBuffer recoveryconfcontents;
+	PQExpBuffer generatedrecoveryconfcontents;

 	/*
 	 * Despite of the recovery parameters will be written to the subscriber,
@@ -1236,6 +1275,9 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo, true);

+	/* Before setting up the recovery parameters save the original content. */
+	recoveryconfcontents = GetRecoveryConfig(conn, datadir);
+
 	/*
 	 * Write recovery parameters.
 	 *
@@ -1246,43 +1288,63 @@ 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, NULL);
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_timeline = 'latest'\n");
+	generatedrecoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);

-	/*
-	 * Set recovery_target_inclusive = false to avoid reapplying the
-	 * transaction committed at 'lsn' after subscription is enabled. This is
-	 * because the provided 'lsn' is also used as the replication start point
-	 * for the subscription. So, the server can send the transaction committed
-	 * at that 'lsn' after replication is started which can lead to applying
-	 * the same transaction twice if we keep recovery_target_inclusive = true.
-	 */
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_inclusive = false\n");
-	appendPQExpBufferStr(recoveryconfcontents,
-						 "recovery_target_action = promote\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_name = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_time = ''\n");
-	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_xid = ''\n");
+	for (int i = 0; recovery_params[i].name != NULL; i++)
+	{
+		if (strcmp(recovery_params[i].name, "recovery_target_lsn") == 0)
+		{
+			const char *lsn_str;
+
+			if (dry_run)
+				lsn_str = psprintf("%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
+			else
+				lsn_str = lsn;
+
+			appendPQExpBuffer(generatedrecoveryconfcontents, "%s = '%s'\n",
+							  recovery_params[i].name, lsn_str);
+		}
+		else
+		{
+			appendPQExpBuffer(generatedrecoveryconfcontents, "%s = %s\n",
+							  recovery_params[i].name, recovery_params[i].value);
+		}
+	}
+
+	if (dry_run)
+		appendPQExpBufferStr(generatedrecoveryconfcontents, "# dry run mode");
+	else
+		WriteRecoveryConfig(conn, datadir, generatedrecoveryconfcontents);
+
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters:\n%s", generatedrecoveryconfcontents->data);
+}
+
+/*
+ * Reset the previously set recovery parameters.
+ */
+static void
+reset_recovery_params(const struct LogicalRepInfo *dbinfo, const char *datadir)
+{
+	PGconn	   *conn;
+	PQExpBuffer generatedrecoveryconfcontents;
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	generatedrecoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);
+
+	appendPQExpBuffer(recoveryconfcontents, "%s",
+					  generatedrecoveryconfcontents->data);

 	if (dry_run)
-	{
 		appendPQExpBufferStr(recoveryconfcontents, "# dry run mode");
-		appendPQExpBuffer(recoveryconfcontents,
-						  "recovery_target_lsn = '%X/%08X'\n",
-						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
-	}
 	else
-	{
-		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
-						  lsn);
-		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
-	}
+		ReplaceRecoveryConfig(conn, datadir, recoveryconfcontents);
+
 	disconnect_database(conn, false);

-	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+	pg_log_debug("recovery parameters were reset");
 }

 /*
@@ -2458,6 +2520,9 @@ main(int argc, char **argv)
 	pg_log_info("stopping the subscriber");
 	stop_standby_server(subscriber_dir);

+	/* Reset recovery parameters */
+	reset_recovery_params(dbinfos.dbinfo, subscriber_dir);
+
 	/* Change system identifier from subscriber */
 	modify_subscriber_sysid(&opt);

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 229fef5b3b5..69fb6a3dbef 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -41,6 +41,16 @@ sub generate_db
 	return $dbname;
 }

+sub test_param_absent {
+    my ($node, $param) = @_;
+    my $auto_conf = $node->data_dir . '/postgresql.auto.conf';
+
+    return 1 unless -e $auto_conf;
+
+    my $content = slurp_file($auto_conf);
+    return $content !~ /^\s*$param\s*=/m;
+}
+
 #
 # Test mandatory options
 command_fails(['pg_createsubscriber'],
@@ -467,6 +477,13 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');

+# Verify that recovery parameters have been reset after pg_createsubscriber
+# We check recovery_target_lsn as a representative parameter - since all
+# recovery parameters are managed as a group, the absence of one indicates
+# that the entire set has been properly cleared from the configuration.
+ok(test_param_absent($node_s, 'recovery_target_lsn'),
+   'recovery_target_lsn parameter was removed');
+
 # Confirm the physical replication slot has been removed
 $result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
--
2.51.0

#9Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alyona Vinter (#8)
2 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

Hello, Alyona!

On Mon, Sep 8, 2025 at 8:35 AM Alyona Vinter <dlaaren8@gmail.com> wrote:

Sorry, wrong patches again. Here are the correct ones.

I went though this patches.
1) I've removed the array of parameters. I see it was proposed by
Michael upthread. But I think his proposal came from the fact we walk
trough the parameters twice. But we end up walking trough the
parameter once in setup_recovery(), while reset_recovery_params() just
restores the previous contents. I think it makes sense to keep the
changes minimal.
2) I reordered patches so that helper function goes first. I think it
essential to order commit in the way that every commit leaves our tree
in working state.
3) I make pgpreltidy run over 040_pg_createsubscriber.pl.
Any thought?

------
Regards,
Alexander Korotkov
Supabase

Attachments:

v4-0001-Implements-helper-function-in-recovery_gen.patchapplication/octet-stream; name=v4-0001-Implements-helper-function-in-recovery_gen.patchDownload
From 5d4032bd916915bd9bd270628a42e03239686ecf Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH v4 1/2] Implements helper function in recovery_gen

These functions support pg_createsubscriber's need to temporarily
configure recovery params and ensure proper cleanup after the conversion
to logical replication is complete.
---
 src/fe_utils/recovery_gen.c         | 77 +++++++++++++++++++++++++++++
 src/include/fe_utils/recovery_gen.h |  3 ++
 2 files changed, 80 insertions(+)

diff --git a/src/fe_utils/recovery_gen.c b/src/fe_utils/recovery_gen.c
index e9023584768..e8e0dde9e00 100644
--- a/src/fe_utils/recovery_gen.c
+++ b/src/fe_utils/recovery_gen.c
@@ -10,6 +10,7 @@
 #include "postgres_fe.h"
 
 #include "common/logging.h"
+#include "common/file_utils.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/string_utils.h"
 
@@ -234,3 +235,79 @@ GetDbnameFromConnectionOptions(const char *connstr)
 	PQconninfoFree(conn_opts);
 	return dbname;
 }
+
+PQExpBuffer
+GetRecoveryConfig(PGconn *pgconn, const char *target_dir)
+{
+	PQExpBuffer contents;
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	char data[1024];
+	size_t bytes_read;
+
+	Assert(pgconn != NULL);
+
+	contents = createPQExpBuffer();
+	if (!contents)
+		pg_fatal("out of memory");
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(filename, "r");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", filename);
+
+	while ((bytes_read = fread(data, 1, sizeof(data), cf)) > 0)
+	{
+		data[bytes_read] = '\0';
+		appendPQExpBufferStr(contents, data);
+	}
+
+	if (ferror(cf))
+	{
+		pg_fatal("could not read from file \"%s\": %m", filename);
+	}
+
+	fclose(cf);
+
+	return contents;
+}
+
+void
+ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents)
+{
+	char		tmp_filename[MAXPGPATH];
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	Assert(pgconn != NULL);
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(tmp_filename, MAXPGPATH, "%s/%s.tmp", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(tmp_filename, "w");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", tmp_filename);
+
+	if (fwrite(contents->data, contents->len, 1, cf) != 1)
+		pg_fatal("could not write to file \"%s\": %m", tmp_filename);
+
+	fclose(cf);
+
+	if (durable_rename(tmp_filename, filename) != 0)
+		pg_fatal("could not rename file \"%s\" to \"%s\": %m",
+				 tmp_filename, filename);
+}
diff --git a/src/include/fe_utils/recovery_gen.h b/src/include/fe_utils/recovery_gen.h
index c13f2263bcd..18219af966b 100644
--- a/src/include/fe_utils/recovery_gen.h
+++ b/src/include/fe_utils/recovery_gen.h
@@ -27,4 +27,7 @@ extern void WriteRecoveryConfig(PGconn *pgconn, const char *target_dir,
 								PQExpBuffer contents);
 extern char *GetDbnameFromConnectionOptions(const char *connstr);
 
+extern PQExpBuffer GetRecoveryConfig(PGconn *pgconn, const char *target_dir);
+extern void ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents);
+
 #endif							/* RECOVERY_GEN_H */
-- 
2.39.5 (Apple Git-154)

v4-0002-Reseting-recovery-target-parameters-in-pg_creates.patchapplication/octet-stream; name=v4-0002-Reseting-recovery-target-parameters-in-pg_creates.patchDownload
From df4719db9a5432968bd00bdb44020b10cf2c2d97 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH v4 2/2] Reseting recovery target parameters in
 pg_createsubscriber

The utility sets recovery target params for correct recovery before
conversion a physical replica to a logical one but does not reset them
afterward. It may cause recovery failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system may incorrectly determine that the
recovery target was never reached because these parameters remain
active.

This change ensures all recovery parameters are properly reset after
conversion to prevent such edge cases.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 38 +++++++++++++++++++
 .../t/040_pg_createsubscriber.pl              | 18 +++++++++
 2 files changed, 56 insertions(+)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 3986882f042..460871cdc64 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -161,6 +161,13 @@ enum WaitPMResult
 	POSTMASTER_STILL_STARTING
 };
 
+/*
+ * Buffer to preserve the original recovery conf contents before modifying
+ * recovery parameters. This allows restoration of the original configuration
+ * after the logical replication process completes, maintaining the system's
+ * previous recovery state.
+ */
+static PQExpBuffer savedrecoveryconfcontents;
 
 /*
  * Cleanup objects that were created by pg_createsubscriber if there is an
@@ -1236,6 +1243,9 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo, true);
 
+	/* Before setting up the recovery parameters save the original content. */
+	savedrecoveryconfcontents = GetRecoveryConfig(conn, datadir);
+
 	/*
 	 * Write recovery parameters.
 	 *
@@ -1285,6 +1295,31 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
 }
 
+/*
+ * Reset the previously set recovery parameters.
+ */
+static void
+reset_recovery_params(const struct LogicalRepInfo *dbinfo, const char *datadir)
+{
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);
+
+	appendPQExpBuffer(savedrecoveryconfcontents, "%s",
+					  recoveryconfcontents->data);
+
+	if (dry_run)
+		appendPQExpBufferStr(savedrecoveryconfcontents, "# dry run mode");
+	else
+		ReplaceRecoveryConfig(conn, datadir, savedrecoveryconfcontents);
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters were reset");
+}
+
 /*
  * Drop physical replication slot on primary if the standby was using it. After
  * the transformation, it has no use.
@@ -2458,6 +2493,9 @@ main(int argc, char **argv)
 	pg_log_info("stopping the subscriber");
 	stop_standby_server(subscriber_dir);
 
+	/* Reset recovery parameters */
+	reset_recovery_params(dbinfos.dbinfo, subscriber_dir);
+
 	/* Change system identifier from subscriber */
 	modify_subscriber_sysid(&opt);
 
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 229fef5b3b5..099f1553a5f 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -41,6 +41,17 @@ sub generate_db
 	return $dbname;
 }
 
+sub test_param_absent
+{
+	my ($node, $param) = @_;
+	my $auto_conf = $node->data_dir . '/postgresql.auto.conf';
+
+	return 1 unless -e $auto_conf;
+
+	my $content = slurp_file($auto_conf);
+	return $content !~ /^\s*$param\s*=/m;
+}
+
 #
 # Test mandatory options
 command_fails(['pg_createsubscriber'],
@@ -467,6 +478,13 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');
 
+# Verify that recovery parameters have been reset after pg_createsubscriber
+# We check recovery_target_lsn as a representative parameter - since all
+# recovery parameters are managed as a group, the absence of one indicates
+# that the entire set has been properly cleared from the configuration.
+ok( test_param_absent($node_s, 'recovery_target_lsn'),
+	'recovery_target_lsn parameter was removed');
+
 # Confirm the physical replication slot has been removed
 $result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
-- 
2.39.5 (Apple Git-154)

#10Michael Paquier
michael@paquier.xyz
In reply to: Alexander Korotkov (#9)
Re: Resetting recovery target parameters in pg_createsubscriber

On Mon, Sep 15, 2025 at 10:29:47AM +0300, Alexander Korotkov wrote:

I went though this patches.
1) I've removed the array of parameters. I see it was proposed by
Michael upthread. But I think his proposal came from the fact we walk
trough the parameters twice. But we end up walking trough the
parameter once in setup_recovery(), while reset_recovery_params() just
restores the previous contents. I think it makes sense to keep the
changes minimal.

Yeah, my concern was about the duplication of the list. As long as a
fix does not do any of that, I'm OK. Sorry if my idea of a list of
parameters felt misguided if we make recovery_gen.c smarter with the
handling of the on-disk files.

2) I reordered patches so that helper function goes first. I think it
essential to order commit in the way that every commit leaves our tree
in working state.

Yep. That would create some noise if one bisects for example. These
are always annoying because they make analysis of a range of commits
longer with more false positives. If you have a large range of
commits, the odds are usually very low, but who knows..

3) I make pgpreltidy run over 040_pg_createsubscriber.pl.
Any thought?

GetRecoveryConfig() and ReplaceRecoveryConfig() should have some
documentation, regarding what the callers of these functions can
expect from them.

+    use_recovery_conf =
+        PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+    snprintf(tmp_filename, MAXPGPATH, "%s/%s.tmp", target_dir,
+             use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+    snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+             use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf"

No need for use_recovery_conf. You could just set a pointer to the
file name instead and avoid the duplication.

+	cf = fopen(tmp_filename, "w");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", tmp_filename);

"a" is used in fopen() when calling WriteRecoveryConfig() when under
use_recovery_conf. Perhaps this inconsistency should be specified as
a comment because we are generating a temporary file from scratch with
the new recovery GUC contents?

This patch also means that pg_createsubscriber is written so as the
contents added to recovery.conf/postgresql.auto.conf by
setup_recovery() are never reset if there is a failure in-flight. Is
that OK or should we also have an exit callback to do the cleanup work
in such cases?

Perhaps these internal manipulations should be documented as well, to
make the users of this tool aware of steps they may need to take in
the event of an in-flight failure? pg_createsubscriber includes a
"How it works" section that explains how the tool works, including the
part about the recovery parameters. The changes of this patch become
implied facts, and are not reflected in the docs. That sounds like a
problem to me because we are hiding some of the the internal logic,
but the docs are written so as they explain all these details.
--
Michael

#11Alyona Vinter
dlaaren8@gmail.com
In reply to: Michael Paquier (#10)
Re: Resetting recovery target parameters in pg_createsubscriber

Hi Michael and Alexander,

Thank you both for your help! I really appreciate it.
As a newcomer here, I might make some mistakes, but I hope with your
guidance I can avoid making them in the future =)

Yeah, my concern was about the duplication of the list. As long as a
fix does not do any of that, I'm OK. Sorry if my idea of a list of
parameters felt misguided if we make recovery_gen.c smarter with the
handling of the on-disk files.

I got your concern about avoiding duplication. I thought that defining all
parameters explicitly in the file header would lead to clearer and nicer
code, which is why I left it that way (even without duplicating). But now I
agree with Alexander's point about keeping the changes minimal.

This patch also means that pg_createsubscriber is written so as the
contents added to recovery.conf/postgresql.auto.conf by
setup_recovery() are never reset if there is a failure in-flight. Is
that OK or should we also have an exit callback to do the cleanup work
in such cases?

It's a good idea to add an exit callback. Additionally, I'd like to propose
adding a pre-flight check at the start. This check would look for any
existing recovery configuration that might be an artifact from a previous
aborted run and warn the user or handle it appropriately. What do you think
about implementing both the exit callback and the pre-flight check?

pg_createsubscriber includes a
"How it works" section that explains how the tool works, including the
part about the recovery parameters.

I looked through the `pg_createsubscriber.c` file but wasn't able to locate
a "How it works" section. Could you please point me to the specific file or
line number you are referring to? Or do you mean all the descriptive
comments? For context, I'm currently working on the version where my patch
is being tested in CI.

I will work on improving the code and will also add the documentation notes
that Michael has pointed out ASAP.

Best regards,
Alyona Vinter

#12Michael Paquier
michael@paquier.xyz
In reply to: Alyona Vinter (#11)
Re: Resetting recovery target parameters in pg_createsubscriber

On Tue, Sep 16, 2025 at 05:27:43PM +0700, Alyona Vinter wrote:

This patch also means that pg_createsubscriber is written so as the
contents added to recovery.conf/postgresql.auto.conf by
setup_recovery() are never reset if there is a failure in-flight. Is
that OK or should we also have an exit callback to do the cleanup work
in such cases?

It's a good idea to add an exit callback. Additionally, I'd like to propose
adding a pre-flight check at the start. This check would look for any
existing recovery configuration that might be an artifact from a previous
aborted run and warn the user or handle it appropriately. What do you think
about implementing both the exit callback and the pre-flight check?

I am not sure how much a pre-flight check would help if we have an
exit callback that would make sure that things are cleaned up on exit.
Is there any need to worry about a kill(9) that would cause the exit
cleanup callback to not be called? We don't bother about that
usually, so I don't see a strong case for it here, either. :)

Alexander may have a different opinion.

I will work on improving the code and will also add the documentation notes
that Michael has pointed out ASAP.

Thanks.
--
Michael

#13Alena Vinter
dlaaren8@gmail.com
In reply to: Michael Paquier (#12)
3 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

Hi,

I'm back with improvements :)
I've added code comments in `recovery_gen.c` and expanded the documentation
in `pg_createsubscriber.sgml`.

About the recovery parameters cleanup: I thought about adding an exit
callback, but it doesn't really make sense because once the target server
gets promoted (which happens soon after we set the parameters), there's no
point in cleaning up - the server is already promoted and can't be used as
a replica again and must be recreated. Also, `reset_recovery_params()`
might call `exit()` itself, which could cause problems with the cleanup
callback.
So I think it's better to just warn users about leftover parameters and let
them handle the cleanup manually if needed.

By the way, is it ok that the second patch includes both code and test
changes together, or should I split them into separate commits?

I look forward to your feedback!

Regards,
Alena Vinter

Attachments:

v5-0002-Reseting-recovery-target-parameters-in-pg_createsubscriber.patchtext/x-patch; charset=US-ASCII; name=v5-0002-Reseting-recovery-target-parameters-in-pg_createsubscriber.patchDownload
From ce69fdbb0c2f7ce340ac714f2aea8e0f1072069a Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 2/3] Reseting recovery target parameters in
 pg_createsubscriber

The utility sets recovery target params for correct recovery before
conversion a physical replica to a logical one but does not reset them
afterward. It may cause recovery failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system may incorrectly determine that the
recovery target was never reached because these parameters remain
active.

This change ensures all recovery parameters are properly reset after
conversion to prevent such edge cases.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 59 ++++++++++++++++++-
 .../t/040_pg_createsubscriber.pl              | 18 ++++++
 2 files changed, 74 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 3986882f042..7ff2708bfff 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -154,6 +154,7 @@ static char *subscriber_dir = NULL;
 
 static bool recovery_ended = false;
 static bool standby_running = false;
+static bool recovery_params_set = false;
 
 enum WaitPMResult
 {
@@ -161,6 +162,13 @@ enum WaitPMResult
 	POSTMASTER_STILL_STARTING
 };
 
+/*
+ * Buffer to preserve the original recovery conf contents before modifying
+ * recovery parameters. This allows restoration of the original configuration
+ * after the logical replication process completes, maintaining the system's
+ * previous recovery state.
+ */
+static PQExpBuffer savedrecoveryconfcontents = NULL;
 
 /*
  * Cleanup objects that were created by pg_createsubscriber if there is an
@@ -169,9 +177,8 @@ enum WaitPMResult
  * 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.
+ * There is no cleanup on the target server *after* its promotion because any
+ * failure at this point means recreate the physical replica and start again.
  */
 static void
 cleanup_objects_atexit(void)
@@ -190,6 +197,18 @@ cleanup_objects_atexit(void)
 		pg_log_warning_hint("The target server cannot be used as a physical replica anymore.  "
 							"You must recreate the physical replica before continuing.");
 	}
+	else if (recovery_params_set)
+	{
+		/*
+		 * Skip parameter reset check since reset happens after recovery.
+		 * Therefore, leftover parameters don't matter as the target needs
+		 * full recreation.
+		 */
+		pg_log_warning("recovery parameters were set but not properly reset on the target");
+		pg_log_warning_hint("Manual removal of recovery parameters is required from 'postgresql.auto.conf' (PostgreSQL %d+) or 'recovery.conf' (older versions)",
+							MINIMUM_VERSION_FOR_RECOVERY_GUC / 10000);
+
+	}
 
 	for (int i = 0; i < num_dbs; i++)
 	{
@@ -1236,6 +1255,9 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo, true);
 
+	/* Before setting up the recovery parameters save the original content. */
+	savedrecoveryconfcontents = GetRecoveryConfig(conn, datadir);
+
 	/*
 	 * Write recovery parameters.
 	 *
@@ -1278,6 +1300,9 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  lsn);
+
+		recovery_params_set = true;
+
 		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
@@ -1285,6 +1310,31 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
 }
 
+/*
+ * Reset the previously set recovery parameters.
+ */
+static void
+reset_recovery_params(const struct LogicalRepInfo *dbinfo, const char *datadir)
+{
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);
+
+	appendPQExpBuffer(savedrecoveryconfcontents, "%s",
+					  recoveryconfcontents->data);
+
+	if (dry_run)
+		appendPQExpBufferStr(savedrecoveryconfcontents, "# dry run mode");
+	else
+		ReplaceRecoveryConfig(conn, datadir, savedrecoveryconfcontents);
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters were reset");
+}
+
 /*
  * Drop physical replication slot on primary if the standby was using it. After
  * the transformation, it has no use.
@@ -2458,6 +2508,9 @@ main(int argc, char **argv)
 	pg_log_info("stopping the subscriber");
 	stop_standby_server(subscriber_dir);
 
+	/* Reset recovery parameters */
+	reset_recovery_params(dbinfos.dbinfo, subscriber_dir);
+
 	/* Change system identifier from subscriber */
 	modify_subscriber_sysid(&opt);
 
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 229fef5b3b5..099f1553a5f 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -41,6 +41,17 @@ sub generate_db
 	return $dbname;
 }
 
+sub test_param_absent
+{
+	my ($node, $param) = @_;
+	my $auto_conf = $node->data_dir . '/postgresql.auto.conf';
+
+	return 1 unless -e $auto_conf;
+
+	my $content = slurp_file($auto_conf);
+	return $content !~ /^\s*$param\s*=/m;
+}
+
 #
 # Test mandatory options
 command_fails(['pg_createsubscriber'],
@@ -467,6 +478,13 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');
 
+# Verify that recovery parameters have been reset after pg_createsubscriber
+# We check recovery_target_lsn as a representative parameter - since all
+# recovery parameters are managed as a group, the absence of one indicates
+# that the entire set has been properly cleared from the configuration.
+ok( test_param_absent($node_s, 'recovery_target_lsn'),
+	'recovery_target_lsn parameter was removed');
+
 # Confirm the physical replication slot has been removed
 $result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
-- 
2.51.0

v5-0001-Implements-helper-function-in-recovery_gen.patchtext/x-patch; charset=US-ASCII; name=v5-0001-Implements-helper-function-in-recovery_gen.patchDownload
From 5480fe56ec6b7f51797c2a9566dc555243ad9549 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 1/3] Implements helper function in recovery_gen

These functions support pg_createsubscriber's need to temporarily
configure recovery params and ensure proper cleanup after the conversion
to logical replication is complete.
---
 src/fe_utils/recovery_gen.c         | 101 ++++++++++++++++++++++++++++
 src/include/fe_utils/recovery_gen.h |   3 +
 2 files changed, 104 insertions(+)

diff --git a/src/fe_utils/recovery_gen.c b/src/fe_utils/recovery_gen.c
index e9023584768..f5e9e968863 100644
--- a/src/fe_utils/recovery_gen.c
+++ b/src/fe_utils/recovery_gen.c
@@ -10,6 +10,7 @@
 #include "postgres_fe.h"
 
 #include "common/logging.h"
+#include "common/file_utils.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/string_utils.h"
 
@@ -234,3 +235,103 @@ GetDbnameFromConnectionOptions(const char *connstr)
 	PQconninfoFree(conn_opts);
 	return dbname;
 }
+
+/*
+ * GetRecoveryConfig
+ *
+ * Reads the recovery configuration file from the target server's data directory
+ * and returns its contents as a PQExpBuffer.
+ */
+PQExpBuffer
+GetRecoveryConfig(PGconn *pgconn, const char *target_dir)
+{
+	PQExpBuffer contents;
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	char		data[1024];
+	size_t		bytes_read;
+
+	Assert(pgconn != NULL);
+
+	contents = createPQExpBuffer();
+	if (!contents)
+		pg_fatal("out of memory");
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(filename, "r");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", filename);
+
+	/* Read file contents in chunks and append to the buffer */
+	while ((bytes_read = fread(data, 1, sizeof(data), cf)) > 0)
+	{
+		data[bytes_read] = '\0';
+		appendPQExpBufferStr(contents, data);
+	}
+
+	if (ferror(cf))
+	{
+		pg_fatal("could not read from file \"%s\": %m", filename);
+	}
+
+	fclose(cf);
+
+	return contents;
+}
+
+/*
+ * ReplaceRecoveryConfig
+ *
+ * Replaces the recovery configuration file on the target server with new contents.
+ *
+ * The operation is performed atomically by writing to a temporary file first,
+ * then renaming it to the final filename.
+ */
+void
+ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents)
+{
+	char		tmp_filename[MAXPGPATH];
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	const char *config_filename =
+	(PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC)
+	? "recovery.conf"
+	: "postgresql.auto.conf";
+
+	Assert(pgconn != NULL);
+
+	/*
+	 * Construct full paths for the configuration file and its temporary
+	 * version
+	 */
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir, config_filename);
+	snprintf(tmp_filename, MAXPGPATH, "%s.tmp", filename);
+
+	/*
+	 * Open temporary file for writing. Mode "w" ensures the file is recreated
+	 * if it already exists.
+	 */
+	cf = fopen(tmp_filename, "w");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", tmp_filename);
+
+	if (fwrite(contents->data, contents->len, 1, cf) != 1)
+		pg_fatal("could not write to file \"%s\": %m", tmp_filename);
+
+	fclose(cf);
+
+	/*
+	 * Atomically replace the old configuration file with the new one by
+	 * renaming the temporary file to the final filename.
+	 */
+	if (durable_rename(tmp_filename, filename) != 0)
+		pg_fatal("could not rename file \"%s\" to \"%s\": %m",
+				 tmp_filename, filename);
+}
diff --git a/src/include/fe_utils/recovery_gen.h b/src/include/fe_utils/recovery_gen.h
index c13f2263bcd..18219af966b 100644
--- a/src/include/fe_utils/recovery_gen.h
+++ b/src/include/fe_utils/recovery_gen.h
@@ -27,4 +27,7 @@ extern void WriteRecoveryConfig(PGconn *pgconn, const char *target_dir,
 								PQExpBuffer contents);
 extern char *GetDbnameFromConnectionOptions(const char *connstr);
 
+extern PQExpBuffer GetRecoveryConfig(PGconn *pgconn, const char *target_dir);
+extern void ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents);
+
 #endif							/* RECOVERY_GEN_H */
-- 
2.51.0

v5-0003-doc-Add-warning-about-leftover-recovery-parameters-in-pg_createsubscriber.patchtext/x-patch; charset=US-ASCII; name=v5-0003-doc-Add-warning-about-leftover-recovery-parameters-in-pg_createsubscriber.patchDownload
From 068ee56f7a84b272098b2402501db48b1405ce31 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 23 Sep 2025 10:23:46 +0700
Subject: [PATCH 3/3] doc: Add warning about leftover recovery parameters in
 pg_createsubscriber

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index bb9cc72576c..bf6e1844a18 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -520,11 +520,19 @@ PostgreSQL documentation
       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.
+      recovery targets that can cause a failure.
+     </para>
+     <para>
+      If an error occurs during this step, recovery parameters may remain in the
+      target server's configuration.  You may need to manually remove these
+      leftover parameters before attempting to run
+      <application>pg_createsubscriber</application> again.
+     </para>
+     <para>
+      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>

--
2.51.0

#14Michael Paquier
michael@paquier.xyz
In reply to: Alena Vinter (#13)
Re: Resetting recovery target parameters in pg_createsubscriber

On Tue, Sep 23, 2025 at 12:04:04PM +0700, Alena Vinter wrote:

About the recovery parameters cleanup: I thought about adding an exit
callback, but it doesn't really make sense because once the target server
gets promoted (which happens soon after we set the parameters), there's no
point in cleaning up - the server is already promoted and can't be used as
a replica again and must be recreated. Also, `reset_recovery_params()`
might call `exit()` itself, which could cause problems with the cleanup
callback.

Your argument does not consider one case, which is very common:
pg_rewind. Even if the standby finishes recovery and is promoted with
its new recovery parameters, we could rewind it rather than recreate a
new standby from scratch. That's cheaper than recreating a new
physical replica from scratch. Keeping the recovery parameters added
by pg_createsubscriber around would make pg_rewind's work more
complicated, because it does similar manipulations, for different
requirements.

The tipping point where we would not be able to reuse the promoted
standby happens as the last step of pg_createsuscriber in
modify_subscriber_sysid() where its system ID is changed. Before
that, the code also makes an effort of cleaning up anything that's
been created in-betwee.

Even the system ID argument is not entirely true, actually. One could
also decide to switch the system ID back to what it was previously to
match with the primary. That requires a bit more magic, but that's
not impossible.

So I think it's better to just warn users about leftover parameters and let
them handle the cleanup manually if needed.

Warnings tend to be ignored and missed, especially these days where
vendors automate these actions. It is true that there could be an
argument about requiring extra implementation steps on each vendor
side, but they would also need to keep up with any new GUCs that
pg_createsubscriber may add in the future when setting up its recovery
parameters, which would mean extra work for everybody, increasing the
range of problems for some logic that's isolated to
pg_createsubscriber.

In short, I disagree with what you are doing here: we should take the
extra step and clean up anything that's been created by the tool when
we know we can safely do so (aka adding a static flag that the
existing cleanup callback should rely on, which is already what your
patch 0003 does to show a warning).

By the way, is it ok that the second patch includes both code and test
changes together, or should I split them into separate commits?

The tests and the fix touch entirely separate code paths, keeping them
together is no big deal.
--
Michael

#15Alena Vinter
dlaaren8@gmail.com
In reply to: Michael Paquier (#14)
3 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

Hi,

In short, I disagree with what you are doing here: we should take the
extra step and clean up anything that's been created by the tool when
we know we can safely do so

I got your point, thanks for pointing to the `pg_rewind` case. I've
attached a new version of the patches. I've changed `ReplaceRecoveryConfig`
a little bit -- now it returns false in case of an error instead of exiting.

Best wishes,
Alena Vinter

Attachments:

v6-0001-Implements-helper-function-in-recovery_gen.patchtext/x-patch; charset=US-ASCII; name=v6-0001-Implements-helper-function-in-recovery_gen.patchDownload
From d9f96c1b5ea468ac4dea427e51d9beb3cd087653 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 1/3] Implements helper function in recovery_gen

These functions support pg_createsubscriber's need to temporarily
configure recovery params and ensure proper cleanup after the conversion
to logical replication is complete.
---
 src/fe_utils/recovery_gen.c         | 117 ++++++++++++++++++++++++++++
 src/include/fe_utils/recovery_gen.h |   4 +
 2 files changed, 121 insertions(+)

diff --git a/src/fe_utils/recovery_gen.c b/src/fe_utils/recovery_gen.c
index e9023584768..e22d7673ff8 100644
--- a/src/fe_utils/recovery_gen.c
+++ b/src/fe_utils/recovery_gen.c
@@ -10,6 +10,7 @@
 #include "postgres_fe.h"
 
 #include "common/logging.h"
+#include "common/file_utils.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/string_utils.h"
 
@@ -234,3 +235,119 @@ GetDbnameFromConnectionOptions(const char *connstr)
 	PQconninfoFree(conn_opts);
 	return dbname;
 }
+
+/*
+ * GetRecoveryConfig
+ *
+ * Reads the recovery configuration file from the target server's data directory
+ * and returns its contents as a PQExpBuffer.
+ */
+PQExpBuffer
+GetRecoveryConfig(PGconn *pgconn, const char *target_dir)
+{
+	PQExpBuffer contents;
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	bool		use_recovery_conf;
+
+	char		data[1024];
+	size_t		bytes_read;
+
+	Assert(pgconn != NULL);
+
+	contents = createPQExpBuffer();
+	if (!contents)
+		pg_fatal("out of memory");
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(filename, "r");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", filename);
+
+	/* Read file contents in chunks and append to the buffer */
+	while ((bytes_read = fread(data, 1, sizeof(data), cf)) > 0)
+	{
+		data[bytes_read] = '\0';
+		appendPQExpBufferStr(contents, data);
+	}
+
+	if (ferror(cf))
+	{
+		pg_fatal("could not read from file \"%s\": %m", filename);
+	}
+
+	fclose(cf);
+
+	return contents;
+}
+
+/*
+ * ReplaceRecoveryConfig
+ *
+ * Replaces the recovery configuration file on the target server with new contents.
+ *
+ * The operation is performed atomically by writing to a temporary file first,
+ * then renaming it to the final filename.
+ *
+ * Returns false if an error occurs. In case of error, a temporary file may
+ * still be present on the server.
+ */
+bool
+ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents)
+{
+	char		tmp_filename[MAXPGPATH];
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	const char *config_filename;
+
+	Assert(pgconn != NULL);
+
+	config_filename =
+		(PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC)
+		? "recovery.conf"
+		: "postgresql.auto.conf";
+
+	/*
+	 * Construct full paths for the configuration file and its temporary
+	 * version
+	 */
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir, config_filename);
+	snprintf(tmp_filename, MAXPGPATH, "%s.tmp", filename);
+
+	/*
+	 * Open temporary file for writing. Mode "w" ensures the file is recreated
+	 * if it already exists.
+	 */
+	cf = fopen(tmp_filename, "w");
+	if (cf == NULL)
+	{
+		pg_log_error("could not open file \"%s\": %m", tmp_filename);
+		return false;
+	}
+
+	if (fwrite(contents->data, contents->len, 1, cf) != 1)
+	{
+		pg_log_error("could not write to file \"%s\": %m", tmp_filename);
+		return false;
+	}
+
+	fclose(cf);
+
+	/*
+	 * Atomically replace the old configuration file with the new one by
+	 * renaming the temporary file to the final filename.
+	 */
+	if (durable_rename(tmp_filename, filename) != 0)
+	{
+		pg_log_error("could not rename file \"%s\" to \"%s\": %m",
+					 tmp_filename, filename);
+		return false;
+	}
+
+	return true;
+}
diff --git a/src/include/fe_utils/recovery_gen.h b/src/include/fe_utils/recovery_gen.h
index c13f2263bcd..f5f21ec088d 100644
--- a/src/include/fe_utils/recovery_gen.h
+++ b/src/include/fe_utils/recovery_gen.h
@@ -27,4 +27,8 @@ extern void WriteRecoveryConfig(PGconn *pgconn, const char *target_dir,
 								PQExpBuffer contents);
 extern char *GetDbnameFromConnectionOptions(const char *connstr);
 
+extern PQExpBuffer GetRecoveryConfig(PGconn *pgconn, const char *target_dir);
+extern bool ReplaceRecoveryConfig(PGconn *pgconn, const char *target_dir,
+								  PQExpBuffer contents);
+
 #endif							/* RECOVERY_GEN_H */
-- 
2.51.0

v6-0003-doc-Add-warning-about-leftover-recovery-parameters-i.patchtext/x-patch; charset=US-ASCII; name=v6-0003-doc-Add-warning-about-leftover-recovery-parameters-i.patchDownload
From e3195ccf67312fa39c2751b9d021760e5eb1ddfd Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 23 Sep 2025 10:23:46 +0700
Subject: [PATCH 3/3] doc: Add warning about leftover recovery parameters in
 pg_createsubscriber

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index bb9cc72576c..fa14aeeef13 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -520,11 +520,19 @@ PostgreSQL documentation
       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.
+      recovery targets that can cause a failure.
+     </para>
+     <para>
+      If an error occurs during this step, recovery parameters might remain in
+      the target server's configuration.  You may need to manually remove these
+      leftover parameters before attempting to run.
+      <application>pg_createsubscriber</application> again.
+     </para>
+     <para>
+      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>
 
-- 
2.51.0

v6-0002-Reseting-recovery-target-parameters-in-pg_createsubs.patchtext/x-patch; charset=US-ASCII; name=v6-0002-Reseting-recovery-target-parameters-in-pg_createsubs.patchDownload
From 7544cc434c488caf834e812be036caac5d9493b5 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 2/3] Reseting recovery target parameters in
 pg_createsubscriber

The utility sets recovery target params for correct recovery before
conversion a physical replica to a logical one but does not reset them
afterward. It may cause recovery failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system may incorrectly determine that the
recovery target was never reached because these parameters remain
active.

This change ensures all recovery parameters are properly reset after
conversion to prevent such edge cases.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 73 ++++++++++++++++++-
 .../t/040_pg_createsubscriber.pl              | 18 +++++
 2 files changed, 88 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 3986882f042..714e8659dcb 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -154,6 +154,8 @@ static char *subscriber_dir = NULL;

 static bool recovery_ended = false;
 static bool standby_running = false;
+static bool recovery_params_set = false;
+static bool recovery_params_reset = false;

 enum WaitPMResult
 {
@@ -161,6 +163,13 @@ enum WaitPMResult
 	POSTMASTER_STILL_STARTING
 };

+/*
+ * Buffer to preserve the original recovery conf contents before modifying
+ * recovery parameters. This allows restoration of the original configuration
+ * after the logical replication process completes, maintaining the system's
+ * previous recovery state.
+ */
+static PQExpBuffer savedrecoveryconfcontents = NULL;

 /*
  * Cleanup objects that were created by pg_createsubscriber if there is an
@@ -169,9 +178,8 @@ enum WaitPMResult
  * 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.
+ * There is no cleanup on the target server *after* its promotion because any
+ * failure at this point means recreate the physical replica and start again.
  */
 static void
 cleanup_objects_atexit(void)
@@ -191,6 +199,27 @@ cleanup_objects_atexit(void)
 							"You must recreate the physical replica before continuing.");
 	}

+	if (recovery_params_set && !recovery_params_reset)
+	{
+		PGconn	   *conn;
+		bool		no_err = true;
+
+		conn = connect_database(dbinfos.dbinfo[0].pubconninfo, false);
+		if (conn != NULL)
+		{
+			no_err = ReplaceRecoveryConfig(conn, subscriber_dir,
+										   savedrecoveryconfcontents);
+			disconnect_database(conn, false);
+			pg_log_info("previously set recovery parameters were properly reset on the target");
+		}
+		if (conn == NULL || !no_err)
+		{
+			pg_log_warning("recovery parameters were set but not properly reset on the target");
+			pg_log_warning_hint("Manual removal of recovery parameters is required from 'postgresql.auto.conf' (PostgreSQL %d+) or 'recovery.conf' (older versions)",
+								MINIMUM_VERSION_FOR_RECOVERY_GUC / 10000);
+		}
+	}
+
 	for (int i = 0; i < num_dbs; i++)
 	{
 		struct LogicalRepInfo *dbinfo = &dbinfos.dbinfo[i];
@@ -1236,6 +1265,9 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo, true);

+	/* Before setting up the recovery parameters save the original content. */
+	savedrecoveryconfcontents = GetRecoveryConfig(conn, datadir);
+
 	/*
 	 * Write recovery parameters.
 	 *
@@ -1278,6 +1310,8 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  lsn);
+
+		recovery_params_set = true;
 		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
@@ -1285,6 +1319,36 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
 }

+/*
+ * Reset the previously set recovery parameters.
+ */
+static void
+reset_recovery_params(const struct LogicalRepInfo *dbinfo, const char *datadir)
+{
+	PGconn	   *conn;
+	PQExpBuffer recoveryconfcontents;
+
+	conn = connect_database(dbinfo[0].pubconninfo, true);
+
+	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);
+
+	appendPQExpBuffer(savedrecoveryconfcontents, "%s",
+					  recoveryconfcontents->data);
+
+	if (dry_run)
+	{
+		appendPQExpBufferStr(savedrecoveryconfcontents, "# dry run mode");
+	}
+	else
+	{
+		ReplaceRecoveryConfig(conn, datadir, savedrecoveryconfcontents);
+		recovery_params_reset = true;
+	}
+	disconnect_database(conn, false);
+
+	pg_log_debug("recovery parameters were reset");
+}
+
 /*
  * Drop physical replication slot on primary if the standby was using it. After
  * the transformation, it has no use.
@@ -2458,6 +2522,9 @@ main(int argc, char **argv)
 	pg_log_info("stopping the subscriber");
 	stop_standby_server(subscriber_dir);

+	/* Reset recovery parameters */
+	reset_recovery_params(dbinfos.dbinfo, subscriber_dir);
+
 	/* Change system identifier from subscriber */
 	modify_subscriber_sysid(&opt);

diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 229fef5b3b5..099f1553a5f 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -41,6 +41,17 @@ sub generate_db
 	return $dbname;
 }

+sub test_param_absent
+{
+	my ($node, $param) = @_;
+	my $auto_conf = $node->data_dir . '/postgresql.auto.conf';
+
+	return 1 unless -e $auto_conf;
+
+	my $content = slurp_file($auto_conf);
+	return $content !~ /^\s*$param\s*=/m;
+}
+
 #
 # Test mandatory options
 command_fails(['pg_createsubscriber'],
@@ -467,6 +478,13 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');

+# Verify that recovery parameters have been reset after pg_createsubscriber
+# We check recovery_target_lsn as a representative parameter - since all
+# recovery parameters are managed as a group, the absence of one indicates
+# that the entire set has been properly cleared from the configuration.
+ok( test_param_absent($node_s, 'recovery_target_lsn'),
+	'recovery_target_lsn parameter was removed');
+
 # Confirm the physical replication slot has been removed
 $result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
--
2.51.0

#16Michael Paquier
michael@paquier.xyz
In reply to: Alena Vinter (#15)
Re: Resetting recovery target parameters in pg_createsubscriber

On Mon, Sep 29, 2025 at 04:57:09PM +0700, Alena Vinter wrote:

I got your point, thanks for pointing to the `pg_rewind` case. I've
attached a new version of the patches. I've changed `ReplaceRecoveryConfig`
a little bit -- now it returns false in case of an error instead of exiting.

#include "common/logging.h"
+#include "common/file_utils.h"

Incorrect include file ordering.

+GetRecoveryConfig(PGconn *pgconn, const char *target_dir)
[...]
+    char        data[1024];
[...]
+    while ((bytes_read = fread(data, 1, sizeof(data), cf)) > 0)
+    {
+        data[bytes_read] = '\0';
+        appendPQExpBufferStr(contents, data);
+    }

You are assuming that this will never overflow. However, recovery
parameters could include commands, which are mostly limited to
MAXPGPATH, itself 1024. So that's unsafe. The in-core routine
pg_get_line(), or the rest in pg_get_line.c is safer to use, relying
on malloc() for the frontend and the lines fetched.

+ pg_log_warning_hint("Manual removal of recovery parameters is required from 'postgresql.auto.conf' (PostgreSQL %d+) or 'recovery.conf' (older versions)", + MINIMUM_VERSION_FOR_RECOVERY_GUC / 10000);

Hmm, okay here. You would need that hint anyway if you cannot connect
to determine to which file the recovery parameters need to go to, the
other code paths failures in ReplaceRecoveryConfig() would include the
file name, which offers a sufficient hint about the version, but a
connect_database() failure does not.

+static bool recovery_params_set = false;
+static bool recovery_params_reset = false;

Hmm. We may need an explanation about these, in the shape of a
comment, to document what's expected from them. Rather than two
booleans, using an enum tracking the state of the parameters would be
cleaner? And actually, you do not need two flags. Why not just
switch recovery_params_set to false once ReplaceRecoveryConfig() is
called?

+reset_recovery_params(const struct LogicalRepInfo *dbinfo, const char
*datadir)
[...]
+   recoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);

Why do we need to call again GenerateRecoveryConfig() when resetting
recovery.conf/postgresql.conf.sample with its original contents before
switching the system ID of the new replica? I may be missing
something, of course, but we're done with recovery so I don't quite
see the point in appending the recovery config generated with the
original contents. If this is justified (don't think it is), this
deserves a comment to explain the reason behind this logic.
--
Michael

#17Alena Vinter
dlaaren8@gmail.com
In reply to: Michael Paquier (#16)
Re: Resetting recovery target parameters in pg_createsubscriber

HI Michael,

Thank you for the review!

Why not just
switch recovery_params_set to false once ReplaceRecoveryConfig() is
called?

Stupid me!

Why do we need to call again GenerateRecoveryConfig() when resetting
recovery.conf/postgresql.conf.sample with its original contents before
switching the system ID of the new replica? I may be missing
something, of course, but we're done with recovery so I don't quite
see the point in appending the recovery config generated with the
original contents. If this is justified (don't think it is), this
deserves a comment to explain the reason behind this logic.

This relates to the point I mentioned earlier about being unsure whether we
should preserve `primary_conninfo`:

I kept primary_conninfo as-is for now since I'm not totally sure if we

need to touch it

The reason I called `GenerateRecoveryConfig()` was to regenerate the
`primary_conninfo` string in the recovery configuration file. If we should
remove it, then the reset function can be much simpler. Could you please
help me to clarify should we regenerate `primary_conninfo` or we can safely
purge it?

Best regards,
Alena Vinter

#18Michael Paquier
michael@paquier.xyz
In reply to: Alena Vinter (#17)
Re: Resetting recovery target parameters in pg_createsubscriber

On Tue, Sep 30, 2025 at 12:22:08PM +0700, Alena Vinter wrote:

This relates to the point I mentioned earlier about being unsure whether we
should preserve `primary_conninfo`:

I kept primary_conninfo as-is for now since I'm not totally sure if we

need to touch it

The reason I called `GenerateRecoveryConfig()` was to regenerate the
`primary_conninfo` string in the recovery configuration file. If we should
remove it, then the reset function can be much simpler. Could you please
help me to clarify should we regenerate `primary_conninfo` or we can safely
purge it?

Based on the contents of the latest patch, we reset the parameters
after promoting the node, and primary_conninfo only matters while we
are in recovery, for a standby recovery WAL using the streaming
replication protocol.
--
Michael

#19Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Alena Vinter (#15)
RE: Resetting recovery target parameters in pg_createsubscriber

Dear Alena,

Thanks for updating the patch. Few comments.

```
+	/* Before setting up the recovery parameters save the original content. */
+	savedrecoveryconfcontents = GetRecoveryConfig(conn, datadir);
```

To confirm, you put the connection to the primary/publisher instead of standby/subscriber.
But it is harmless because streaming replication requires that both instances
have the same major version. Is it correct?

```
+			pg_log_warning_hint("Manual removal of recovery parameters is required from 'postgresql.auto.conf' (PostgreSQL %d+) or 'recovery.conf' (older versions)",
+								MINIMUM_VERSION_FOR_RECOVERY_GUC / 10000);
```

Can we cache the version info when we firstly connect to the primary node to
print appropriate filename? Or is it hacky?

```
+	if (dry_run)
+	{
+		appendPQExpBufferStr(savedrecoveryconfcontents, "# dry run mode");
+	}
```

Per my understanding, setup_recovery() puts the indicator becasue the content
can be printed. I think it is not needed since reset_recovery_params() does not
have that, or we can even print the parameters.

```
+sub test_param_absent
+{
+	my ($node, $param) = @_;
+	my $auto_conf = $node->data_dir . '/postgresql.auto.conf';
+
+	return 1 unless -e $auto_conf;
+
+	my $content = slurp_file($auto_conf);
+	return $content !~ /^\s*$param\s*=/m;
+}
```

Can you add a short comment atop the function? Something like:
"Check whether the given parameter is specified in postgresql.auto.conf"

Best regards,
Hayato Kuroda
FUJITSU LIMITED

#20Ilyasov Ian
ianilyasov@outlook.com
In reply to: Hayato Kuroda (Fujitsu) (#19)
RE: Resetting recovery target parameters in pg_createsubscriber

Hello Alena!

I am new in reviewing here and tried to review your patch:

I agree with the review of Michael considering
+char data[1024];

This looks unsafe.

+static bool recovery_params_set = false;
+static bool recovery_params_reset = false;

Using two booleans here looks wrong to me.
Maybe one is enough with refactored logic in
cleanup_objects_atexit()?

+pg_log_warning_hint("Manual removal of recovery parameters is required from 'postgresql.auto.conf' (PostgreSQL %d+) or 'recovery.conf' (older versions)",

Do we need info about recovery.conf here since patch applies only to master?

Also I am not sure what scenario we are protecting against.
I set up logical replication via pg_createsubscriber first and did this:
./bin/pg_ctl -D standby -l standby stop -m fast
touch standby/recovery.signal
./bin/pg_ctl -D standby -l standby start
with restore_command = 'cp /home/postgresql-install/wal_archive/%f "%p"'

With no patch I got:
LOG: invalid record length at 0/A0000A0: expected at least 24, got 0
LOG: redo is not required
FATAL: recovery ended before configured recovery target was reached

But with patches applied I successfully started the standby.

Did I get the idea right?

Kind regards,
Ian Ilyasov.

#21Michael Paquier
michael@paquier.xyz
In reply to: Ilyasov Ian (#20)
Re: Resetting recovery target parameters in pg_createsubscriber

On Sun, Oct 05, 2025 at 10:30:53PM +0000, Ilyasov Ian wrote:

Do we need info about recovery.conf here since patch applies only to
master?

And actually, I think that you are pointing at a bug here.
pg_createsubscriber does updates of the control file but it includes
zero checks based on PG_CONTROL_VERSION to make sure that it is able
to work with a version compatible with what's on disk. The CRC check
would be reported as incorrect after calling get_controlfile(), but
it's disturbing to claim that the control file looks corrupted.

So, oops?

[.. checks ..]

The last control file update has been done in 44fe30fdab67, and
attempting to run pg_createsubscriber on a v17 cluster leads to:
$ pg_createsubscriber -D $HOME/data/5433 -P "host=/tmp port=5432" -d postgres
pg_createsubscriber: error: control file appears to be corrupt

So, yes, oops. We document that pg_cretesubscriber should have the
same major version as the source and target servers, which is good.
This error is no good, especially as checking it is just a few lines
of code, and that the take is actually PG_CONTROL_VERSION for control
file consistency.
--
Michael

#22Alena Vinter
dlaaren8@gmail.com
In reply to: Ilyasov Ian (#20)
3 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

Hi everyone,

Thank you for all the valuable feedback! I've improved the patches in the
latest version.

Based on the contents of the latest patch, we reset the parameters
after promoting the node, and primary_conninfo only matters while we
are in recovery, for a standby recovery WAL using the streaming
replication protocol.

Michael, thanks for helping! This fact simplifies the code. I put resetting
the parameters exclusively in the `atexit` callback -- this approach seems
neater to me. What do you think?

Did I get the idea right?

Ian, yes, you got it right. The core issue occurs when postgres encounters
a checkpoint during recovery, determines redo isn't needed (because there
are no records after the checkpoint), but then fails with a fatal error
because it cannot reach the specified LSN target (which is lower than the
checkpoint LSN). I reckon this is a recovery logic issue, but I also
believe the component that sets recovery parameters should be responsible
for cleaning them up when they're no longer required.

Best wishes,
Alena Vinter

Attachments:

v7-0003-doc-Add-warning-about-leftover-recovery-parameters-i.patchtext/x-patch; charset=US-ASCII; name=v7-0003-doc-Add-warning-about-leftover-recovery-parameters-i.patchDownload
From 6df46b0c9e7accd468946a891d988ad71b7121eb Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 23 Sep 2025 10:23:46 +0700
Subject: [PATCH 3/3] doc: Add warning about leftover recovery parameters in
 pg_createsubscriber

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index bb9cc72576c..fa14aeeef13 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -520,11 +520,19 @@ PostgreSQL documentation
       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.
+      recovery targets that can cause a failure.
+     </para>
+     <para>
+      If an error occurs during this step, recovery parameters might remain in
+      the target server's configuration.  You may need to manually remove these
+      leftover parameters before attempting to run.
+      <application>pg_createsubscriber</application> again.
+     </para>
+     <para>
+      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>
 
-- 
2.51.0

v7-0002-Reseting-recovery-target-parameters-in-pg_createsubs.patchtext/x-patch; charset=US-ASCII; name=v7-0002-Reseting-recovery-target-parameters-in-pg_createsubs.patchDownload
From ac319b445cf501fe0349590d9a4d2225a419d660 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 2/3] Reseting recovery target parameters in
 pg_createsubscriber

The utility sets recovery target params for correct recovery before
conversion a physical replica to a logical one but does not reset them
afterward. It may cause recovery failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system may incorrectly determine that the
recovery target was never reached because these parameters remain
active.

This change ensures all recovery parameters are properly reset after
conversion to prevent such edge cases.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 56 ++++++++++++++-----
 .../t/040_pg_createsubscriber.pl              | 31 ++++++++++
 2 files changed, 74 insertions(+), 13 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 3986882f042..dccf5dcbc87 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -154,6 +154,7 @@ static char *subscriber_dir = NULL;
 
 static bool recovery_ended = false;
 static bool standby_running = false;
+static bool recovery_params_set = false;
 
 enum WaitPMResult
 {
@@ -161,21 +162,51 @@ enum WaitPMResult
 	POSTMASTER_STILL_STARTING
 };
 
+/*
+ * Buffer to preserve the original recovery conf contents before modifying
+ * recovery parameters. This allows restoration of the original configuration
+ * after the logical replication process completes, maintaining the system's
+ * previous recovery state.
+ */
+static PQExpBuffer savedrecoveryconfcontents = NULL;
+
+/*
+ * Cached version of the subscriber server, used for version-specific behavior
+ */
+static int	sub_version = 0;
 
 /*
- * Cleanup objects that were created by pg_createsubscriber if there is an
- * error.
+ * Cleanup objects that were created by pg_createsubscriber.
  *
  * 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.
+ * There is no cleanup on the target server *after* its promotion because any
+ * failure at this point means recreate the physical replica and start again.
  */
 static void
 cleanup_objects_atexit(void)
 {
+	/*
+	 * Replace recovery config with its original contents. This ensures the
+	 * subscriber doesn't try using outdated recovery parameters.
+	 */
+	if (recovery_params_set)
+	{
+		bool		no_err = ReplaceRecoveryConfig(sub_version, subscriber_dir,
+												   savedrecoveryconfcontents);
+
+		if (!no_err)
+		{
+			bool		use_recovery_conf =
+			sub_version < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+			pg_log_warning("recovery parameters were set but not properly reset on the target");
+			pg_log_warning_hint("Manual removal of recovery parameters is required from '%s'",
+								use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+		}
+	}
+
 	if (success)
 		return;
 
@@ -1019,6 +1050,7 @@ check_subscriber(const struct LogicalRepInfo *dbinfo)
 	pg_log_info("checking settings on subscriber");
 
 	conn = connect_database(dbinfo[0].subconninfo, true);
+	sub_version = PQserverVersion(conn);
 
 	/* The target server must be a standby */
 	if (!server_is_in_recovery(conn))
@@ -1236,6 +1268,9 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo, true);
 
+	/* Before setting up the recovery parameters save the original content. */
+	savedrecoveryconfcontents = GetRecoveryConfig(conn, datadir);
+
 	/*
 	 * Write recovery parameters.
 	 *
@@ -1267,17 +1302,12 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_time = ''\n");
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_xid = ''\n");
 
-	if (dry_run)
-	{
-		appendPQExpBufferStr(recoveryconfcontents, "# dry run mode");
-		appendPQExpBuffer(recoveryconfcontents,
-						  "recovery_target_lsn = '%X/%08X'\n",
-						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
-	}
-	else
+	if (!dry_run)
 	{
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  lsn);
+
+		recovery_params_set = true;
 		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 229fef5b3b5..6dee0837ee9 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -41,6 +41,30 @@ sub generate_db
 	return $dbname;
 }
 
+# Check if a parameter is absent from the server's configuration file.
+# Uses 'recovery.conf' for PostgreSQL < 12, 'postgresql.auto.conf' for newer
+# versions.
+sub test_param_absent
+{
+	my ($node, $param) = @_;
+	my $version = $node->pg_version->major;
+	my $conf;
+
+	if ($version < 12)
+	{
+		$conf = $node->data_dir . '/recovery.conf';
+	}
+	else
+	{
+		$conf = $node->data_dir . '/postgresql.auto.conf';
+	}
+
+	return 1 unless -e $conf;
+
+	my $content = slurp_file($conf);
+	return $content !~ /^\s*$param\s*=/m;
+}
+
 #
 # Test mandatory options
 command_fails(['pg_createsubscriber'],
@@ -467,6 +491,13 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');
 
+# Verify that recovery parameters have been reset after pg_createsubscriber
+# We check recovery_target_lsn as a representative parameter - since all
+# recovery parameters are managed as a group, the absence of one indicates
+# that the entire set has been properly cleared from the configuration.
+ok( test_param_absent($node_s, 'recovery_target_lsn'),
+	'recovery_target_lsn parameter was removed');
+
 # Confirm the physical replication slot has been removed
 $result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
-- 
2.51.0

v7-0001-Implements-helper-function-in-recovery_gen.patchtext/x-patch; charset=US-ASCII; name=v7-0001-Implements-helper-function-in-recovery_gen.patchDownload
From 29d3f5f7abc730112e8770d3da84232af4754c46 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 2 Sep 2025 18:15:13 +0700
Subject: [PATCH 1/3] Implements helper function in recovery_gen

These functions support pg_createsubscriber's need to temporarily
configure recovery params and ensure proper cleanup after the conversion
to logical replication is complete.
---
 src/fe_utils/recovery_gen.c         | 113 ++++++++++++++++++++++++++++
 src/include/fe_utils/recovery_gen.h |   4 +
 2 files changed, 117 insertions(+)

diff --git a/src/fe_utils/recovery_gen.c b/src/fe_utils/recovery_gen.c
index e9023584768..e2e501ce693 100644
--- a/src/fe_utils/recovery_gen.c
+++ b/src/fe_utils/recovery_gen.c
@@ -9,7 +9,9 @@
  */
 #include "postgres_fe.h"
 
+#include "common/file_utils.h"
 #include "common/logging.h"
+#include "common/string.h"
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/string_utils.h"
 
@@ -234,3 +236,114 @@ GetDbnameFromConnectionOptions(const char *connstr)
 	PQconninfoFree(conn_opts);
 	return dbname;
 }
+
+/*
+ * GetRecoveryConfig
+ *
+ * Reads the recovery configuration file from the target server's data directory
+ * and returns its contents as a PQExpBuffer.
+ */
+PQExpBuffer
+GetRecoveryConfig(PGconn *pgconn, const char *target_dir)
+{
+	PQExpBuffer contents;
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	char	   *line;
+	bool		use_recovery_conf;
+
+	Assert(pgconn != NULL);
+
+	contents = createPQExpBuffer();
+	if (!contents)
+		pg_fatal("out of memory");
+
+	use_recovery_conf =
+		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;
+
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
+			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");
+
+	cf = fopen(filename, "r");
+	if (cf == NULL)
+		pg_fatal("could not open file \"%s\": %m", filename);
+
+	/* Read file contents line-by-line and append to the buffer */
+	while ((line = pg_get_line(cf, NULL)) != NULL)
+	{
+		appendPQExpBufferStr(contents, line);
+	}
+
+	if (ferror(cf))
+	{
+		pg_fatal("could not read from file \"%s\": %m", filename);
+	}
+
+	fclose(cf);
+
+	return contents;
+}
+
+/*
+ * ReplaceRecoveryConfig
+ *
+ * Replaces the recovery configuration file on the target server with new contents.
+ *
+ * The operation is performed atomically by writing to a temporary file first,
+ * then renaming it to the final filename.
+ *
+ * Returns false if an error occurs. In case of error, a temporary file may
+ * still be present on the server.
+ */
+bool
+ReplaceRecoveryConfig(int version, const char *target_dir, PQExpBuffer contents)
+{
+	char		tmp_filename[MAXPGPATH];
+	char		filename[MAXPGPATH];
+	FILE	   *cf;
+	const char *config_filename;
+
+	config_filename =
+		(version < MINIMUM_VERSION_FOR_RECOVERY_GUC)
+		? "recovery.conf"
+		: "postgresql.auto.conf";
+
+	/*
+	 * Construct full paths for the configuration file and its temporary
+	 * version
+	 */
+	snprintf(filename, MAXPGPATH, "%s/%s", target_dir, config_filename);
+	snprintf(tmp_filename, MAXPGPATH, "%s.tmp", filename);
+
+	/*
+	 * Open temporary file for writing. Mode "w" ensures the file is recreated
+	 * if it already exists.
+	 */
+	cf = fopen(tmp_filename, "w");
+	if (cf == NULL)
+	{
+		pg_log_error("could not open file \"%s\": %m", tmp_filename);
+		return false;
+	}
+
+	if (fwrite(contents->data, contents->len, 1, cf) != 1)
+	{
+		pg_log_error("could not write to file \"%s\": %m", tmp_filename);
+		return false;
+	}
+
+	fclose(cf);
+
+	/*
+	 * Atomically replace the old configuration file with the new one by
+	 * renaming the temporary file to the final filename.
+	 */
+	if (durable_rename(tmp_filename, filename) != 0)
+	{
+		pg_log_error("could not rename file \"%s\" to \"%s\": %m",
+					 tmp_filename, filename);
+		return false;
+	}
+
+	return true;
+}
diff --git a/src/include/fe_utils/recovery_gen.h b/src/include/fe_utils/recovery_gen.h
index c13f2263bcd..b54397db740 100644
--- a/src/include/fe_utils/recovery_gen.h
+++ b/src/include/fe_utils/recovery_gen.h
@@ -27,4 +27,8 @@ extern void WriteRecoveryConfig(PGconn *pgconn, const char *target_dir,
 								PQExpBuffer contents);
 extern char *GetDbnameFromConnectionOptions(const char *connstr);
 
+extern PQExpBuffer GetRecoveryConfig(PGconn *pgconn, const char *target_dir);
+extern bool ReplaceRecoveryConfig(int version, const char *target_dir,
+								  PQExpBuffer contents);
+
 #endif							/* RECOVERY_GEN_H */
-- 
2.51.0

#23Michael Paquier
michael@paquier.xyz
In reply to: Alena Vinter (#22)
Re: Resetting recovery target parameters in pg_createsubscriber

On Mon, Oct 06, 2025 at 01:25:12PM +0700, Alena Vinter wrote:

Michael, thanks for helping! This fact simplifies the code. I put resetting
the parameters exclusively in the `atexit` callback -- this approach seems
neater to me. What do you think?

I have been looking at this patch for a couple of hours, and I don't
really like the result, for a variety of reasons. Some of the reasons
come with the changes in recovery_gen.c themselves, as proposed in the
patch, where the only thing we want to do it replace the contents of
one file by the other, some other reasons come from the way
pg_createsubscriber complicates its life on HEAD. There is no need to
read the contents line by line and write them back, we can just do
file manipulations.

The reason why the patch does things this way currently is that it has
zero knowledge of the file location where the recovery parameters
contents are written, because this location is internal in
recovery_gen.c, at least based on how pg_createsubscriber is written.
And well, this fact is wrong even on HEAD: we know where the recovery
parameters are written because pg_createsubscriber is documented as
only supporting the same major version as the one where the tool has
been compiled. So it is pointless to call WriteRecoveryConfig() with
a connection object (using a PGconn pointer in this API is an artifact
of pg_basebackup, where we support base backups taken from older major
versions when using a newer version of the tool). pg_createsubscriber
has no need to bind to this limitation, but we don't need to improve
this point for the sake of this thread.

The proposed patch is written without taking into account this issue,
and the patch has a lot of logic that's not necessary. There is no
point in referring to recovery.conf in the code and the tests, as
well.

Anyway, a second reason why I am not cool with the patch is that the
contents written by pg_createsubscriber are entirely erased from
existence, and I see a good point in keeping a trace of them at least
for post-operation debugging purposes. With all that in mind, I came
up with the following solution, which is able to fix what you want to
address (aka not load any of the recovery parameters written by the
tool if you reactivate a standby with a new signal file), while also
satisfying my condition, which is to keep a track of the parameters
written. Hence, let's:
- forget about the changes in recovery_gen.c.
- call WriteRecoveryConfig() with only one line added in the contents
written to the "recovery" file (which is postgresql.conf.auto, okay):
include_if_exists = 'pg_createsubscriber.conf'
- Write the parameters generated by pg_createsubscriber to this new
configuration file.
- In the exit callback, call durable_rename() and rename
pg_createsubscriber.conf to a pg_createsubscriber.conf.old. There is
no need to cache the backend version or rely on a connection. We'll
unlikely see a failure. Even if there is a failure, fixing the
problem would be just to move or delete the extra file, and
documenting that is simpler.

All that points to the direction that we may not want to backpatch any
of this, considering these changes as improvements in usability.
--
Michael

#24Robert Haas
robertmhaas@gmail.com
In reply to: Michael Paquier (#23)
Re: Resetting recovery target parameters in pg_createsubscriber

On Wed, Oct 8, 2025 at 7:43 AM Michael Paquier <michael@paquier.xyz> wrote:

With all that in mind, I came
up with the following solution, which is able to fix what you want to
address (aka not load any of the recovery parameters written by the
tool if you reactivate a standby with a new signal file), while also
satisfying my condition, which is to keep a track of the parameters
written.

I'd like to back up one more step: why do we think that this is even a
valid scenario in the first place? The original scenario involves
running pg_createsubscriber and then putting the server back into
recovery mode. But why is it valid to just put the server back into
recovery mode at that point? That doesn't seem like something that you
can just go do and expect it to work, especially if you don't check
that other parameters have the values that you want. Generally,
recovery is a one-time event, and once you exit, you only reenter on a
newly-taken backup or after a crash or a pg_rewind. There are, of
course, other times when you can force a server back into recovery
without anything bad happening, but it's not my impression that we
support that in general; it's something you can choose to do as an
expert operator if you are certain that it's OK in your scenario.

So my question is: why should we do anything at all about this?

--
Robert Haas
EDB: http://www.enterprisedb.com

#25Alena Vinter
dlaaren8@gmail.com
In reply to: Robert Haas (#24)
2 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

Hi everyone,

Michael, thank you for outlining your alternative approach.
After rethinking the current patch state with a clearer vision, I realized
that simply truncating the postgresql.auto.conf file is sufficient. All
modifications made by pg_createsubscriber in this file are append-only, so
truncation reliably restores it to its original state without adding extra
logic. This keeps the patch small and clean.

For older versions using recovery.conf, the situation differs — since that
file is fully rewritten during recovery setup, we instead restore the
previously saved original file using a durable rename.

Regarding debugging: the contents are not entirely lost. pg_createsubscriber
already prints the new recovery configuration as debug output, so the full
parameter set remains visible in the logs for inspection when needed. My
point is that adding include directives isn't needed, as we already have
debug output, and, moreover, they aren't applied to recovery.conf.

Robert, this scenario actually occurred in production at one of our
customer environments. Even though this workflow may be uncommon,
PostgreSQL should still handle it gracefully. The fact that the server can
end up in a state where it cannot start because it fails to reach a
recovery target point far in the past suggests a potential area for
improvement in the recovery process. It would be helpful if the system
could detect such a case — where the recovery target precedes the current
consistent point — and either skip recovery or emit a clear diagnostic
message rather than failing to start.

Best regards,
Vinter Alena

Attachments:

v8-0001-Reseting-recovery-target-parameters-in-pg_createsubs.patchtext/x-patch; charset=US-ASCII; name=v8-0001-Reseting-recovery-target-parameters-in-pg_createsubs.patchDownload
From e156a12d97589d53682bbe3dbff7b1608789cf54 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Mon, 27 Oct 2025 13:02:02 +0700
Subject: [PATCH 1/2] Reseting recovery target parameters in
 `pg_createsubscriber`

The utility sets recovery target params for correct recovery before
conversion a physical replica to a logical one but does not reset them
afterward. It may cause recovery failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system may incorrectly determine that the
recovery target was never reached because these parameters remain
active.

This change ensures all recovery parameters are properly reset after
conversion to prevent such edge cases.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 100 +++++++++++++++---
 .../t/040_pg_createsubscriber.pl              |  31 ++++++
 2 files changed, 115 insertions(+), 16 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 9fa5caaf91d..c5a8997e5db 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -12,6 +12,7 @@
  */
 
 #include "postgres_fe.h"
+#include "utils/elog.h"
 
 #include <sys/stat.h>
 #include <sys/time.h>
@@ -28,9 +29,11 @@
 #include "fe_utils/string_utils.h"
 #include "fe_utils/version.h"
 #include "getopt_long.h"
+#include "storage/fd.h"
 
 #define	DEFAULT_SUB_PORT	"50432"
 #define	OBJECTTYPE_PUBLICATIONS  0x0001
+#define RECOVERY_CONF_PREFIX ".original"
 
 /* Command-line options */
 struct CreateSubscriberOptions
@@ -155,6 +158,11 @@ static char *subscriber_dir = NULL;
 
 static bool recovery_ended = false;
 static bool standby_running = false;
+static bool recovery_params_set = false;
+
+static const char *recovery_conf_filename = NULL;	/* cached recovery conf
+													 * filename */
+static off_t recovery_conf_size = 0;	/* size of original recovery conf file */
 
 enum WaitPMResult
 {
@@ -164,19 +172,59 @@ enum WaitPMResult
 
 
 /*
- * Cleanup objects that were created by pg_createsubscriber if there is an
- * error.
+ * Clean up objects created by pg_createsubscriber.
  *
- * 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.
+ * Publications and replication slots are created on the primary. Depending on
+ * the step where it failed, already created objects should be removed if
+ * possible (sometimes this won't work due to a connection issue).
+ * There is no cleanup on the target server *after* its promotion, because any
+ * failure at this point means recreating the physical replica and starting
+ * again.
  */
 static void
 cleanup_objects_atexit(void)
 {
+	/*
+	 * Restore the recovery config to its original contents, ensuring that the
+	 * subscriber doesn't use outdated recovery parameters.
+	 */
+	if (recovery_params_set)
+	{
+		int			err = 0;
+		char		filename[MAXPGPATH];
+
+		snprintf(filename, MAXPGPATH, "%s/%s", subscriber_dir, recovery_conf_filename);
+
+		/*
+		 * 'recovery.conf' are fully rewritten during the 'recovery_setup'
+		 * function, so we need to restore saved original contents
+		 */
+		if (strcmp(recovery_conf_filename, "recovery.conf") == 0)
+		{
+			char		filename_with_original_contents[MAXPGPATH];
+
+			snprintf(filename_with_original_contents, MAXPGPATH,
+					 "%s" RECOVERY_CONF_PREFIX, filename);
+
+			err = durable_rename(filename_with_original_contents, filename, ERROR);
+		}
+		else					/* postgresql.auto.conf */
+		{
+			if ((err = truncate(filename, recovery_conf_size)) != 0)
+			{
+				pg_log_error("could not truncate file \"%s\" to %u: %m",
+							 filename, (unsigned int) recovery_conf_size);
+			}
+		}
+
+		if (err != 0)
+		{
+			pg_log_warning("recovery parameters were set but not properly reset on the target");
+			pg_log_warning_hint("manual removal of recovery parameters is required from '%s'",
+								recovery_conf_filename);
+		}
+	}
+
 	if (success)
 		return;
 
@@ -1025,6 +1073,9 @@ check_subscriber(const struct LogicalRepInfo *dbinfo)
 	pg_log_info("checking settings on subscriber");
 
 	conn = connect_database(dbinfo[0].subconninfo, true);
+	recovery_conf_filename =
+		PQserverVersion(conn) < MINIMUM_VERSION_FOR_RECOVERY_GUC ?
+		"recovery.conf" : "postgresql.auto.conf";
 
 	/* The target server must be a standby */
 	if (!server_is_in_recovery(conn))
@@ -1234,6 +1285,8 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 {
 	PGconn	   *conn;
 	PQExpBuffer recoveryconfcontents;
+	char		filename[MAXPGPATH];
+	struct stat st;
 
 	/*
 	 * Despite of the recovery parameters will be written to the subscriber,
@@ -1242,6 +1295,16 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo, true);
 
+	/*
+	 * Before setting up the recovery parameters save the original size for
+	 * restoring later.
+	 */
+	snprintf(filename, MAXPGPATH, "%s/%s", datadir, recovery_conf_filename);
+	if (stat(filename, &st) < 0)
+		pg_fatal("could not stat file \"%s\": %m", filename);
+
+	recovery_conf_size = st.st_size;
+
 	/*
 	 * Write recovery parameters.
 	 *
@@ -1273,17 +1336,22 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_time = ''\n");
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_xid = ''\n");
 
-	if (dry_run)
-	{
-		appendPQExpBufferStr(recoveryconfcontents, "# dry run mode");
-		appendPQExpBuffer(recoveryconfcontents,
-						  "recovery_target_lsn = '%X/%08X'\n",
-						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
-	}
-	else
+	if (!dry_run)
 	{
+		if (strcmp(recovery_conf_filename, "recovery.conf") == 0)
+		{
+			char		filename_with_original_contents[MAXPGPATH];
+
+			snprintf(filename_with_original_contents, MAXPGPATH,
+					 "%s" RECOVERY_CONF_PREFIX, filename);
+
+			durable_rename(filename, filename_with_original_contents, FATAL);
+		}
+
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  lsn);
+
+		recovery_params_set = true;
 		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 1e887a0657a..c108747a9db 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -41,6 +41,30 @@ sub generate_db
 	return $dbname;
 }
 
+# Check if a parameter is absent from the server's configuration file.
+# Uses 'recovery.conf' for PostgreSQL < 12, 'postgresql.auto.conf' for newer
+# versions.
+sub test_param_absent
+{
+	my ($node, $param) = @_;
+	my $version = $node->pg_version->major;
+	my $conf;
+
+	if ($version < 12)
+	{
+		$conf = $node->data_dir . '/recovery.conf';
+	}
+	else
+	{
+		$conf = $node->data_dir . '/postgresql.auto.conf';
+	}
+
+	return 1 unless -e $conf;
+
+	my $content = slurp_file($conf);
+	return $content !~ /^\s*$param\s*=/m;
+}
+
 #
 # Test mandatory options
 command_fails(['pg_createsubscriber'],
@@ -467,6 +491,13 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');
 
+# Verify that recovery parameters have been reset after pg_createsubscriber
+# We check recovery_target_lsn as a representative parameter - since all
+# recovery parameters are managed as a group, the absence of one indicates
+# that the entire set has been properly cleared from the configuration.
+ok( test_param_absent($node_s, 'recovery_target_lsn'),
+	'recovery_target_lsn parameter was removed');
+
 # Confirm the physical replication slot has been removed
 $result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
-- 
2.51.0

v8-0002-doc-Add-warning-about-leftover-recovery-parameters-i.patchtext/x-patch; charset=US-ASCII; name=v8-0002-doc-Add-warning-about-leftover-recovery-parameters-i.patchDownload
From c755b53aeb8061f85308b1f57f028579ea481cf7 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Mon, 27 Oct 2025 13:02:51 +0700
Subject: [PATCH 2/2] doc: Add warning about leftover recovery parameters in
 'pg_createsubscriber'

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index bb9cc72576c..fa14aeeef13 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -520,11 +520,19 @@ PostgreSQL documentation
       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.
+      recovery targets that can cause a failure.
+     </para>
+     <para>
+      If an error occurs during this step, recovery parameters might remain in
+      the target server's configuration.  You may need to manually remove these
+      leftover parameters before attempting to run.
+      <application>pg_createsubscriber</application> again.
+     </para>
+     <para>
+      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>
 
-- 
2.51.0

#26Robert Haas
robertmhaas@gmail.com
In reply to: Alena Vinter (#25)
Re: Resetting recovery target parameters in pg_createsubscriber

On Mon, Oct 27, 2025 at 8:22 AM Alena Vinter <dlaaren8@gmail.com> wrote:

Robert, this scenario actually occurred in production at one of our customer environments. Even though this workflow may be uncommon, PostgreSQL should still handle it gracefully. The fact that the server can end up in a state where it cannot start because it fails to reach a recovery target point far in the past suggests a potential area for improvement in the recovery process. It would be helpful if the system could detect such a case — where the recovery target precedes the current consistent point — and either skip recovery or emit a clear diagnostic message rather than failing to start.

The question isn't whether the workflow is common. If something is
broken, we should ideally fix it even if we don't believe that it is
very likely to occur. The question is whether the workflow is
something that a user can reasonably expect to work. If you remove all
of your data files and then complain that the database doesn't work
any more, that's not an indication of a problem with PostgreSQL, but
rather an indication that it's not a good idea to remove all of your
data files. More generally, if you make manual changes to the data
directory and the results are unsatisfactory, we generally consider
that to be an error on your part rather than a problem with
PostgreSQL. You can of course edit configuration files like
postgresql.conf or pg_hba.conf and expect things to work as long as
the resulting configuration file is still valid, but you can't
manually modify pg_control on disk and then call it a bug when
something goes wrong.

In the case at hand, you've offered no justification for why it's OK
to put the server back into recovery at all -- normally, you only put
a server in recovery if you're spinning it up from a backup, which is
not the case in this scenario. I don't really understand why that
would be a valid thing to do, and I even less understand why you
should expect to be able to do it without checking the recovery
configuration and still have things work.

--
Robert Haas
EDB: http://www.enterprisedb.com

#27Alexander Korotkov
aekorotkov@gmail.com
In reply to: Robert Haas (#26)
Re: Resetting recovery target parameters in pg_createsubscriber

On Wed, Oct 29, 2025 at 9:47 PM Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, Oct 27, 2025 at 8:22 AM Alena Vinter <dlaaren8@gmail.com> wrote:

Robert, this scenario actually occurred in production at one of our customer environments. Even though this workflow may be uncommon, PostgreSQL should still handle it gracefully. The fact that the server can end up in a state where it cannot start because it fails to reach a recovery target point far in the past suggests a potential area for improvement in the recovery process. It would be helpful if the system could detect such a case — where the recovery target precedes the current consistent point — and either skip recovery or emit a clear diagnostic message rather than failing to start.

The question isn't whether the workflow is common. If something is
broken, we should ideally fix it even if we don't believe that it is
very likely to occur. The question is whether the workflow is
something that a user can reasonably expect to work. If you remove all
of your data files and then complain that the database doesn't work
any more, that's not an indication of a problem with PostgreSQL, but
rather an indication that it's not a good idea to remove all of your
data files. More generally, if you make manual changes to the data
directory and the results are unsatisfactory, we generally consider
that to be an error on your part rather than a problem with
PostgreSQL. You can of course edit configuration files like
postgresql.conf or pg_hba.conf and expect things to work as long as
the resulting configuration file is still valid, but you can't
manually modify pg_control on disk and then call it a bug when
something goes wrong.

In the case at hand, you've offered no justification for why it's OK
to put the server back into recovery at all -- normally, you only put
a server in recovery if you're spinning it up from a backup, which is
not the case in this scenario. I don't really understand why that
would be a valid thing to do, and I even less understand why you
should expect to be able to do it without checking the recovery
configuration and still have things work.

Can we see this from the different prospective? pg_createsubscriber
is intended to turn physical replica into a logical replica. And it
leaves subscriber database cluster with rudimentary configuration
options needed purely for its intermediate step. Whatever usage
scenario is, user might deleted these extra options if needed. This
is not a big deal. However, it's certainly cleaner for
pg_createsubscriber to avoid leaving this options (especially if their
appearance is not documented).

------
Regards,
Alexander Korotkov
Supabase

#28Robert Haas
robertmhaas@gmail.com
In reply to: Alexander Korotkov (#27)
Re: Resetting recovery target parameters in pg_createsubscriber

On Thu, Oct 30, 2025 at 7:08 PM Alexander Korotkov <aekorotkov@gmail.com> wrote:

Can we see this from the different prospective? pg_createsubscriber
is intended to turn physical replica into a logical replica. And it
leaves subscriber database cluster with rudimentary configuration
options needed purely for its intermediate step. Whatever usage
scenario is, user might deleted these extra options if needed. This
is not a big deal. However, it's certainly cleaner for
pg_createsubscriber to avoid leaving this options (especially if their
appearance is not documented).

I think that's a fair point. Michael's proposal doesn't sound too bad
to me from that point of view, as it seems to reduce the amount of
machinery that we need to introduce in order to accomplish the goal of
not leaving configuration behind. It's a little unfortunate that
pg_createsubscriber needs to modify the configuration file at all,
rather than say passing flags through on the PostgreSQL command line.
However, doing it that way might make the code a lot more complicated,
and it's not worth a lot of code complexity to avoid leaving recovery
parameters behind given that, as you can say, the user can always just
delete them. Also, to your point, the fact that they will be added
really ought to be documented.

But what I don't want to us to do is write a commit message, or add
tests, that imply that the scenario so far proposed is a reasonable
one. What we were told is that the server is put back into recovery
just after being made into a primary, which seems clearly nonsensical.
You might think of using pg_rewind to put a server back into recovery
after a failover, but in that situation, there is at least 1 standby
following the primary, and you're trying to get them to switch roles.
But here, just after running pg_subscriber, there's no standby yet. If
you put the one and only server with that system ID back into
recovery, there's no other server it can possibly follow, so what's
the point?

Maybe what we could use a scenario is one where the node created by
pg_createsubscriber continues to run as a primary, and eventually
someone creates a standby from it which also inherits its physical
replication configuration and then that cause some kind of problem.
Now, to be fair, that scenario could also happen without using
pg_createsubscriber at all: you could spin up a new primary via PITR,
for example, and that configuration could propagate to standbys
created from the primary and then cause problems, and as far as I know
we do nothing to protect against that. That doesn't mean that we can't
try to protect against this, but I do think it needs to be justified
by reference to a sequence of actions that would be reasonable to
perform on unpatched PostgreSQL. Otherwise, I think we'll be adding to
future confusion in an area that can already be extremely confusing.

--
Robert Haas
EDB: http://www.enterprisedb.com

#29Alexander Korotkov
aekorotkov@gmail.com
In reply to: Alena Vinter (#25)
Re: Resetting recovery target parameters in pg_createsubscriber

On Mon, Oct 27, 2025 at 2:22 PM Alena Vinter <dlaaren8@gmail.com> wrote:

Michael, thank you for outlining your alternative approach.
After rethinking the current patch state with a clearer vision, I realized that simply truncating the postgresql.auto.conf file is sufficient. All modifications made by pg_createsubscriber in this file are append-only, so truncation reliably restores it to its original state without adding extra logic. This keeps the patch small and clean.

For older versions using recovery.conf, the situation differs — since that file is fully rewritten during recovery setup, we instead restore the previously saved original file using a durable rename.

Regarding debugging: the contents are not entirely lost. pg_createsubscriber already prints the new recovery configuration as debug output, so the full parameter set remains visible in the logs for inspection when needed. My point is that adding include directives isn't needed, as we already have debug output, and, moreover, they aren't applied to recovery.conf.

I have rechecked this. It appears that pg_createsubscriber writes the
recovery configuration to the output and only in verbose mode. So,
it's far no guaranteed that this information would be accessible. One
may run pg_createsubscriber not in verbose mode or don't save its
output. I suggest we should re-implement this in a way Michael
proposed [1]: save the configuration to pg_createsubscriber.conf.old
file.

Links.
1. /messages/by-id/aOZOJ8p8LEcw0SpH@paquier.xyz

------
Regards,
Alexander Korotkov
Supabase

#30Alena Vinter
dlaaren8@gmail.com
In reply to: Alexander Korotkov (#29)
Re: Resetting recovery target parameters in pg_createsubscriber

On Tue, 4 Nov 2025 at 04:28, Alexander Korotkov <aekorotkov@gmail.com>
wrote:

I have rechecked this. It appears that pg_createsubscriber writes the
recovery configuration to the output and only in verbose mode. So,
it's far no guaranteed that this information would be accessible. One
may run pg_createsubscriber not in verbose mode or don't save its
output. I suggest we should re-implement this in a way Michael
proposed [1]: save the configuration to pg_createsubscriber.conf.old
file.

Alexander, I'm not in favor of saving additional files. This approach seems
to replace one type of leftover artifact (recovery params) with another
(debug-files). Neither option is good.
As Michael pointed out, the parameters might be useful for post-debugging
purposes. This suggests to me that they are, by nature, debugging
information. Therefore, it seems appropriate that they should be captured
by the verbose/debug mode. If verbose mode isn't used, we lose more than
just the recovery parameters — we also lose the sequence of commands for
managing replication slots and other steps. Following this logic, why not
save all information in non-verbose mode that might be used for debugging?

Robert, I'll think more about a valid scenario (including the one you
proposed) and get back with results later.

---
Regards,
Alena Vinter

#31Robert Haas
robertmhaas@gmail.com
In reply to: Alexander Korotkov (#9)
Re: Resetting recovery target parameters in pg_createsubscriber

On Thu, Nov 13, 2025 at 7:46 AM Alena Vinter <dlaaren8@gmail.com> wrote:

Robert, here's a realistic scenario where the issue occurs:
1. Start with a primary and physical standby
2. Convert the standby to a logical replica using `pg_createsubscriber`
3. Later, create a new physical standby from a backup of this logical replica
4. The new standby fails to start because it cannot reach consistency point

The root cause is that `pg_createsubscriber` leaves behind recovery parameters that interfere with the new standby's startup process, causing recovery to stop before reaching a consistency point.

Yes, I agree this is a much better scenario to test. Thanks.

--
Robert Haas
EDB: http://www.enterprisedb.com

#32Alena Vinter
dlaaren8@gmail.com
In reply to: Alexander Korotkov (#9)
3 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

It seems my previous message isn't visible in the thread. I can see
Robert's reply, but not the original message he was responding to. I'm
resending the message and all attachments to ensure it's saved in the
thread.

On Thu, 13 Nov 2025 at 19:45, Alena Vinter <dlaaren8@gmail.com> wrote:

Show quoted text

Hi everyone!

Robert, here's a realistic scenario where the issue occurs:
1. Start with a primary and physical standby
2. Convert the standby to a logical replica using `pg_createsubscriber`
3. Later, create a new physical standby from a backup of this logical
replica
4. The new standby fails to start because it cannot reach consistency point

The root cause is that `pg_createsubscriber` leaves behind recovery
parameters that interfere with the new standby's startup process, causing
recovery to stop before reaching a consistency point.

To demonstrate this, I've expanded the existing '
040_pg_createsubscriber.pl' test to include this scenario. I've also
attached a standalone TAP test below for easier verification of the
specific failure case.

---
Regards,
Alena Vinter

Attachments:

041_node_from_backup_after_pg_createsubscriber.plapplication/x-perl; name=041_node_from_backup_after_pg_createsubscriber.plDownload
v8-0002-doc-Add-warning-about-leftover-recovery-parameters-i.patchtext/x-patch; charset=US-ASCII; name=v8-0002-doc-Add-warning-about-leftover-recovery-parameters-i.patchDownload
From 4240b48b46e5b8890f62d6461417bef91eac6bad Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Mon, 27 Oct 2025 13:02:51 +0700
Subject: [PATCH 2/2] doc: Add warning about leftover recovery parameters in
 'pg_createsubscriber'

---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index bb9cc72576c..fa14aeeef13 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -520,11 +520,19 @@ PostgreSQL documentation
       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.
+      recovery targets that can cause a failure.
+     </para>
+     <para>
+      If an error occurs during this step, recovery parameters might remain in
+      the target server's configuration.  You may need to manually remove these
+      leftover parameters before attempting to run.
+      <application>pg_createsubscriber</application> again.
+     </para>
+     <para>
+      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>
 
-- 
2.51.1

v8-0001-Reseting-recovery-target-parameters-in-pg_createsubs.patchtext/x-patch; charset=US-ASCII; name=v8-0001-Reseting-recovery-target-parameters-in-pg_createsubs.patchDownload
From 44f58736b59064aaf2d7c27910165e732135700d Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Mon, 27 Oct 2025 13:02:02 +0700
Subject: [PATCH 1/2] Reseting recovery target parameters in
 `pg_createsubscriber`

The utility sets recovery target params for correct recovery before
conversion a physical replica to a logical one but does not reset them
afterward. It may cause recovery failures in certain scenarios.
For example, if recovery begins from a checkpoint where no WAL records
need to be applied, the system may incorrectly determine that the
recovery target was never reached because these parameters remain
active.

This change ensures all recovery parameters are properly reset after
conversion to prevent such edge cases.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 100 +++++++++++++++---
 .../t/040_pg_createsubscriber.pl              |  64 +++++++++++
 2 files changed, 148 insertions(+), 16 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index df41836e70f..012660e718e 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -29,9 +29,12 @@
 #include "fe_utils/string_utils.h"
 #include "fe_utils/version.h"
 #include "getopt_long.h"
+#include "storage/fd.h"
+#include "utils/elog.h"
 
 #define	DEFAULT_SUB_PORT	"50432"
 #define	OBJECTTYPE_PUBLICATIONS  0x0001
+#define RECOVERY_CONF_PREFIX ".original"
 
 /* Command-line options */
 struct CreateSubscriberOptions
@@ -155,22 +158,67 @@ static char *subscriber_dir = NULL;
 
 static bool recovery_ended = false;
 static bool standby_running = false;
+static bool recovery_params_set = false;
+
+static const char *recovery_conf_filename = NULL;	/* cached recovery conf
+													 * filename */
+static off_t recovery_conf_size = 0;	/* size of original recovery conf file */
 
 
 /*
- * Cleanup objects that were created by pg_createsubscriber if there is an
- * error.
+ * Clean up objects created by pg_createsubscriber.
  *
- * 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.
+ * Publications and replication slots are created on the primary. Depending on
+ * the step where it failed, already created objects should be removed if
+ * possible (sometimes this won't work due to a connection issue).
+ * There is no cleanup on the target server *after* its promotion, because any
+ * failure at this point means recreating the physical replica and starting
+ * again.
  */
 static void
 cleanup_objects_atexit(void)
 {
+	/*
+	 * Restore the recovery config to its original contents, ensuring that the
+	 * subscriber doesn't use outdated recovery parameters.
+	 */
+	if (recovery_params_set)
+	{
+		int			err = 0;
+		char		filename[MAXPGPATH];
+
+		snprintf(filename, MAXPGPATH, "%s/%s", subscriber_dir, recovery_conf_filename);
+
+		/*
+		 * 'recovery.conf' are fully rewritten during the 'recovery_setup'
+		 * function, so we need to restore saved original contents
+		 */
+		if (strcmp(recovery_conf_filename, "recovery.conf") == 0)
+		{
+			char		filename_with_original_contents[MAXPGPATH];
+
+			snprintf(filename_with_original_contents, MAXPGPATH,
+					 "%s" RECOVERY_CONF_PREFIX, filename);
+
+			err = durable_rename(filename_with_original_contents, filename, ERROR);
+		}
+		else					/* postgresql.auto.conf */
+		{
+			if ((err = truncate(filename, recovery_conf_size)) != 0)
+			{
+				pg_log_error("could not truncate file \"%s\" to %u: %m",
+							 filename, (unsigned int) recovery_conf_size);
+			}
+		}
+
+		if (err != 0)
+		{
+			pg_log_warning("recovery parameters were set but not properly reset on the target");
+			pg_log_warning_hint("manual removal of recovery parameters is required from '%s'",
+								recovery_conf_filename);
+		}
+	}
+
 	if (success)
 		return;
 
@@ -1026,6 +1074,9 @@ check_subscriber(const struct LogicalRepInfo *dbinfo)
 	pg_log_info("checking settings on subscriber");
 
 	conn = connect_database(dbinfo[0].subconninfo, true);
+	recovery_conf_filename =
+		PQserverVersion(conn) < MINIMUM_VERSION_FOR_RECOVERY_GUC ?
+		"recovery.conf" : "postgresql.auto.conf";
 
 	/* The target server must be a standby */
 	if (!server_is_in_recovery(conn))
@@ -1238,6 +1289,8 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 {
 	PGconn	   *conn;
 	PQExpBuffer recoveryconfcontents;
+	char		filename[MAXPGPATH];
+	struct stat st;
 
 	/*
 	 * Despite of the recovery parameters will be written to the subscriber,
@@ -1246,6 +1299,16 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 */
 	conn = connect_database(dbinfo[0].pubconninfo, true);
 
+	/*
+	 * Before setting up the recovery parameters save the original size for
+	 * restoring later.
+	 */
+	snprintf(filename, MAXPGPATH, "%s/%s", datadir, recovery_conf_filename);
+	if (stat(filename, &st) < 0)
+		pg_fatal("could not stat file \"%s\": %m", filename);
+
+	recovery_conf_size = st.st_size;
+
 	/*
 	 * Write recovery parameters.
 	 *
@@ -1277,17 +1340,22 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_time = ''\n");
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_xid = ''\n");
 
-	if (dry_run)
-	{
-		appendPQExpBufferStr(recoveryconfcontents, "# dry run mode\n");
-		appendPQExpBuffer(recoveryconfcontents,
-						  "recovery_target_lsn = '%X/%08X'\n",
-						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
-	}
-	else
+	if (!dry_run)
 	{
+		if (strcmp(recovery_conf_filename, "recovery.conf") == 0)
+		{
+			char		filename_with_original_contents[MAXPGPATH];
+
+			snprintf(filename_with_original_contents, MAXPGPATH,
+					 "%s" RECOVERY_CONF_PREFIX, filename);
+
+			durable_rename(filename, filename_with_original_contents, FATAL);
+		}
+
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
 						  lsn);
+
+		recovery_params_set = true;
 		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 3d6086dc489..c25d5c9822c 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -41,6 +41,30 @@ sub generate_db
 	return $dbname;
 }
 
+# Check if a parameter is absent from the server's configuration file.
+# Uses 'recovery.conf' for PostgreSQL < 12, 'postgresql.auto.conf' for newer
+# versions.
+sub test_param_absent
+{
+	my ($node, $param) = @_;
+	my $version = $node->pg_version->major;
+	my $conf;
+
+	if ($version < 12)
+	{
+		$conf = $node->data_dir . '/recovery.conf';
+	}
+	else
+	{
+		$conf = $node->data_dir . '/postgresql.auto.conf';
+	}
+
+	return 1 unless -e $conf;
+
+	my $content = slurp_file($conf);
+	return $content !~ /^\s*$param\s*=/m;
+}
+
 #
 # Test mandatory options
 command_fails(['pg_createsubscriber'],
@@ -160,6 +184,7 @@ primary_slot_name = '$slotname'
 primary_conninfo = '$pconnstr dbname=postgres'
 hot_standby_feedback = on
 ]);
+my $sconnstr = $node_s->connstr;
 $node_s->set_standby_mode();
 $node_s->start;
 
@@ -467,6 +492,13 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');
 
+# Verify that recovery parameters have been reset after pg_createsubscriber
+# We check recovery_target_lsn as a representative parameter - since all
+# recovery parameters are managed as a group, the absence of one indicates
+# that the entire set has been properly cleared from the configuration.
+ok( test_param_absent($node_s, 'recovery_target_lsn'),
+	'recovery_target_lsn parameter was removed');
+
 # Confirm the physical replication slot has been removed
 $result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
@@ -537,10 +569,42 @@ my $sysid_s = $node_s->safe_psql('postgres',
 	'SELECT system_identifier FROM pg_control_system()');
 isnt($sysid_p, $sysid_s, 'system identifier was changed');
 
+# Create a physical standby from the promoted subscriber
+$node_s->safe_psql('postgres',
+    "SELECT pg_create_physical_replication_slot('$slotname');");
+
+# Create backup from promoted subscriber
+$node_s->backup('backup_3');
+
+# Initialize new physical standby
+my $node_k = PostgreSQL::Test::Cluster->new('node_k');
+$node_k->init_from_backup($node_s, 'backup_3',
+    has_streaming => 1);
+
+# Configure the new standby
+$node_k->append_conf('postgresql.conf', qq[
+primary_slot_name = '$slotname'
+primary_conninfo = '$sconnstr dbname=postgres'
+hot_standby_feedback = on
+]);
+
+$node_k->set_standby_mode();
+my $node_k_name = $node_s->name;
+command_ok(
+	[
+		'pg_ctl', '--wait',
+		'--pgdata' => $node_k->data_dir,
+		'--log' => $node_k->logfile,
+		'--options' => "--cluster-name=$node_k_name",
+		'start'
+	],
+	"node_k has started");
+
 # clean up
 $node_p->teardown_node;
 $node_s->teardown_node;
 $node_t->teardown_node;
 $node_f->teardown_node;
+$node_k->teardown_node;
 
 done_testing();
-- 
2.51.1

#33Michael Paquier
michael@paquier.xyz
In reply to: Alena Vinter (#32)
Re: Resetting recovery target parameters in pg_createsubscriber

On Tue, Nov 25, 2025 at 01:45:56PM +0700, Alena Vinter wrote:

It seems my previous message isn't visible in the thread. I can see
Robert's reply, but not the original message he was responding to. I'm
resending the message and all attachments to ensure it's saved in the
thread.

I don't understand the fuzziness of the patch around recovery.conf.
Why would that be required knowing that pg_createsubscriber only
supports the same major version as the target server? We have dropped
support for recovery.conf a long time ago, and this includes all
stable branches.

FWIW, based on the concerns I'm grabbing, nothing presented on this
thread would be candidate material for a backpatch. Just mentioning
in passing.
--
Michael

#34Michael Paquier
michael@paquier.xyz
In reply to: Robert Haas (#31)
Re: Resetting recovery target parameters in pg_createsubscriber

On Wed, Nov 19, 2025 at 02:49:29PM -0500, Robert Haas wrote:

On Thu, Nov 13, 2025 at 7:46 AM Alena Vinter <dlaaren8@gmail.com> wrote:

The root cause is that `pg_createsubscriber` leaves behind recovery
parameters that interfere with the new standby's startup process,
causing recovery to stop before reaching a consistency point.

Yes, I agree this is a much better scenario to test. Thanks.

Robert, does this line of arguments address the concerns you had
upthread? Or do you still see a reason why not cleaning up these
recovery parameters would not be a good idea?

I don't see a strong need for a backpatch here as the approach I have
mentioned with a secondary configuration file, and an
include_if_exists would be a kind of design change for the tool
itself. So this would qualify as a HEAD-only thing, if of course
you wouldd agree with the change to clean up the recovery parameters.
--
Michael

#35Robert Haas
robertmhaas@gmail.com
In reply to: Michael Paquier (#34)
Re: Resetting recovery target parameters in pg_createsubscriber

On Wed, Dec 3, 2025 at 12:11 AM Michael Paquier <michael@paquier.xyz> wrote:

Robert, does this line of arguments address the concerns you had
upthread? Or do you still see a reason why not cleaning up these
recovery parameters would not be a good idea?

I don't see a strong need for a backpatch here as the approach I have
mentioned with a secondary configuration file, and an
include_if_exists would be a kind of design change for the tool
itself. So this would qualify as a HEAD-only thing, if of course
you wouldd agree with the change to clean up the recovery parameters.

As I see it, there's no bug here, because I don't think we've made any
promise to the user that they don't need to check what recovery
parameters are configured before running recovery. However, your
proposal seems like it could make life more convenient for users,
which seems like a good thing to do. The revised test case doesn't, in
my opinion, represent a scenario that absolutely has to work without
user intervention, but it's nicer if it does. So, I would see
committing this as a feature improvement but not back-patching it as a
reasonable way forward.

--
Robert Haas
EDB: http://www.enterprisedb.com

#36Andrey Rudometov
unlimitedhikari@gmail.com
In reply to: Alena Vinter (#32)
1 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

Hello Alena!

While reading through your patch, i noticed backend's include of
+#include "storage/fd.h"

which, as I understand, is needed for
+err = durable_rename(filename_with_original_contents, filename, ERROR);
+durable_rename(filename, filename_with_original_contents, FATAL);

Postgres' source code actually has two different durable_rename
signatures - one for frontend, and one for backend. As
pg_createsubscriber uses frontend definitions, I suggest replacing
this durable_rename with one from common/file_utils.h.

A patch with my approach to replacement is in attachments
(to be applied after yours).

p.s. I wonder why this builds at all - as far as I have tried, using
BE in FE usually leads to a ton of compilation errors or, at least,
warnings treated as errors - p.e, in this case backend definition
uses ereport(), which does not work in frontend utilities.

--
best regards,
Andrey Rudometov

Attachments:

v1-0001-Change-createsubscriber-s-durable_rename-to-fe.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Change-createsubscriber-s-durable_rename-to-fe.patchDownload
From a61a4773d612df945e7169e738fc6ccd35eb76d9 Mon Sep 17 00:00:00 2001
From: Andrey Rudometov <unlimitedhikari@gmail.com>
Date: Thu, 11 Dec 2025 12:13:11 +0700
Subject: [PATCH] Change createsubscriber's durable_rename to fe

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 85cfafbc069..ecb25747c3c 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -29,8 +29,7 @@
 #include "fe_utils/string_utils.h"
 #include "fe_utils/version.h"
 #include "getopt_long.h"
-#include "storage/fd.h"
-#include "utils/elog.h"
+#include "common/file_utils.h"
 
 #define	DEFAULT_SUB_PORT	"50432"
 #define	OBJECTTYPE_PUBLICATIONS  0x0001
@@ -200,7 +199,7 @@ cleanup_objects_atexit(void)
 			snprintf(filename_with_original_contents, MAXPGPATH,
 					 "%s" RECOVERY_CONF_PREFIX, filename);
 
-			err = durable_rename(filename_with_original_contents, filename, ERROR);
+			err = durable_rename(filename_with_original_contents, filename);
 		}
 		else					/* postgresql.auto.conf */
 		{
@@ -1345,11 +1344,14 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 		if (strcmp(recovery_conf_filename, "recovery.conf") == 0)
 		{
 			char		filename_with_original_contents[MAXPGPATH];
+			int			err = 0;
 
 			snprintf(filename_with_original_contents, MAXPGPATH,
 					 "%s" RECOVERY_CONF_PREFIX, filename);
 
-			durable_rename(filename, filename_with_original_contents, FATAL);
+			err = durable_rename(filename, filename_with_original_contents);
+			if (err)
+				pg_fatal("could not rename file \"%s\"", filename);
 		}
 
 		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
-- 
2.43.0

#37Michael Paquier
michael@paquier.xyz
In reply to: Andrey Rudometov (#36)
Re: Resetting recovery target parameters in pg_createsubscriber

On Sat, Dec 13, 2025 at 12:07:35PM +0700, Andrey Rudometov wrote:

A patch with my approach to replacement is in attachments
(to be applied after yours).

p.s. I wonder why this builds at all - as far as I have tried, using
BE in FE usually leads to a ton of compilation errors or, at least,
warnings treated as errors - p.e, in this case backend definition
uses ereport(), which does not work in frontend utilities.

Yep, that's indeed something that the patch should not do at all.

-			durable_rename(filename, filename_with_original_contents, FATAL);
+			err = durable_rename(filename, filename_with_original_contents);
+			if (err)
+				pg_fatal("could not rename file \"%s\"", filename);

I don't see a point in re-emitting an error message as well as you are
suggesting here. durable_rename() does this job already on failure
with a set of pg_log_error(). The only difference is that pg_fatal()
is a shortcut for pg_log_error()+exit().
--
Michael

#38Andrey Rudometov
unlimitedhikari@gmail.com
In reply to: Alena Vinter (#30)
Re: Resetting recovery target parameters in pg_createsubscriber

Hello Alena!

As discussion here came to a halt, I want to add some
(counter)arguments, which may or may not make Michael's
proposal sound preferable to you.

On Mon, 10 Nov 2025 at 12:02, Alena Vinter <dlaaren8@gmail.com> wrote:

I'm not in favor of saving additional files.

As far as I understand, there should be only one to two
additional files - it's not like they will multiply.
Some extensions can use additional files, so why
pg_createsubscriber couldn't?

Therefore, it seems appropriate that they should be
captured by the verbose/debug mode. If verbose mode
isn't used, we lose more than just the recovery parameters

But if you save them as messages in server log, then user
might lose them even in verbose mode. Log is not stored
infinitely, and user may not have access to messages
if some time has passed after the incident. A file
seems to be a more reliable solution here.

P.s. About re-emitting error message with pg_fatal in my
proposal - I agree, simple exit() should suffice.

--
best regards,
Andrey Rudometov

#39Michael Paquier
michael@paquier.xyz
In reply to: Andrey Rudometov (#38)
Re: Resetting recovery target parameters in pg_createsubscriber

On Wed, Dec 24, 2025 at 06:12:24PM +0700, Andrey Rudometov wrote:

As discussion here came to a halt, I want to add some
(counter)arguments, which may or may not make Michael's
proposal sound preferable to you.

If somebody could post a refreshed patch set that addresses the
comments raised until now, I would be happy to look at it and get
something done for this release.
--
Michael

#40Alena Vinter
dlaaren8@gmail.com
In reply to: Michael Paquier (#39)
2 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

Hi everyone!

Apologies for the long silence — I was temporarily pulled away from this
work and didn’t want to send an update until I could properly address the
feedback.

I’m now back on track and have refined the implementation based on earlier
discussions. The current version fully adopts the `include_if_exists`
approach, writes temporary recovery settings to a separate config file, and
disables it on exit by renaming to `.disabled`.

Thank you for your patience — I appreciate any further review!

---
Alena Vinter

Attachments:

v9-0001-pg_createsubscriber-use-include_if_exists-for-recove.patchtext/x-patch; charset=US-ASCII; name=v9-0001-pg_createsubscriber-use-include_if_exists-for-recove.patchDownload
From fc1a4bdf8ee44f4b7a2da81bd6a1164eef6855f4 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 30 Dec 2025 13:56:18 +0700
Subject: [PATCH 1/2] pg_createsubscriber: use include_if_exists for recovery
 config and disable on exit

Write temporary recovery parameters to pg_createsubscriber.conf and
include it from postgresql.auto.conf via include_if_exists.  Upon
completion or failure, rename the file to
pg_createsubscriber.conf.disabled so it is ignored on subsequent
restarts.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 77 ++++++++++++++-----
 .../t/040_pg_createsubscriber.pl              | 34 ++++++++
 2 files changed, 90 insertions(+), 21 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index dab4dfb3a52..81ab4641a31 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -29,9 +29,13 @@
 #include "fe_utils/string_utils.h"
 #include "fe_utils/version.h"
 #include "getopt_long.h"
+#include "common/file_utils.h"

 #define	DEFAULT_SUB_PORT	"50432"
 #define	OBJECTTYPE_PUBLICATIONS  0x0001
+#define PG_AUTOCONF_FILENAME	"postgresql.auto.conf"
+#define INCLUDED_CONF_FILE		"pg_createsubscriber.conf"
+#define INCLUDED_CONF_FILE_DISABLED		"pg_createsubscriber.conf.disabled"

 /* Command-line options */
 struct CreateSubscriberOptions
@@ -156,22 +160,43 @@ static char *subscriber_dir = NULL;

 static bool recovery_ended = false;
 static bool standby_running = false;
-
+static bool recovery_params_set = false;

 /*
- * Cleanup objects that were created by pg_createsubscriber if there is an
- * error.
+ * Clean up objects created by pg_createsubscriber.
  *
- * 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.
+ * Publications and replication slots are created on the primary. Depending on
+ * the step where it failed, already created objects should be removed if
+ * possible (sometimes this won't work due to a connection issue).
+ * There is no cleanup on the target server *after* its promotion, because any
+ * failure at this point means recreating the physical replica and starting
+ * again.
  */
 static void
 cleanup_objects_atexit(void)
 {
+	/*
+	 * Disable the recovery configuration by renaming the
+	 * "pg_createsubscriber.conf" file. Since "postgresql.auto.conf" uses
+	 * `include_if_exists`, postgres will silently ignore the missing file.
+	 * The renamed file is preserved fo debugging.
+	 */
+	if (recovery_params_set)
+	{
+		char conf_filename[MAXPGPATH];
+		char conf_filename_disabled[MAXPGPATH];
+		int err;
+
+		snprintf(conf_filename, MAXPGPATH, "%s/%s", subscriber_dir, INCLUDED_CONF_FILE);
+		snprintf(conf_filename_disabled, MAXPGPATH, "%s/%s", subscriber_dir, INCLUDED_CONF_FILE_DISABLED);
+
+		if ((err = durable_rename(conf_filename, conf_filename_disabled)) != 0)
+		{
+			pg_log_warning("recovery parameters were set but not properly reset on the target");
+			pg_log_warning_hint("manual removal of recovery parameters is required from");
+		}
+	}
+
 	if (success)
 		return;

@@ -1322,23 +1347,33 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_name = ''\n");
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_time = ''\n");
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_xid = ''\n");
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n", lsn);

-	if (dry_run)
-	{
-		appendPQExpBufferStr(recoveryconfcontents, "# dry run mode\n");
-		appendPQExpBuffer(recoveryconfcontents,
-						  "recovery_target_lsn = '%X/%08X'\n",
-						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
-	}
-	else
+	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	if (!dry_run)
 	{
-		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
-						  lsn);
+		char  conf_filename[MAXPGPATH];
+		FILE *fd;
+
+		recovery_params_set = true;
+
+		snprintf(conf_filename, MAXPGPATH, "%s/%s", datadir, INCLUDED_CONF_FILE);
+
+		fd = fopen(conf_filename, "w");
+		if (fd == NULL)
+			pg_fatal("could not open file \"%s\": %m", conf_filename);
+
+		if (fwrite(recoveryconfcontents->data, recoveryconfcontents->len, 1, fd) != 1)
+			pg_fatal("could not write to file \"%s\": %m", conf_filename);
+
+		fclose(fd);
+
+		resetPQExpBuffer(recoveryconfcontents);
+		appendPQExpBufferStr(recoveryconfcontents, "include_if_exists '" INCLUDED_CONF_FILE "'");
 		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
 	}
 	disconnect_database(conn, false);
-
-	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
 }

 /*
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index 4657172c9ac..8a9200aead7 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -160,6 +160,7 @@ primary_slot_name = '$slotname'
 primary_conninfo = '$pconnstr dbname=postgres'
 hot_standby_feedback = on
 ]);
+my $sconnstr = $node_s->connstr;
 $node_s->set_standby_mode();
 $node_s->start;

@@ -579,10 +580,43 @@ is($result, qq($db1|{test_pub3}
 $db2|{pub2}),
 	"subscriptions use the correct publications");

+
+# Create a physical standby from the promoted subscriber
+$node_s->safe_psql('postgres',
+    "SELECT pg_create_physical_replication_slot('$slotname');");
+
+# Create backup from promoted subscriber
+$node_s->backup('backup_3');
+
+# Initialize new physical standby
+my $node_k = PostgreSQL::Test::Cluster->new('node_k');
+$node_k->init_from_backup($node_s, 'backup_3',
+    has_streaming => 1);
+
+# Configure the new standby
+$node_k->append_conf('postgresql.conf', qq[
+primary_slot_name = '$slotname'
+primary_conninfo = '$sconnstr dbname=postgres'
+hot_standby_feedback = on
+]);
+
+$node_k->set_standby_mode();
+my $node_k_name = $node_s->name;
+command_ok(
+	[
+		'pg_ctl', '--wait',
+		'--pgdata' => $node_k->data_dir,
+		'--log' => $node_k->logfile,
+		'--options' => "--cluster-name=$node_k_name",
+		'start'
+	],
+	"node_k has started");
+
 # clean up
 $node_p->teardown_node;
 $node_s->teardown_node;
 $node_t->teardown_node;
 $node_f->teardown_node;
+$node_k->teardown_node;

 done_testing();
--
2.52.0

v9-0002-Doc-configuration-file-handling-in-pg_createsubscrib.patchtext/x-patch; charset=US-ASCII; name=v9-0002-Doc-configuration-file-handling-in-pg_createsubscrib.patchDownload
From daa7126c061f39f1e345fbb0e98b33db07366885 Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 30 Dec 2025 14:12:06 +0700
Subject: [PATCH 2/2] Doc: configuration file handling in pg_createsubscriber

Update pg_createsubscriber.sgml to explain that recovery parameters are
written to a separate pg_createsubscriber.conf file included via
include_if_exists, and that the file is renamed to .disabled upon
completion or failure to prevent reloading on restart.
---
 doc/src/sgml/ref/pg_createsubscriber.sgml | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index e450c6a5b37..268fc414216 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -518,8 +518,11 @@ PostgreSQL documentation

     <step>
      <para>
-      Write recovery parameters into the target data directory and restart the
-      target server.  It specifies an LSN (<xref
+      Write recovery parameters into the separate configuration file
+      <filename>pg_createsubscriber.conf</filename> that is included from
+      <filename>postgresql.auto.conf</filename> using
+      <literal>include_if_exists</literal> in the target data directory, then
+      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
@@ -532,7 +535,10 @@ PostgreSQL documentation
       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.
+      does not end until the given number of seconds.  Upon completion or
+      failure, the included configuration file is renamed to
+      <filename>pg_createsubscriber.conf.disabled</filename> so it is no
+      longer loaded on subsequent restarts.
      </para>
     </step>

--
2.52.0

#41Michael Paquier
michael@paquier.xyz
In reply to: Alena Vinter (#40)
1 attachment(s)
Re: Resetting recovery target parameters in pg_createsubscriber

On Tue, Dec 30, 2025 at 02:34:21PM +0700, Alena Vinter wrote:

I’m now back on track and have refined the implementation based on earlier
discussions. The current version fully adopts the `include_if_exists`
approach, writes temporary recovery settings to a separate config file, and
disables it on exit by renaming to `.disabled`.

It seems roughly OK, so I have put my hands on it. A couple of notes
regarding the things done in the updated version attached:
- Addition of two tests to check pg_createsubscriber.conf.disabled in
the data folders of node S and K.
- More description in the tests.
- The "dry run mode" node has disappeared from the recovery parameter
StringInfo, so added it back at the top of the parameters generated.
- Missing a newline after the include_if_exists.
- dirable_rename() logs already something on failure, I see no need
for an extra warning to say the same. Adding the warning telling that
a manual intervention may be required is good, though.
- Let's group the document change with the main patch.
- More stylistic changes, comments and code.
- The new test fails if we undo the changes in pg_createsubscriber.c,
as we'd want, in the shape of node K FATAL-ing due to an incorrect
recovery configuration fed to the node.

Alena, what do you think?
--
Michael

Attachments:

v10-0001-pg_createsubscriber-use-include_if_exists-for-re.patchtext/x-diff; charset=us-asciiDownload
From e4601823d84b8226f3a230ddb131c00dfdd8b46b Mon Sep 17 00:00:00 2001
From: Alena Vinter <dlaaren8@gmail.com>
Date: Tue, 30 Dec 2025 13:56:18 +0700
Subject: [PATCH v10] pg_createsubscriber: use include_if_exists for recovery
 config and disable on exit

Write temporary recovery parameters to pg_createsubscriber.conf and
include it from postgresql.auto.conf via include_if_exists.  Upon
completion or failure, rename the file to
pg_createsubscriber.conf.disabled so it is ignored on subsequent
restarts.
---
 src/bin/pg_basebackup/pg_createsubscriber.c   | 100 ++++++++++++++----
 .../t/040_pg_createsubscriber.pl              |  46 ++++++++
 doc/src/sgml/ref/pg_createsubscriber.sgml     |  12 ++-
 3 files changed, 132 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index 9a825bfd166e..3ce24860c04b 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -20,6 +20,7 @@
 
 #include "common/connect.h"
 #include "common/controldata_utils.h"
+#include "common/file_utils.h"
 #include "common/logging.h"
 #include "common/pg_prng.h"
 #include "common/restricted_token.h"
@@ -33,6 +34,21 @@
 #define	DEFAULT_SUB_PORT	"50432"
 #define	OBJECTTYPE_PUBLICATIONS  0x0001
 
+/*
+ * Configuration files for recovery parameters.
+ *
+ * The recovery parameters are set in INCLUDED_CONF_FILE, itself loaded by
+ * the server through an include_if_exists in postgresql.auto.conf.
+ *
+ * INCLUDED_CONF_FILE is renamed to INCLUDED_CONF_FILE_DISABLED when exiting,
+ * so as the recovery parameters set by this tool never take effect on node
+ * restart.  The contents of INCLUDED_CONF_FILE_DISABLED can be useful for
+ * debugging.
+ */
+#define PG_AUTOCONF_FILENAME		"postgresql.auto.conf"
+#define INCLUDED_CONF_FILE			"pg_createsubscriber.conf"
+#define INCLUDED_CONF_FILE_DISABLED	INCLUDED_CONF_FILE ".disabled"
+
 /* Command-line options */
 struct CreateSubscriberOptions
 {
@@ -156,22 +172,43 @@ static char *subscriber_dir = NULL;
 
 static bool recovery_ended = false;
 static bool standby_running = false;
+static bool recovery_params_set = false;
 
 
 /*
- * Cleanup objects that were created by pg_createsubscriber if there is an
- * error.
+ * Clean up objects created by pg_createsubscriber.
  *
- * 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.
+ * Publications and replication slots are created on the primary.  Depending
+ * on the step where it failed, already-created objects should be removed if
+ * possible (sometimes this won't work due to a connection issue).
+ * There is no cleanup on the target server *after* its promotion, because any
+ * failure at this point means recreating the physical replica and starting
+ * again.
+ *
+ * The recovery configuration is always removed, by renaming the included
+ * configuration file out of the way.
  */
 static void
 cleanup_objects_atexit(void)
 {
+	/* Rename the included configuration file, if necessary. */
+	if (recovery_params_set)
+	{
+		char		conf_filename[MAXPGPATH];
+		char		conf_filename_disabled[MAXPGPATH];
+
+		snprintf(conf_filename, MAXPGPATH, "%s/%s", subscriber_dir,
+				 INCLUDED_CONF_FILE);
+		snprintf(conf_filename_disabled, MAXPGPATH, "%s/%s", subscriber_dir,
+				 INCLUDED_CONF_FILE_DISABLED);
+
+		if (durable_rename(conf_filename, conf_filename_disabled) != 0)
+		{
+			/* durable_rename() has already logged something. */
+			pg_log_warning_hint("A manual removal of the recovery parameters may be required.");
+		}
+	}
+
 	if (success)
 		return;
 
@@ -1303,6 +1340,10 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	 * targets (name, time, xid, LSN).
 	 */
 	recoveryconfcontents = GenerateRecoveryConfig(conn, NULL, NULL);
+
+	if (dry_run)
+		appendPQExpBufferStr(recoveryconfcontents, "# dry run mode\n");
+
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target = ''\n");
 	appendPQExpBufferStr(recoveryconfcontents,
 						 "recovery_target_timeline = 'latest'\n");
@@ -1322,23 +1363,36 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_name = ''\n");
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_time = ''\n");
 	appendPQExpBufferStr(recoveryconfcontents, "recovery_target_xid = ''\n");
-
-	if (dry_run)
-	{
-		appendPQExpBufferStr(recoveryconfcontents, "# dry run mode\n");
-		appendPQExpBuffer(recoveryconfcontents,
-						  "recovery_target_lsn = '%X/%08X'\n",
-						  LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr));
-	}
-	else
-	{
-		appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n",
-						  lsn);
-		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
-	}
-	disconnect_database(conn, false);
+	appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n", lsn);
 
 	pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data);
+
+	if (!dry_run)
+	{
+		char		conf_filename[MAXPGPATH];
+		FILE	   *fd;
+
+		/* Write the recovery parameters to INCLUDED_CONF_FILE */
+		snprintf(conf_filename, MAXPGPATH, "%s/%s", datadir,
+				 INCLUDED_CONF_FILE);
+		fd = fopen(conf_filename, "w");
+		if (fd == NULL)
+			pg_fatal("could not open file \"%s\": %m", conf_filename);
+
+		if (fwrite(recoveryconfcontents->data, recoveryconfcontents->len, 1, fd) != 1)
+			pg_fatal("could not write to file \"%s\": %m", conf_filename);
+
+		fclose(fd);
+		recovery_params_set = true;
+
+		/* Include conditionally the recovery parameters. */
+		resetPQExpBuffer(recoveryconfcontents);
+		appendPQExpBufferStr(recoveryconfcontents,
+							 "include_if_exists '" INCLUDED_CONF_FILE "'\n");
+		WriteRecoveryConfig(conn, datadir, recoveryconfcontents);
+	}
+
+	disconnect_database(conn, false);
 }
 
 /*
diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
index a905183b738e..e1de946488ed 100644
--- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
+++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl
@@ -160,6 +160,7 @@ primary_slot_name = '$slotname'
 primary_conninfo = '$pconnstr dbname=postgres'
 hot_standby_feedback = on
 ]);
+my $sconnstr = $node_s->connstr;
 $node_s->set_standby_mode();
 $node_s->start;
 
@@ -472,6 +473,11 @@ command_ok(
 	],
 	'run pg_createsubscriber on node S');
 
+# Check that included file is renamed after success.
+my $node_s_datadir = $node_s->data_dir;
+ok( -f "$node_s_datadir/pg_createsubscriber.conf.disabled",
+	"pg_createsubscriber.conf.disabled exists in node S");
+
 # Confirm the physical replication slot has been removed
 $result = $node_p->safe_psql($db1,
 	"SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'"
@@ -579,10 +585,50 @@ is($result, qq($db1|{test_pub3}
 $db2|{pub2}),
 	"subscriptions use the correct publications");
 
+# Verify that node K, set as a standby, is able to start correctly without
+# the recovery configuration written by pg_createsubscriber interfering.
+# This node is created from node S, where pg_createsubscriber has been run.
+
+# Create a physical standby from the promoted subscriber
+$node_s->safe_psql('postgres',
+	"SELECT pg_create_physical_replication_slot('$slotname');");
+
+# Create backup from promoted subscriber
+$node_s->backup('backup_3');
+
+# Initialize new physical standby
+my $node_k = PostgreSQL::Test::Cluster->new('node_k');
+$node_k->init_from_backup($node_s, 'backup_3', has_streaming => 1);
+
+my $node_k_datadir = $node_k->data_dir;
+ok( -f "$node_k_datadir/pg_createsubscriber.conf.disabled",
+	"pg_createsubscriber.conf.disabled exists in node K");
+
+# Configure the new standby
+$node_k->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+primary_conninfo = '$sconnstr dbname=postgres'
+hot_standby_feedback = on
+]);
+
+$node_k->set_standby_mode();
+my $node_k_name = $node_s->name;
+command_ok(
+	[
+		'pg_ctl', '--wait',
+		'--pgdata' => $node_k->data_dir,
+		'--log' => $node_k->logfile,
+		'--options' => "--cluster-name=$node_k_name",
+		'start'
+	],
+	"node K has started");
+
 # clean up
 $node_p->teardown_node;
 $node_s->teardown_node;
 $node_t->teardown_node;
 $node_f->teardown_node;
+$node_k->teardown_node;
 
 done_testing();
diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml
index e450c6a5b376..cf45ff3573d1 100644
--- a/doc/src/sgml/ref/pg_createsubscriber.sgml
+++ b/doc/src/sgml/ref/pg_createsubscriber.sgml
@@ -518,8 +518,11 @@ PostgreSQL documentation
 
     <step>
      <para>
-      Write recovery parameters into the target data directory and restart the
-      target server.  It specifies an LSN (<xref
+      Write recovery parameters into the separate configuration file
+      <filename>pg_createsubscriber.conf</filename> that is included from
+      <filename>postgresql.auto.conf</filename> using
+      <literal>include_if_exists</literal> in the target data directory, then
+      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
@@ -532,7 +535,10 @@ PostgreSQL documentation
       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.
+      does not end until the given number of seconds.  Upon completion, the
+      included configuration file is renamed to
+      <filename>pg_createsubscriber.conf.disabled</filename> so as it is no
+      longer loaded on subsequent restarts.
      </para>
     </step>
 
-- 
2.51.0

#42Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#41)
Re: Resetting recovery target parameters in pg_createsubscriber

On Mon, Jan 05, 2026 at 03:33:17PM +0900, Michael Paquier wrote:

It seems roughly OK, so I have put my hands on it. A couple of notes
regarding the things done in the updated version attached:
- Addition of two tests to check pg_createsubscriber.conf.disabled in
the data folders of node S and K.
- More description in the tests.
- The "dry run mode" node has disappeared from the recovery parameter
StringInfo, so added it back at the top of the parameters generated.
- Missing a newline after the include_if_exists.
- dirable_rename() logs already something on failure, I see no need
for an extra warning to say the same. Adding the warning telling that
a manual intervention may be required is good, though.
- Let's group the document change with the main patch.
- More stylistic changes, comments and code.
- The new test fails if we undo the changes in pg_createsubscriber.c,
as we'd want, in the shape of node K FATAL-ing due to an incorrect
recovery configuration fed to the node.

After a second round of review, there was still an issue here: the LSN
returned by create_logical_replication_slot() could be NULL in
dry-run mode, and the code sets recovery_target_lsn to
InvalidXLogRecPtr in the recovery configuration printed out, meaning a
NULL pointer dereference. I have switched the patch to do what
happens on HEAD: InvalidXLogRecPtr for the dry-run mode. That also
leads to less diffs. And done.
--
Michael

#43Alena Vinter
dlaaren8@gmail.com
In reply to: Michael Paquier (#42)
Re: Resetting recovery target parameters in pg_createsubscriber

Hi,

Thanks to everyone for the feedback, cleanup, and improvements!

---
Best regards,
Alena Vinter