Copy function for logical replication slots

Started by Masahiko Sawadaover 7 years ago41 messages
#1Masahiko Sawada
sawada.mshk@gmail.com
1 attachment(s)

Hi,

I'd like to propose a copy function for logical replication slots.
Currently when we create a new logical replication slot it starts to
read WAL from an LSN of the current insert. This function copies a
existing logical replication slot while changing output plugin and
persistence. That is, the copied new replication slot starts from the
same LSN as the source one. Since a new copied slot starts from the
same LSN of existing one we don't need to care about WAL reservation.

A use case I imagined is for investigations for example. I mean that
when replication collision occurs on subscriber there is no way to see
what replicated data is conflicting (perhaps error log helps it but is
not detailed) and there is no way to advance a replication origin in
order to exactly skip to apply conflicting data. By creating a new
logical slot with a different output plugin at the same LSN, we can
see what data a replication slot will decode (and send) and those LSNs
as well. This function will help for that purpose.

Here is execution samples.

postgres(1:17715)=# select
pg_create_logical_replication_slot('orig_slot', 'test_decoding');
pg_create_logical_replication_slot
------------------------------------
(orig_slot,0/164A410)
(1 row)

Time: 17.759 ms
postgres(1:17715)=# select
pg_copy_logical_replication_slot('orig_slot', 'copy1_slot');
pg_copy_logical_replication_slot
----------------------------------
(copy1_slot,0/164A410)
(1 row)

Time: 6.074 ms
postgres(1:17715)=# select
pg_copy_logical_replication_slot('orig_slot', 'copy2_slot',
'wal2json');
pg_copy_logical_replication_slot
----------------------------------
(copy2_slot,0/164A410)
(1 row)

Time: 6.201 ms
postgres(1:17715)=# select
pg_copy_logical_replication_slot('orig_slot', 'copy3_slot',
'wal2json', true);
pg_copy_logical_replication_slot
----------------------------------
(copy3_slot,0/164A410)
(1 row)

Time: 5.071 ms
postgres(1:17715)=# select * from pg_replication_slots ;
slot_name | plugin | slot_type | datoid | database |
temporary | active | active_pid | xmin | catalog_xmin | restart_lsn |
confirmed_flush_lsn
------------+---------------+-----------+--------+----------+-----------+--------+------------+------+--------------+-------------+---------------------
copy3_slot | wal2json | logical | 13237 | postgres | t
| t | 17715 | | 568 | 0/164A3D8 | 0/164A410
copy2_slot | wal2json | logical | 13237 | postgres | f
| f | | | 568 | 0/164A3D8 | 0/164A410
copy1_slot | orig_slot | logical | 13237 | postgres | f
| f | | | 568 | 0/164A3D8 | 0/164A410
orig_slot | test_decoding | logical | 13237 | postgres | f
| f | | | 568 | 0/164A3D8 | 0/164A410
(4 rows)

Feedback is very welcome.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v1-0001-Copy-logical-replication-slot.patchapplication/octet-stream; name=v1-0001-Copy-logical-replication-slot.patchDownload
From 3f22012d4da98ee49193fbfce7b9afa57a24913e Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Tue, 26 Jun 2018 21:51:50 +0900
Subject: [PATCH v1] Copy logical replication slot.

---
 doc/src/sgml/func.sgml                      |  21 ++++
 src/backend/replication/logical/logical.c   |  13 ++-
 src/backend/replication/slotfuncs.c         | 163 +++++++++++++++++++++++-----
 src/backend/replication/walsender.c         |   1 +
 src/include/catalog/pg_proc.dat             |  21 ++++
 src/include/replication/logical.h           |   1 +
 src/test/recovery/t/006_logical_decoding.pl |  13 ++-
 7 files changed, 202 insertions(+), 31 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 5dce8ef..253f0e3 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19252,6 +19252,27 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copy a existing <parameter>src_slot_name</parameter> logical (decoding) slot
+        to <parameter>dst_slot_name</parameter> while changing the output plugin
+        and persistence. Copied logical slot starts from the same LSN as the
+        source logical slot. Both <parameter>plugin</parameter> and
+        <parameter>temporary</parameter> are optional. If <parameter>plugin</parameter>
+        or <parameter>temporary</parameter> are omitted, the same values of
+        the source logical slot are set.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 61588d6..7e7d54a 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -223,6 +223,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr start_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -266,7 +267,15 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	/* Find start location to read WAL if not specified */
+	if (XLogRecPtrIsInvalid(start_lsn))
+		ReplicationSlotReserveWal();
+	else
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = start_lsn;
+		SpinLockRelease(&slot->mutex);
+	}
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -311,7 +320,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, start_lsn, xmin_horizon,
 								 need_full_snapshot, true,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 2806e10..09f227d 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -92,30 +92,19 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 	PG_RETURN_DATUM(result);
 }
 
-
 /*
- * SQL function for creating a new logical replication slot.
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Return a confirmed_lsn of new replication slot.
  */
-Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+static XLogRecPtr
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr start_lsn)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
 	LogicalDecodingContext *ctx = NULL;
-
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
-	Datum		values[2];
-	bool		nulls[2];
+	XLogRecPtr	result;
 
 	Assert(!MyReplicationSlot);
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
 	check_permissions();
 
 	CheckLogicalDecodingRequirements();
@@ -128,39 +117,157 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									start_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = CStringGetTextDatum(NameStr(MyReplicationSlot->data.name));
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
 
-	memset(nulls, 0, sizeof(nulls));
-
-	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
-
 	/* ok, slot is now fully created, mark it as persistent if needed */
 	if (!temporary)
 		ReplicationSlotPersist();
+
+	result = MyReplicationSlot->data.confirmed_flush;
+
 	ReplicationSlotRelease();
 
-	PG_RETURN_DATUM(result);
+	return result;
+}
+
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	XLogRecPtr	confirmed_flush;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	confirmed_flush = create_logical_replication_slot(NameStr(*name),
+													  NameStr(*plugin),
+													  temporary,
+													  InvalidXLogRecPtr);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = CStringGetTextDatum(NameStr(*name));
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
 }
 
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	Name		plugin;
+	bool		temporary;
+	ReplicationSlot	*src_slot;
+	XLogRecPtr	confirmed_flush;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+	int			i;
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Look for source replication slot */
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		if (s->in_use && strcmp(NameStr(*src_name), NameStr(s->data.name)) == 0)
+			src_slot = s;
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	/* The source replication slot doesn't exist */
+	if (src_slot == NULL)
+		ereport(ERROR,
+				(errmsg("logical replication slot \"%s\" does not exist",
+						NameStr(*src_name))));
+
+	/* Default is the same as source slot */
+	plugin = &src_slot->data.name;
+	temporary = (src_slot->data.persistency == RS_TEMPORARY);
+
+	/* check the optional arguments */
+	if (PG_NARGS() >= 3)
+		plugin = PG_GETARG_NAME(2);
+	if (PG_NARGS() >= 4)
+		temporary = PG_GETARG_BOOL(3);
+
+	confirmed_flush = create_logical_replication_slot(NameStr(*dst_name),
+													  NameStr(*plugin),
+													  temporary,
+													  src_slot->data.restart_lsn);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = CStringGetTextDatum(NameStr(*dst_name));
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
 
 /*
  * SQL function for dropping a replication slot.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index e47ddca..c49f2d2 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -918,6 +918,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 40d54ed..862b89d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9807,6 +9807,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4005', descr => 'copy up a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name bool',
+  proallargtypes => '{name,name,name,bool,text,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,plugin,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4006', descr => 'copy up a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name',
+  proallargtypes => '{name,name,name,text,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,plugin,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4007', descr => 'copy up a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,text,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c25ac1f..d1643e5 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr start_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index e3a5fe9..eff256d 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -7,7 +7,7 @@ use strict;
 use warnings;
 use PostgresNode;
 use TestLib;
-use Test::More tests => 16;
+use Test::More tests => 17;
 use Config;
 
 # Initialize master node
@@ -27,6 +27,11 @@ $node_master->safe_psql('postgres',
 	qq[SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding');]
 );
 
+# Copy logical slot
+$node_master->safe_psql('postgres',
+	qq[SELECT pg_copy_logical_replication_slot('test_slot', 'copy_slot', 'test_decoding', false);]
+);
+
 $node_master->safe_psql('postgres',
 	qq[INSERT INTO decoding_test(x,y) SELECT s, s::text FROM generate_series(1,10) s;]
 );
@@ -37,6 +42,12 @@ my ($result) = $node_master->safe_psql('postgres',
 is(scalar(my @foobar = split /^/m, $result),
 	12, 'Decoding produced 12 rows inc BEGIN/COMMIT');
 
+# Basic decoding works with copied slot
+my ($result) = $node_master->safe_psql('postgres',
+	qq[SELECT pg_logical_slot_get_changes('copy_slot', NULL, NULL);]);
+is(scalar(my @foobar = split /^/m, $result),
+	12, 'Decoding produced 12 rows inc BEGIN/COMMIT');
+
 # If we immediately crash the server we might lose the progress we just made
 # and replay the same changes again. But a clean shutdown should never repeat
 # the same changes when we use the SQL decoding interface.
-- 
2.10.5

#2Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#1)
Re: Copy function for logical replication slots

On Thu, Jun 28, 2018 at 11:51:20AM +0900, Masahiko Sawada wrote:

A use case I imagined is for investigations for example. I mean that
when replication collision occurs on subscriber there is no way to see
what replicated data is conflicting (perhaps error log helps it but is
not detailed) and there is no way to advance a replication origin in
order to exactly skip to apply conflicting data. By creating a new
logical slot with a different output plugin at the same LSN, we can
see what data a replication slot will decode (and send) and those LSNs
as well. This function will help for that purpose.

Hm. Shouldn't the original slot copied be owned by the process doing
the copy with ReplicationSlotAcquire? There could be some cases where
copying a physical slot also makes sense.
--
Michael

#3Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#2)
1 attachment(s)
Re: Copy function for logical replication slots

On Thu, Jun 28, 2018 at 12:29 PM, Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Jun 28, 2018 at 11:51:20AM +0900, Masahiko Sawada wrote:

A use case I imagined is for investigations for example. I mean that
when replication collision occurs on subscriber there is no way to see
what replicated data is conflicting (perhaps error log helps it but is
not detailed) and there is no way to advance a replication origin in
order to exactly skip to apply conflicting data. By creating a new
logical slot with a different output plugin at the same LSN, we can
see what data a replication slot will decode (and send) and those LSNs
as well. This function will help for that purpose.

Hm. Shouldn't the original slot copied be owned by the process doing
the copy with ReplicationSlotAcquire?

Right, it should do and release it before creating new one.

There could be some cases where
copying a physical slot also makes sense.

I've thought that but I didn't find concrete use case. That's why I
started with only logical slot.

Attached v2 patch.

Regards,
--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v2-0001-Copy-logical-replication-slot.patchapplication/octet-stream; name=v2-0001-Copy-logical-replication-slot.patchDownload
From 9434805e5d357b48c43d0d91d33a0a58e42d13ce Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Tue, 26 Jun 2018 21:51:50 +0900
Subject: [PATCH v2] Copy logical replication slot.

---
 doc/src/sgml/func.sgml                      |  21 ++++
 src/backend/replication/logical/logical.c   |  13 ++-
 src/backend/replication/slotfuncs.c         | 152 +++++++++++++++++++++++-----
 src/backend/replication/walsender.c         |   1 +
 src/include/catalog/pg_proc.dat             |  21 ++++
 src/include/replication/logical.h           |   1 +
 src/test/recovery/t/006_logical_decoding.pl |  13 ++-
 7 files changed, 191 insertions(+), 31 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 5dce8ef..253f0e3 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19252,6 +19252,27 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copy a existing <parameter>src_slot_name</parameter> logical (decoding) slot
+        to <parameter>dst_slot_name</parameter> while changing the output plugin
+        and persistence. Copied logical slot starts from the same LSN as the
+        source logical slot. Both <parameter>plugin</parameter> and
+        <parameter>temporary</parameter> are optional. If <parameter>plugin</parameter>
+        or <parameter>temporary</parameter> are omitted, the same values of
+        the source logical slot are set.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 61588d6..7e7d54a 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -223,6 +223,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr start_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -266,7 +267,15 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	/* Find start location to read WAL if not specified */
+	if (XLogRecPtrIsInvalid(start_lsn))
+		ReplicationSlotReserveWal();
+	else
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = start_lsn;
+		SpinLockRelease(&slot->mutex);
+	}
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -311,7 +320,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, start_lsn, xmin_horizon,
 								 need_full_snapshot, true,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 2806e10..6872588 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -92,30 +92,19 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 	PG_RETURN_DATUM(result);
 }
 
-
 /*
- * SQL function for creating a new logical replication slot.
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Return a confirmed_lsn of new replication slot.
  */
-Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+static XLogRecPtr
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr start_lsn)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
 	LogicalDecodingContext *ctx = NULL;
-
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
-	Datum		values[2];
-	bool		nulls[2];
+	XLogRecPtr	result;
 
 	Assert(!MyReplicationSlot);
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
 	check_permissions();
 
 	CheckLogicalDecodingRequirements();
@@ -128,39 +117,146 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									start_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = CStringGetTextDatum(NameStr(MyReplicationSlot->data.name));
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
 
-	memset(nulls, 0, sizeof(nulls));
-
-	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
-
 	/* ok, slot is now fully created, mark it as persistent if needed */
 	if (!temporary)
 		ReplicationSlotPersist();
+
+	result = MyReplicationSlot->data.confirmed_flush;
+
 	ReplicationSlotRelease();
 
-	PG_RETURN_DATUM(result);
+	return result;
 }
 
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	XLogRecPtr	confirmed_flush;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	confirmed_flush = create_logical_replication_slot(NameStr(*name),
+													  NameStr(*plugin),
+													  temporary,
+													  InvalidXLogRecPtr);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = CStringGetTextDatum(NameStr(*name));
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	char		*plugin;	/* optional argment */
+	bool		temporary;	/* optional argment */
+	XLogRecPtr	confirmed_flush;
+	XLogRecPtr	start_lsn;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
+
+	/* Save some fields before releasing */
+	start_lsn = MyReplicationSlot->data.restart_lsn;
+	plugin = pstrdup(NameStr(MyReplicationSlot->data.plugin));
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
+
+	/* Release it */
+	ReplicationSlotRelease();
+
+	/* Check the optional arguments */
+	if (PG_NARGS() >= 3)
+		plugin = NameStr(*(PG_GETARG_NAME(2)));
+	if (PG_NARGS() >= 4)
+		temporary = PG_GETARG_BOOL(3);
+
+	confirmed_flush = create_logical_replication_slot(NameStr(*dst_name),
+													  plugin,
+													  temporary,
+													  start_lsn);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = CStringGetTextDatum(NameStr(*dst_name));
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
 
 /*
  * SQL function for dropping a replication slot.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index e47ddca..c49f2d2 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -918,6 +918,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 40d54ed..862b89d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9807,6 +9807,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4005', descr => 'copy up a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name bool',
+  proallargtypes => '{name,name,name,bool,text,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,plugin,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4006', descr => 'copy up a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name',
+  proallargtypes => '{name,name,name,text,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,plugin,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4007', descr => 'copy up a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,text,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c25ac1f..d1643e5 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr start_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index e3a5fe9..76e91f5 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -7,7 +7,7 @@ use strict;
 use warnings;
 use PostgresNode;
 use TestLib;
-use Test::More tests => 16;
+use Test::More tests => 17;
 use Config;
 
 # Initialize master node
@@ -27,6 +27,11 @@ $node_master->safe_psql('postgres',
 	qq[SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding');]
 );
 
+# Copy logical slot
+$node_master->safe_psql('postgres',
+	qq[SELECT pg_copy_logical_replication_slot('test_slot', 'copy_slot', 'test_decoding', false);]
+);
+
 $node_master->safe_psql('postgres',
 	qq[INSERT INTO decoding_test(x,y) SELECT s, s::text FROM generate_series(1,10) s;]
 );
@@ -37,6 +42,12 @@ my ($result) = $node_master->safe_psql('postgres',
 is(scalar(my @foobar = split /^/m, $result),
 	12, 'Decoding produced 12 rows inc BEGIN/COMMIT');
 
+# Basic decoding works with copied slot
+$result = $node_master->safe_psql('postgres',
+	qq[SELECT pg_logical_slot_get_changes('copy_slot', NULL, NULL);]);
+is(scalar(@foobar = split /^/m, $result),
+	12, 'Decoding produced 12 rows inc BEGIN/COMMIT');
+
 # If we immediately crash the server we might lose the progress we just made
 # and replay the same changes again. But a clean shutdown should never repeat
 # the same changes when we use the SQL decoding interface.
-- 
2.10.5

#4Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#3)
Re: Copy function for logical replication slots

On Thu, Jun 28, 2018 at 03:34:00PM +0900, Masahiko Sawada wrote:

On Thu, Jun 28, 2018 at 12:29 PM, Michael Paquier <michael@paquier.xyz> wrote:

Hm. Shouldn't the original slot copied be owned by the process doing
the copy with ReplicationSlotAcquire?

Right, it should do and release it before creating new one.

Yes, or MyReplicationSlot would not like that.

There could be some cases where
copying a physical slot also makes sense.

I've thought that but I didn't find concrete use case. That's why I
started with only logical slot.

Let's imagine the case of a single base backup which is associated to a
given replication slot, and that this backup is then used to spawn
multiple standbys where each one of them needs a separate slot to
consume changes at their pace. If you can copy the slot used in the
first backup, then both nodes could consume it. That looks useful to
me to make sure that both slots are based a consistent point.
--
Michael

#5Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Michael Paquier (#4)
Re: Copy function for logical replication slots

On 6/28/18 08:47, Michael Paquier wrote:

There could be some cases where
copying a physical slot also makes sense.

I've thought that but I didn't find concrete use case. That's why I
started with only logical slot.

Let's imagine the case of a single base backup which is associated to a
given replication slot, and that this backup is then used to spawn
multiple standbys where each one of them needs a separate slot to
consume changes at their pace. If you can copy the slot used in the
first backup, then both nodes could consume it. That looks useful to
me to make sure that both slots are based a consistent point.

I think this use case of cloning replicas would also be interesting in
the logical slot case.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#6Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Peter Eisentraut (#5)
Re: Copy function for logical replication slots

On Thu, Jun 28, 2018 at 5:37 PM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

On 6/28/18 08:47, Michael Paquier wrote:

There could be some cases where
copying a physical slot also makes sense.

I've thought that but I didn't find concrete use case. That's why I
started with only logical slot.

Let's imagine the case of a single base backup which is associated to a
given replication slot, and that this backup is then used to spawn
multiple standbys where each one of them needs a separate slot to
consume changes at their pace. If you can copy the slot used in the
first backup, then both nodes could consume it. That looks useful to
me to make sure that both slots are based a consistent point.

Thank you, that sounds useful. I'll update the patch to include physical slots.

I think this use case of cloning replicas would also be interesting in
the logical slot case.

+1

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

#7Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Masahiko Sawada (#6)
1 attachment(s)
Re: Copy function for logical replication slots

On Thu, Jun 28, 2018 at 7:10 PM, Masahiko Sawada <sawada.mshk@gmail.com> wrote:

On Thu, Jun 28, 2018 at 5:37 PM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

On 6/28/18 08:47, Michael Paquier wrote:

There could be some cases where
copying a physical slot also makes sense.

I've thought that but I didn't find concrete use case. That's why I
started with only logical slot.

Let's imagine the case of a single base backup which is associated to a
given replication slot, and that this backup is then used to spawn
multiple standbys where each one of them needs a separate slot to
consume changes at their pace. If you can copy the slot used in the
first backup, then both nodes could consume it. That looks useful to
me to make sure that both slots are based a consistent point.

Thank you, that sounds useful. I'll update the patch to include physical slots.

I think this use case of cloning replicas would also be interesting in
the logical slot case.

+1

Attached an updated patch including copy function support for logical
slots as well as physical slots. Please review it.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v3-0001-Copy-physical-and-logical-replication-slot.patchapplication/octet-stream; name=v3-0001-Copy-physical-and-logical-replication-slot.patchDownload
From f895ec3417a9ee4c2f58e31fa8fc33c48dbf534e Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Tue, 26 Jun 2018 21:51:50 +0900
Subject: [PATCH v3] Copy physical and logical replication slot.

---
 doc/src/sgml/func.sgml                      |  41 ++++
 src/backend/replication/logical/logical.c   |  13 +-
 src/backend/replication/slotfuncs.c         | 308 +++++++++++++++++++++++-----
 src/backend/replication/walsender.c         |   1 +
 src/include/catalog/pg_proc.dat             |  35 ++++
 src/include/replication/logical.h           |   1 +
 src/test/recovery/t/006_logical_decoding.pl |  13 +-
 7 files changed, 360 insertions(+), 52 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index edc9be9..5a5369a 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19252,6 +19252,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>temporary</parameter> <type>bool</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copy a existing <parameter>src_slot_name</parameter> physical slot
+        to <parameter>dst_slot_name</parameter>. The copied physical slot starts
+        to reserve WAL from the same LSN as the source slot if the source slot
+        already reserves WAL. <parameter>temporary</parameter> is optional. If
+        <parameter>temporary</parameter> is omitted, the same value of the
+        source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copy a existing <parameter>src_slot_name</parameter> logical (decoding) slot
+        to <parameter>dst_slot_name</parameter> while changing the output plugin
+        and persistence. The copied logical slot starts from the same LSN as the
+        source logical slot. Both <parameter>plugin</parameter> and
+        <parameter>temporary</parameter> are optional. If <parameter>plugin</parameter>
+        or <parameter>temporary</parameter> are omitted, the same values of
+        the source logical slot are used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 61588d6..7e7d54a 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -223,6 +223,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr start_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -266,7 +267,15 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	/* Find start location to read WAL if not specified */
+	if (XLogRecPtrIsInvalid(start_lsn))
+		ReplicationSlotReserveWal();
+	else
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = start_lsn;
+		SpinLockRelease(&slot->mutex);
+	}
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -311,7 +320,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, start_lsn, xmin_horizon,
 								 need_full_snapshot, true,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 2806e10..baab35e 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -36,86 +36,181 @@ check_permissions(void)
 }
 
 /*
- * SQL function for creating a new physical (streaming replication)
- * replication slot.
+ * Helper function for creating a new physical replication slot with
+ * given arguments. Return a restart_lsn of new replication slot or
+ * InvalidXLogRecPtr if WAL reservation is not required.
  */
-Datum
-pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
+static XLogRecPtr
+create_physical_replication_slot(char *name, bool immediately_reserve,
+								 bool temporary, XLogRecPtr start_lsn)
 {
-	Name		name = PG_GETARG_NAME(0);
-	bool		immediately_reserve = PG_GETARG_BOOL(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-	Datum		values[2];
-	bool		nulls[2];
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
+	XLogRecPtr	result = InvalidXLogRecPtr;
 
 	Assert(!MyReplicationSlot);
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
 	check_permissions();
 
 	CheckSlotRequirements();
 
 	/* acquire replication slot, this will check for conflicting names */
-	ReplicationSlotCreate(NameStr(*name), false,
+	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	nulls[0] = false;
-
 	if (immediately_reserve)
 	{
 		/* Reserve WAL as the user asked for it */
-		ReplicationSlotReserveWal();
+		if (XLogRecPtrIsInvalid(start_lsn))
+			ReplicationSlotReserveWal();
+		else
+			MyReplicationSlot->data.restart_lsn = start_lsn;
 
 		/* Write this slot to disk */
 		ReplicationSlotMarkDirty();
 		ReplicationSlotSave();
 
-		values[1] = LSNGetDatum(MyReplicationSlot->data.restart_lsn);
-		nulls[1] = false;
+		result = MyReplicationSlot->data.restart_lsn;
 	}
+
+	ReplicationSlotRelease();
+
+	return result;
+}
+
+/*
+ * SQL function for creating a new physical (streaming replication)
+ * replication slot.
+ */
+Datum
+pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	bool		immediately_reserve = PG_GETARG_BOOL(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	Datum		values[2];
+	bool		nulls[2];
+	XLogRecPtr	result_lsn;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		result;
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	result_lsn = create_physical_replication_slot(NameStr(*name),
+												  immediately_reserve,
+												  temporary,
+												  InvalidXLogRecPtr);
+
+	values[0] = NameGetDatum(name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
 	else
 	{
-		nulls[1] = true;
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
 	}
 
 	tuple = heap_form_tuple(tupdesc, values, nulls);
 	result = HeapTupleGetDatum(tuple);
 
-	ReplicationSlotRelease();
-
 	PG_RETURN_DATUM(result);
 }
 
-
 /*
- * SQL function for creating a new logical replication slot.
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
  */
 Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
-	LogicalDecodingContext *ctx = NULL;
+	return pg_copy_physical_replication_slot(fcinfo);
+}
 
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
+/*
+ * SQL function for copying a physical replication slot.
+ */
+Datum
+pg_copy_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	bool		temporary; /* optional argument */
+	bool		immediately_reserve;
+	XLogRecPtr	start_lsn;
+	XLogRecPtr	result_lsn;
 	Datum		values[2];
 	bool		nulls[2];
-
-	Assert(!MyReplicationSlot);
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
 
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
 
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
+
+	/* Check type of replication slot */
+	if (SlotIsLogical(MyReplicationSlot))
+	{
+		ReplicationSlotRelease();
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy logical replication slot to physical replication slot"))));
+	}
+
+	/* Save some values of the source slot before releasing */
+	start_lsn = MyReplicationSlot->data.restart_lsn;
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
+
+	/* Release it */
+	ReplicationSlotRelease();
+
+	/* check the optional argument */
+	if (PG_NARGS() >= 3)
+		temporary = PG_GETARG_BOOL(2);
+
+	/* Reserve WAL at creation if the source slots already reserves */
+	immediately_reserve = !XLogRecPtrIsInvalid(start_lsn);
+
+	result_lsn = create_physical_replication_slot(NameStr(*dst_name),
+												  immediately_reserve,
+												  temporary,
+												  start_lsn);
+
+	values[0] = NameGetDatum(dst_name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
+	else
+	{
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
+	}
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
+}
+
+/*
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Return a confirmed_lsn of new replication slot.
+ */
+static XLogRecPtr
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr start_lsn)
+{
+	LogicalDecodingContext *ctx = NULL;
+	XLogRecPtr	result;
+
+	Assert(!MyReplicationSlot);
+
 	check_permissions();
 
 	CheckLogicalDecodingRequirements();
@@ -128,39 +223,154 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									start_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = CStringGetTextDatum(NameStr(MyReplicationSlot->data.name));
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
 
-	memset(nulls, 0, sizeof(nulls));
-
-	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
-
 	/* ok, slot is now fully created, mark it as persistent if needed */
 	if (!temporary)
 		ReplicationSlotPersist();
+
+	result = MyReplicationSlot->data.confirmed_flush;
+
 	ReplicationSlotRelease();
 
-	PG_RETURN_DATUM(result);
+	return result;
+}
+
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	XLogRecPtr	confirmed_flush;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	confirmed_flush = create_logical_replication_slot(NameStr(*name),
+													  NameStr(*plugin),
+													  temporary,
+													  InvalidXLogRecPtr);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = CStringGetTextDatum(NameStr(*name));
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
 }
 
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	char		*plugin;	/* optional argument */
+	bool		temporary;	/* optional argument */
+	XLogRecPtr	confirmed_flush;
+	XLogRecPtr	start_lsn;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
+
+	/* Check type of replication slot */
+	if (!SlotIsLogical(MyReplicationSlot))
+	{
+		ReplicationSlotRelease();
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy physical replication slot to logical replication slot"))));
+	}
+
+	/* Save some values of the source slot before releasing */
+	start_lsn = MyReplicationSlot->data.restart_lsn;
+	plugin = pstrdup(NameStr(MyReplicationSlot->data.plugin));
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
+
+	/* Release it */
+	ReplicationSlotRelease();
+
+	/* Check the optional arguments */
+	if (PG_NARGS() >= 3)
+		plugin = NameStr(*(PG_GETARG_NAME(2)));
+	if (PG_NARGS() >= 4)
+		temporary = PG_GETARG_BOOL(3);
+
+	confirmed_flush = create_logical_replication_slot(NameStr(*dst_name),
+													  plugin,
+													  temporary,
+													  start_lsn);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = CStringGetTextDatum(NameStr(*dst_name));
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
 
 /*
  * SQL function for dropping a replication slot.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index e47ddca..c49f2d2 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -918,6 +918,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 40d54ed..499b0da 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9787,6 +9787,20 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,immediately_reserve,temporary,slot_name,lsn}',
   prosrc => 'pg_create_physical_replication_slot' },
+{ oid => '4008', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot' },
+{ oid => '4009', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{slot_name,dst_name,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot_no_temp' },
 { oid => '3780', descr => 'drop a replication slot',
   proname => 'pg_drop_replication_slot', provolatile => 'v', proparallel => 'u',
   prorettype => 'void', proargtypes => 'name',
@@ -9807,6 +9821,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4005', descr => 'copy up a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name bool',
+  proallargtypes => '{name,name,name,bool,text,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,plugin,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4006', descr => 'copy up a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name',
+  proallargtypes => '{name,name,name,text,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,plugin,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4007', descr => 'copy up a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,text,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c25ac1f..d1643e5 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr start_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index e3a5fe9..8dc91e2 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -7,7 +7,7 @@ use strict;
 use warnings;
 use PostgresNode;
 use TestLib;
-use Test::More tests => 16;
+use Test::More tests => 17;
 use Config;
 
 # Initialize master node
@@ -27,6 +27,11 @@ $node_master->safe_psql('postgres',
 	qq[SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding');]
 );
 
+# Copy logical slot
+$node_master->safe_psql('postgres',
+	qq[SELECT pg_copy_logical_replication_slot('test_slot', 'copy_slot', 'test_decoding', false);]
+);
+
 $node_master->safe_psql('postgres',
 	qq[INSERT INTO decoding_test(x,y) SELECT s, s::text FROM generate_series(1,10) s;]
 );
@@ -37,6 +42,12 @@ my ($result) = $node_master->safe_psql('postgres',
 is(scalar(my @foobar = split /^/m, $result),
 	12, 'Decoding produced 12 rows inc BEGIN/COMMIT');
 
+# Basic decoding works using the copied slot
+$result = $node_master->safe_psql('postgres',
+	qq[SELECT pg_logical_slot_get_changes('copy_slot', NULL, NULL);]);
+is(scalar(@foobar = split /^/m, $result),
+	12, 'Decoding produced 12 rows inc BEGIN/COMMIT using the copied slot');
+
 # If we immediately crash the server we might lose the progress we just made
 # and replay the same changes again. But a clean shutdown should never repeat
 # the same changes when we use the SQL decoding interface.
-- 
2.10.5

#8Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#7)
Re: Copy function for logical replication slots

On Mon, Jul 02, 2018 at 04:31:32PM +0900, Masahiko Sawada wrote:

Attached an updated patch including copy function support for logical
slots as well as physical slots. Please review it.

I had a look at this patch.

As the output plugin can be changed for logical slots, having two
functions is a good thing.

+# Basic decoding works using the copied slot
+$result = $node_master->safe_psql('postgres',
+   qq[SELECT pg_logical_slot_get_changes('copy_slot', NULL, NULL);]);
+is(scalar(@foobar = split /^/m, $result),
+   12, 'Decoding produced 12 rows inc BEGIN/COMMIT using the copied slot');

This could live as well as part of the test suite for test_decoding, and
that would be faster as well. There are more scenarios that could be
tested as well:
- Copy a temporary slot to become permanent and switch its plugin type,
then check the state of the slot in pg_replication_slots.
- Do the reverse, aka copy a permanent slot and make it temporary.
- Checking that the default behaviors are preserved: if persistent is
not changed then the new slot should have the same persistence as the
origin. The same applies for the output plugin, and for both logical
and replication slots.
- Check that the reversed LSN is the same for both the origin and the
target.

+    Copy a existing <parameter>src_slot_name</parameter> physical slot
+    to <parameter>dst_slot_name</parameter>. The copied physical slot starts
+    to reserve WAL from the same LSN as the source slot if the source slot
+    already reserves WAL. <parameter>temporary</parameter> is optional. If
+    <parameter>temporary</parameter> is omitted, the same value of the
+    source slot is used.

Typo here. Copy AN existing slot. I would reword that a bit:
Copies an existing physical replication slot name src_slot_name to a
physical replication slot named dst_slot_name.
Extra one: "the same value AS the source slot is used."

+    Copy a existing <parameter>src_slot_name</parameter> logical (decoding) slot
+    to <parameter>dst_slot_name</parameter> while changing the output plugin
+    and persistence.

There may be no need for "decoding" here, the same phrasing as suggested
above would be nicer I think. For LSN I would suggest to add an
<acronym> markup.

I am not sure if it makes much sense to be able to copy from a slot
which has not yet been used to reserve WAL, but to change the properties
of a slot I could get that forbidding the behavior is not really
intuitive either.

-   ReplicationSlotReserveWal();
+   /* Find start location to read WAL if not specified */
+   if (XLogRecPtrIsInvalid(start_lsn))
+       ReplicationSlotReserveWal();
+   else
+   {
+       SpinLockAcquire(&slot->mutex);
+       slot->data.restart_lsn = start_lsn;
+       SpinLockRelease(&slot->mutex);
+   }
Hmm.  Instead of all this stanza in CreateInitDecodingContext(), I would
have imagined that what should happen is that the new fresh slot gets
created with the same status data as the origin.  This saves quite a bit
of extra post-creation computing, and we are also sure that the origin
slot has consistent data as it is owned by the process doing the copy.

One property which seems important to me is to make sure that the target
slot has the same data as the origin slot once the caller knows that the
copy has completed, and not that the target slot may perhaps have the
same data as the origin while creating the target. Do you see the
difference? Your patch does the latter, because it creates the new slot
after releasing the lock it held on the origin, while I would think that
the former is more important, aka keep the lock for the duration of the
copy. I am not completely sure if that's a property we want to keep,
but that deserves discussion as we should not do a copy while the origin
slot may still be consumed in parallel. For physical slot the copy is
straight-forward, less for logical slots.
--
Michael

#9Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#8)
Re: Copy function for logical replication slots

On Tue, Jul 3, 2018 at 1:01 PM, Michael Paquier <michael@paquier.xyz> wrote:

On Mon, Jul 02, 2018 at 04:31:32PM +0900, Masahiko Sawada wrote:

Attached an updated patch including copy function support for logical
slots as well as physical slots. Please review it.

I had a look at this patch.

Thank you for the reviews.

As the output plugin can be changed for logical slots, having two
functions is a good thing.

+# Basic decoding works using the copied slot
+$result = $node_master->safe_psql('postgres',
+   qq[SELECT pg_logical_slot_get_changes('copy_slot', NULL, NULL);]);
+is(scalar(@foobar = split /^/m, $result),
+   12, 'Decoding produced 12 rows inc BEGIN/COMMIT using the copied slot');

This could live as well as part of the test suite for test_decoding, and
that would be faster as well. There are more scenarios that could be
tested as well:
- Copy a temporary slot to become permanent and switch its plugin type,
then check the state of the slot in pg_replication_slots.
- Do the reverse, aka copy a permanent slot and make it temporary.
- Checking that the default behaviors are preserved: if persistent is
not changed then the new slot should have the same persistence as the
origin. The same applies for the output plugin, and for both logical
and replication slots.
- Check that the reversed LSN is the same for both the origin and the
target.

Will fix.

+    Copy a existing <parameter>src_slot_name</parameter> physical slot
+    to <parameter>dst_slot_name</parameter>. The copied physical slot starts
+    to reserve WAL from the same LSN as the source slot if the source slot
+    already reserves WAL. <parameter>temporary</parameter> is optional. If
+    <parameter>temporary</parameter> is omitted, the same value of the
+    source slot is used.

Typo here. Copy AN existing slot. I would reword that a bit:
Copies an existing physical replication slot name src_slot_name to a
physical replication slot named dst_slot_name.
Extra one: "the same value AS the source slot is used."

+    Copy a existing <parameter>src_slot_name</parameter> logical (decoding) slot
+    to <parameter>dst_slot_name</parameter> while changing the output plugin
+    and persistence.

There may be no need for "decoding" here, the same phrasing as suggested
above would be nicer I think. For LSN I would suggest to add an
<acronym> markup.

Will fix.

I am not sure if it makes much sense to be able to copy from a slot
which has not yet been used to reserve WAL, but to change the properties
of a slot I could get that forbidding the behavior is not really
intuitive either.

-   ReplicationSlotReserveWal();
+   /* Find start location to read WAL if not specified */
+   if (XLogRecPtrIsInvalid(start_lsn))
+       ReplicationSlotReserveWal();
+   else
+   {
+       SpinLockAcquire(&slot->mutex);
+       slot->data.restart_lsn = start_lsn;
+       SpinLockRelease(&slot->mutex);
+   }
Hmm.  Instead of all this stanza in CreateInitDecodingContext(), I would
have imagined that what should happen is that the new fresh slot gets
created with the same status data as the origin.  This saves quite a bit
of extra post-creation computing, and we are also sure that the origin
slot has consistent data as it is owned by the process doing the copy.

Hmm, such post-creation computing is not necessary for the copied
slot? I'm concerned we might miss some operations required for for
logical replication slot.

One property which seems important to me is to make sure that the target
slot has the same data as the origin slot once the caller knows that the
copy has completed, and not that the target slot may perhaps have the
same data as the origin while creating the target. Do you see the
difference? Your patch does the latter, because it creates the new slot
after releasing the lock it held on the origin, while I would think that
the former is more important, aka keep the lock for the duration of the
copy.

Did you mean "the caller" is clients who call
pg_copy_logical_replication_slot function? If so, I think it's very
difficult to ensure the former because the origin slot can advance
during returning the result to the client. Also if we keep the lock on
the origin slot during the copy I think that one problem is that we
will own two replication slots at a time. But there are a lot of code
that premise a process holds at most one replication slot.

I am not completely sure if that's a property we want to keep,
but that deserves discussion as we should not do a copy while the origin
slot may still be consumed in parallel. For physical slot the copy is
straight-forward, less for logical slots.

I think this copy functions ensure that the target slot has the same
values as the origin slot at the time when the function is called.
Isn't it clear?

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

#10Craig Ringer
craig@2ndquadrant.com
In reply to: Masahiko Sawada (#1)
Re: Copy function for logical replication slots

On 28 June 2018 at 10:51, Masahiko Sawada <sawada.mshk@gmail.com> wrote:

Hi,

I'd like to propose a copy function for logical replication slots.
Currently when we create a new logical replication slot it starts to
read WAL from an LSN of the current insert. This function copies a
existing logical replication slot while changing output plugin and
persistence. That is, the copied new replication slot starts from the
same LSN as the source one. Since a new copied slot starts from the
same LSN of existing one we don't need to care about WAL reservation.

Strong agreement from me.

The inability to switch plugins is a massive pain. I've worked around it
with similar functions bundled into the extensions I work with that do
logical decoding related work. It makes sense to have it in core.

A use case I imagined is for investigations for example. I mean that

when replication collision occurs on subscriber there is no way to see
what replicated data is conflicting (perhaps error log helps it but is
not detailed) and there is no way to advance a replication origin in
order to exactly skip to apply conflicting data.

pglogical handles this by letting you peek the slot in a separate json
protocol output mode, but that doesn't help with pgoutput.

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

#11Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#9)
Re: Copy function for logical replication slots

On Tue, Jul 03, 2018 at 05:27:07PM +0900, Masahiko Sawada wrote:

On Tue, Jul 3, 2018 at 1:01 PM, Michael Paquier <michael@paquier.xyz> wrote:

One property which seems important to me is to make sure that the target
slot has the same data as the origin slot once the caller knows that the
copy has completed, and not that the target slot may perhaps have the
same data as the origin while creating the target. Do you see the
difference? Your patch does the latter, because it creates the new slot
after releasing the lock it held on the origin, while I would think that
the former is more important, aka keep the lock for the duration of the
copy.

Did you mean "the caller" is clients who call
pg_copy_logical_replication_slot function? If so, I think it's very
difficult to ensure the former because the origin slot can advance
during returning the result to the client. Also if we keep the lock on
the origin slot during the copy I think that one problem is that we
will own two replication slots at a time. But there are a lot of code
that premise a process holds at most one replication slot.

When I mean the caller, I mean the client in charge of the slot
creation.  Anyway, this bit is really worrying me in your patch:
-   ReplicationSlotReserveWal();
+   /* Find start location to read WAL if not specified */
+   if (XLogRecPtrIsInvalid(start_lsn))
+       ReplicationSlotReserveWal();
+   else
+   {
+       SpinLockAcquire(&slot->mutex);
+       slot->data.restart_lsn = start_lsn;
+       SpinLockRelease(&slot->mutex);
+   }
Your patch simply increases the window mentioned here in slot.c because
there is no interlock between the checkpointer and the process creating
the slot.  Please see here:
/*
 * The replication slot mechanism is used to prevent removal of required
 * WAL. As there is no interlock between this routine and checkpoints, WAL
 * segments could concurrently be removed when a now stale return value of
 * ReplicationSlotsComputeRequiredLSN() is used. In the unlikely case that
 * this happens we'll just retry.
 */
So, slots created without a non-arbitrary start position have at least
the will to restart if a checkpointer is running in parallel, but your
patch increases the window used, and does not even retry nor does it
fail to create the slot if the restart LSN points to a segment not here
anymore.  The trick I think here is that the slot copy should not cause
checkpoint slowdowns, so more thoughts are needed here, and this is not
really trivial.

I am not completely sure if that's a property we want to keep,
but that deserves discussion as we should not do a copy while the origin
slot may still be consumed in parallel. For physical slot the copy is
straight-forward, less for logical slots.

I think this copy functions ensure that the target slot has the same
values as the origin slot at the time when the function is called.
Isn't it clear?

Not really per the point above, the current version of the copy gives no
actual consistency guarantees.
--
Michael

#12Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#11)
Re: Copy function for logical replication slots

On Thu, Jul 5, 2018 at 4:47 PM, Michael Paquier <michael@paquier.xyz> wrote:

On Tue, Jul 03, 2018 at 05:27:07PM +0900, Masahiko Sawada wrote:

On Tue, Jul 3, 2018 at 1:01 PM, Michael Paquier <michael@paquier.xyz> wrote:

One property which seems important to me is to make sure that the target
slot has the same data as the origin slot once the caller knows that the
copy has completed, and not that the target slot may perhaps have the
same data as the origin while creating the target. Do you see the
difference? Your patch does the latter, because it creates the new slot
after releasing the lock it held on the origin, while I would think that
the former is more important, aka keep the lock for the duration of the
copy.

Did you mean "the caller" is clients who call
pg_copy_logical_replication_slot function? If so, I think it's very
difficult to ensure the former because the origin slot can advance
during returning the result to the client. Also if we keep the lock on
the origin slot during the copy I think that one problem is that we
will own two replication slots at a time. But there are a lot of code
that premise a process holds at most one replication slot.

When I mean the caller, I mean the client in charge of the slot
creation.  Anyway, this bit is really worrying me in your patch:
-   ReplicationSlotReserveWal();
+   /* Find start location to read WAL if not specified */
+   if (XLogRecPtrIsInvalid(start_lsn))
+       ReplicationSlotReserveWal();
+   else
+   {
+       SpinLockAcquire(&slot->mutex);
+       slot->data.restart_lsn = start_lsn;
+       SpinLockRelease(&slot->mutex);
+   }
Your patch simply increases the window mentioned here in slot.c because
there is no interlock between the checkpointer and the process creating
the slot.  Please see here:
/*
* The replication slot mechanism is used to prevent removal of required
* WAL. As there is no interlock between this routine and checkpoints, WAL
* segments could concurrently be removed when a now stale return value of
* ReplicationSlotsComputeRequiredLSN() is used. In the unlikely case that
* this happens we'll just retry.
*/
So, slots created without a non-arbitrary start position have at least
the will to restart if a checkpointer is running in parallel, but your
patch increases the window used, and does not even retry nor does it
fail to create the slot if the restart LSN points to a segment not here
anymore.  The trick I think here is that the slot copy should not cause
checkpoint slowdowns, so more thoughts are needed here, and this is not
really trivial.

Yes, you're right. To guarantee that restart LSN of copied slot is
available, it seems to me that it's better to copy new slot while
holding the origin slot as you mentioned before. Since the replication
slot creation code assumes that a process creating a new slot doesn't
have any slots we should save origin slot temporary and create new
one, and then restore it. It might be a bit tricky but would work
fine.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

#13Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#12)
Re: Copy function for logical replication slots

On Thu, Jul 05, 2018 at 05:24:48PM +0900, Masahiko Sawada wrote:

Yes, you're right. To guarantee that restart LSN of copied slot is
available, it seems to me that it's better to copy new slot while
holding the origin slot as you mentioned before. Since the replication
slot creation code assumes that a process creating a new slot doesn't
have any slots we should save origin slot temporary and create new
one, and then restore it.

This will require some refactoring first I think as most of the slot
routines assume that the process owning it is the one doing the calls,
so this has a string smell of a patch set being splitted.

It might be a bit tricky but would work fine.

Sawada-san, will you be able to rewrite this patch soon or should it be
moved to the next commit fest? I would suggest to do the latter as this
is no small work, and this needs careful thoughts.
--
Michael

#14Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#13)
Re: Copy function for logical replication slots

On Fri, Jul 6, 2018 at 9:21 AM, Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Jul 05, 2018 at 05:24:48PM +0900, Masahiko Sawada wrote:

Yes, you're right. To guarantee that restart LSN of copied slot is
available, it seems to me that it's better to copy new slot while
holding the origin slot as you mentioned before. Since the replication
slot creation code assumes that a process creating a new slot doesn't
have any slots we should save origin slot temporary and create new
one, and then restore it.

This will require some refactoring first I think as most of the slot
routines assume that the process owning it is the one doing the calls,
so this has a string smell of a patch set being splitted.

It might be a bit tricky but would work fine.

Sawada-san, will you be able to rewrite this patch soon or should it be
moved to the next commit fest? I would suggest to do the latter as this
is no small work, and this needs careful thoughts.

I think that this patch might be splitted but I will be able to send
an updated patch in the next week. As you suggestion this patch needs
more careful thoughts. I'll move this patch to the next commit fest if
I will not be able to sent it. Is that okay?

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

#15Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#14)
Re: Copy function for logical replication slots

On Mon, Jul 09, 2018 at 10:06:00AM +0900, Masahiko Sawada wrote:

I think that this patch might be splitted but I will be able to send
an updated patch in the next week. As you suggestion this patch needs
more careful thoughts. I'll move this patch to the next commit fest if
I will not be able to sent it. Is that okay?

Fine by me. Thanks for the update.
--
Michael

#16Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#15)
1 attachment(s)
Re: Copy function for logical replication slots

On Mon, Jul 9, 2018 at 10:34 AM, Michael Paquier <michael@paquier.xyz> wrote:

On Mon, Jul 09, 2018 at 10:06:00AM +0900, Masahiko Sawada wrote:

I think that this patch might be splitted but I will be able to send
an updated patch in the next week. As you suggestion this patch needs
more careful thoughts. I'll move this patch to the next commit fest if
I will not be able to sent it. Is that okay?

Fine by me. Thanks for the update.

Attached new version of patch incorporated the all comments I got from
Michael-san.

To prevent the WAL segment file of restart_lsn of the origin slot from
removal during creating the target slot, I've chosen a way to copy new
one while holding the origin one. One problem to implement this way is
that the current replication slot code doesn't allow us to do
straightforwardly; the code assumes that the process creating a new
slot is not holding another slot. So I've changed the copy functions
so that it save temporarily MyReplicationSlot and then restore and
release it after creation the target slot. To ensure that both the
origin and target slot are released properly in failure cases I used
PG_ENSURE_ERROR_CLEANUP(). That way, we can keep the code changes of
the logical decoding at a minimum. I've thought that we can change the
logical decoding code so that it can assumes that the process can have
more than one slots at once but it seems overkill to me.

Please review it.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v4-0001-Copy-function-for-logical-and-physical-replicatio.patchapplication/octet-stream; name=v4-0001-Copy-function-for-logical-and-physical-replicatio.patchDownload
From 17d503ec1970e23806004f6d39dc0318113f467a Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 9 Jul 2018 16:37:17 +0900
Subject: [PATCH v4] Copy function for logical and physical replication slot.

---
 contrib/test_decoding/expected/slot.out   | 228 ++++++++++++++++++
 contrib/test_decoding/logical.conf        |   2 +-
 contrib/test_decoding/sql/slot.sql        |  80 +++++++
 doc/src/sgml/func.sgml                    |  41 ++++
 src/backend/replication/logical/logical.c |   5 +-
 src/backend/replication/slot.c            |  89 ++++---
 src/backend/replication/slotfuncs.c       | 380 ++++++++++++++++++++++++++----
 src/backend/replication/walsender.c       |   3 +-
 src/include/catalog/pg_proc.dat           |  35 +++
 src/include/replication/logical.h         |   1 +
 src/include/replication/slot.h            |   2 +-
 11 files changed, 780 insertions(+), 86 deletions(-)

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 2737a8a..b87ef37 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -148,3 +148,231 @@ SELECT pg_drop_replication_slot('regression_slot3');
  
 (1 row)
 
+--
+-- Test copy function for logical replication slots
+--
+-- Create original logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+-- Preserve all values of the original slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Change output plugin, preserve the persisitence
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Change both output plugin and persistence
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_both', 'pgoutput', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_both', 'pgoutput', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |         slot_name          |    plugin     | temporary 
+------------+---------------+-----------+----------------------------+---------------+-----------
+ orig_slot1 | test_decoding | f         | copied_slot1_change_both   | pgoutput      | t
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin | pgoutput      | f
+ orig_slot1 | test_decoding | f         | copied_slot1_no_change     | test_decoding | f
+ orig_slot2 | test_decoding | t         | copied_slot2_change_both   | pgoutput      | f
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin | pgoutput      | t
+ orig_slot2 | test_decoding | t         | copied_slot2_no_change     | test_decoding | t
+(6 rows)
+
+-- Now we have maximum 8 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase max_replication_slots.
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_change_both');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+--
+-- Test copy function for physical replication slots
+--
+-- Create original physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot3', true, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+-- Preserve all values of the original slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot3', 'copied_slot3_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Change persistence
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot3', 'copied_slot3_temp', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+       slot_name        | slot_type | temporary 
+------------------------+-----------+-----------
+ orig_slot1             | physical  | f
+ orig_slot2             | physical  | f
+ orig_slot3             | physical  | t
+ copied_slot1_no_change | physical  | f
+ copied_slot2_no_change | physical  | f
+ copied_slot3_no_change | physical  | t
+ copied_slot1_temp      | physical  | t
+ copied_slot3_temp      | physical  | f
+(8 rows)
+
+SELECT
+    o.slot_name, c.slot_name
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |       slot_name        
+------------+------------------------
+ orig_slot2 | copied_slot2_no_change
+ orig_slot2 | copied_slot3_no_change
+ orig_slot2 | copied_slot3_temp
+ orig_slot2 | orig_slot3
+ orig_slot3 | copied_slot2_no_change
+ orig_slot3 | copied_slot3_no_change
+ orig_slot3 | copied_slot3_temp
+ orig_slot3 | orig_slot2
+(8 rows)
+
+-- Now we have maximum 8 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase max_replication_slots.
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot3_temp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/logical.conf b/contrib/test_decoding/logical.conf
index 367f706..bc09e43 100644
--- a/contrib/test_decoding/logical.conf
+++ b/contrib/test_decoding/logical.conf
@@ -1,2 +1,2 @@
 wal_level = logical
-max_replication_slots = 4
+max_replication_slots = 8
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index 24cdf71..7a846f3 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -74,3 +74,83 @@ SELECT slot_name FROM pg_create_physical_replication_slot('regression_slot3');
 SELECT pg_replication_slot_advance('regression_slot3', '0/0'); -- invalid LSN
 SELECT pg_replication_slot_advance('regression_slot3', '0/1'); -- error
 SELECT pg_drop_replication_slot('regression_slot3');
+
+--
+-- Test copy function for logical replication slots
+--
+
+-- Create original logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+
+-- Preserve all values of the original slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+
+-- Change output plugin, preserve the persisitence
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', 'pgoutput');
+
+-- Change both output plugin and persistence
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_both', 'pgoutput', true);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_both', 'pgoutput', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Now we have maximum 8 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+SELECT pg_drop_replication_slot('copied_slot2_change_both');
+
+--
+-- Test copy function for physical replication slots
+--
+
+-- Create original physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', false, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot3', true, true);
+
+-- Preserve all values of the original slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot3', 'copied_slot3_no_change');
+
+-- Change persistence
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot3', 'copied_slot3_temp', false);
+
+-- Check all copied slots status
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+SELECT
+    o.slot_name, c.slot_name
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Now we have maximum 8 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+SELECT pg_drop_replication_slot('copied_slot3_temp');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index edc9be9..93aff1b 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19252,6 +19252,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>temporary</parameter> <type>bool</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing physical replication slot name <parameter>src_slot_name</parameter>
+        to a physical replicaiton slot named <parameter>dst_slot_name</parameter>.
+        The copied physical slot starts to reserve WAL from the same <acronym>LSN</acronym> as the
+        source slot if the source slot already reserves WAL.
+        <parameter>temporary</parameter> is optional. If <parameter>temporary</parameter>
+        is omitted, the same value as the source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing logical replication slot name <parameter>src_slot_name</parameter>
+        to a logical replication slot named <parameter>dst_slot_name</parameter>
+        while changing the output plugin and persistence. The copied logical slot starts
+        from the same <acronym>LSN</acronym> as the source logical slot. Both <parameter>plugin</parameter> and
+        <parameter>temporary</parameter> are optional. If <parameter>plugin</parameter>
+        or <parameter>temporary</parameter> are omitted, the same values as
+        the source logical slot are used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index c2d0e0c..5b0381e 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -223,6 +223,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -266,7 +267,7 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	ReplicationSlotReserveWal(restart_lsn);
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -311,7 +312,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, restart_lsn, xmin_horizon,
 								 need_full_snapshot, true,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index fb95b44..514a1f9 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -986,11 +986,13 @@ CheckSlotRequirements(void)
 /*
  * Reserve WAL for the currently active slot.
  *
- * Compute and set restart_lsn in a manner that's appropriate for the type of
- * the slot and concurrency safe.
+ * If an lsn to reserve is not requested, compute and set restart_lsn
+ * in a manner that's appropriate for the type of the slot and concurrency safe.
+ * If requested, set restart_lsn and check if the corresponding wal segment
+ * is available.
  */
 void
-ReplicationSlotReserveWal(void)
+ReplicationSlotReserveWal(XLogRecPtr requested_lsn)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
 
@@ -1001,47 +1003,57 @@ ReplicationSlotReserveWal(void)
 	 * The replication slot mechanism is used to prevent removal of required
 	 * WAL. As there is no interlock between this routine and checkpoints, WAL
 	 * segments could concurrently be removed when a now stale return value of
-	 * ReplicationSlotsComputeRequiredLSN() is used. In the unlikely case that
-	 * this happens we'll just retry.
+	 * ReplicationSlotsComputeRequiredLSN() is used. If the lsn to reserve is
+	 * not requested, in the unlikely case that this happens we'll just retry.
 	 */
 	while (true)
 	{
 		XLogSegNo	segno;
 		XLogRecPtr	restart_lsn;
 
-		/*
-		 * For logical slots log a standby snapshot and start logical decoding
-		 * at exactly that position. That allows the slot to start up more
-		 * quickly.
-		 *
-		 * That's not needed (or indeed helpful) for physical slots as they'll
-		 * start replay at the last logged checkpoint anyway. Instead return
-		 * the location of the last redo LSN. While that slightly increases
-		 * the chance that we have to retry, it's where a base backup has to
-		 * start replay at.
-		 */
-		if (!RecoveryInProgress() && SlotIsLogical(slot))
+		if (!XLogRecPtrIsInvalid(requested_lsn))
 		{
-			XLogRecPtr	flushptr;
-
-			/* start at current insert position */
-			restart_lsn = GetXLogInsertRecPtr();
+			/* Set the requested lsn */
 			SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
+			slot->data.restart_lsn = requested_lsn;
 			SpinLockRelease(&slot->mutex);
-
-			/* make sure we have enough information to start */
-			flushptr = LogStandbySnapshot();
-
-			/* and make sure it's fsynced to disk */
-			XLogFlush(flushptr);
 		}
 		else
 		{
-			restart_lsn = GetRedoRecPtr();
-			SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
-			SpinLockRelease(&slot->mutex);
+			/*
+			 * For logical slots log a standby snapshot and start logical decoding
+			 * at exactly that position. That allows the slot to start up more
+			 * quickly.
+			 *
+			 * That's not needed (or indeed helpful) for physical slots as they'll
+			 * start replay at the last logged checkpoint anyway. Instead return
+			 * the location of the last redo LSN. While that slightly increases
+			 * the chance that we have to retry, it's where a base backup has to
+			 * start replay at.
+			 */
+			if (!RecoveryInProgress() && SlotIsLogical(slot))
+			{
+				XLogRecPtr	flushptr;
+
+				/* start at current insert position */
+				restart_lsn = GetXLogInsertRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+
+				/* make sure we have enough information to start */
+				flushptr = LogStandbySnapshot();
+
+				/* and make sure it's fsynced to disk */
+				XLogFlush(flushptr);
+			}
+			else
+			{
+				restart_lsn = GetRedoRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+			}
 		}
 
 		/* prevent WAL removal as fast as possible */
@@ -1057,6 +1069,19 @@ ReplicationSlotReserveWal(void)
 		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
 		if (XLogGetLastRemovedSegno() < segno)
 			break;
+
+		/*
+		 * The requested wal lsn is no longer available. We don't want to retry
+		 * it, so raise an error.
+		 */
+		if (!XLogRecPtrIsInvalid(restart_lsn))
+		{
+			char filename[MAXFNAMELEN];
+
+			XLogFileName(filename, ThisTimeLineID, segno, wal_segment_size);
+			ereport(ERROR,
+					(errmsg("could not reserve WAL segment %s", filename)));
+		}
 	}
 }
 
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 23af323..d3d26e8 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -17,10 +17,12 @@
 #include "miscadmin.h"
 
 #include "access/htup_details.h"
+#include "access/xlog_internal.h"
 #include "replication/decode.h"
 #include "replication/slot.h"
 #include "replication/logical.h"
 #include "replication/logicalfuncs.h"
+#include "storage/ipc.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -36,86 +38,228 @@ check_permissions(void)
 }
 
 /*
- * SQL function for creating a new physical (streaming replication)
- * replication slot.
+ * Error cleanup callback for copy replication slot functions. Release
+ * both MyReplicationSlot and the saved replication slot.
  */
-Datum
-pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
+static void
+copy_replication_slot_callback(int code, Datum arg)
 {
-	Name		name = PG_GETARG_NAME(0);
-	bool		immediately_reserve = PG_GETARG_BOOL(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-	Datum		values[2];
-	bool		nulls[2];
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
+	ReplicationSlot	*savedslot = (ReplicationSlot *) DatumGetPointer(arg);
 
-	Assert(!MyReplicationSlot);
+	if (MyReplicationSlot)
+		ReplicationSlotRelease();
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
+	/* Release the saved slot if exist while preventing double releasing */
+	if (savedslot && savedslot != MyReplicationSlot)
+	{
+		MyReplicationSlot = savedslot;
+		ReplicationSlotRelease();
+	}
+}
+
+/*
+ * Helper function for creating a new physical replication slot with
+ * given arguments. Return a restart_lsn of new replication slot or
+ * InvalidXLogRecPtr if WAL reservation is not required.
+ */
+static XLogRecPtr
+create_physical_replication_slot(char *name, bool immediately_reserve,
+								 bool temporary, XLogRecPtr restart_lsn)
+{
+	XLogRecPtr	result = InvalidXLogRecPtr;
+
+	Assert(!MyReplicationSlot);
 
 	check_permissions();
 
 	CheckSlotRequirements();
 
 	/* acquire replication slot, this will check for conflicting names */
-	ReplicationSlotCreate(NameStr(*name), false,
+	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	nulls[0] = false;
-
 	if (immediately_reserve)
 	{
 		/* Reserve WAL as the user asked for it */
-		ReplicationSlotReserveWal();
+		ReplicationSlotReserveWal(restart_lsn);
 
 		/* Write this slot to disk */
 		ReplicationSlotMarkDirty();
 		ReplicationSlotSave();
 
-		values[1] = LSNGetDatum(MyReplicationSlot->data.restart_lsn);
-		nulls[1] = false;
+		result = MyReplicationSlot->data.restart_lsn;
 	}
+
+	ReplicationSlotRelease();
+
+	return result;
+}
+
+/*
+ * SQL function for creating a new physical (streaming replication)
+ * replication slot.
+ */
+Datum
+pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	bool		immediately_reserve = PG_GETARG_BOOL(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	Datum		values[2];
+	bool		nulls[2];
+	XLogRecPtr	result_lsn;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		result;
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	result_lsn = create_physical_replication_slot(NameStr(*name),
+												  immediately_reserve,
+												  temporary,
+												  InvalidXLogRecPtr);
+
+	values[0] = NameGetDatum(name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
 	else
 	{
-		nulls[1] = true;
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
 	}
 
 	tuple = heap_form_tuple(tupdesc, values, nulls);
 	result = HeapTupleGetDatum(tuple);
 
-	ReplicationSlotRelease();
-
 	PG_RETURN_DATUM(result);
 }
 
-
 /*
- * SQL function for creating a new logical replication slot.
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
  */
 Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
-	LogicalDecodingContext *ctx = NULL;
+	return pg_copy_physical_replication_slot(fcinfo);
+}
 
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
+/*
+ * SQL function for copying a physical replication slot.
+ */
+Datum
+pg_copy_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	bool		temporary; /* optional argument */
+	bool		immediately_reserve;
+	ReplicationSlot	*saveslot = NULL;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	result_lsn;
 	Datum		values[2];
 	bool		nulls[2];
-
-	Assert(!MyReplicationSlot);
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
 
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
 
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
+
+	/* Check type of replication slot */
+	if (SlotIsLogical(MyReplicationSlot))
+	{
+		ReplicationSlotRelease();
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a logical replication slot to a physical replication slot"))));
+	}
+
+	/* Save values of the source slot */
+	restart_lsn = MyReplicationSlot->data.restart_lsn;
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
+
+	/* Reserve WAL at creation if the source slot already reserves */
+	immediately_reserve = !XLogRecPtrIsInvalid(restart_lsn);
+
+	/* check the optional argument */
+	if (PG_NARGS() >= 3)
+		temporary = PG_GETARG_BOOL(2);
+
+	/*
+	 * To prevent the restart_lsn WAL of the source slot from removal
+	 * during copying a new slot, we copy it while holding the source slot.
+	 * Since we are not allowed to create a new one while holding another
+	 * one, we temporarily save the acquired slot and restore it after
+	 * creation. Set callback function to ensure we release replication
+	 * slots if fail below.
+	 */
+	if (immediately_reserve)
+		saveslot = MyReplicationSlot;
+	else
+		ReplicationSlotRelease();
+
+	PG_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+	{
+		if (immediately_reserve)
+			MyReplicationSlot = NULL;
+
+		result_lsn = create_physical_replication_slot(NameStr(*dst_name),
+													  immediately_reserve,
+													  temporary,
+													  restart_lsn);
+		Assert(MyReplicationSlot == NULL);
+
+		/*
+		 * Restore source slot, if saved. We must not change the saveslot
+		 * to cancel the callback function.
+		 */
+		if (saveslot)
+			MyReplicationSlot = saveslot;
+	}
+	PG_END_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+
+	/* Release the source slot, if not yet */
+	if (immediately_reserve)
+		ReplicationSlotRelease();
+
+	values[0] = NameGetDatum(dst_name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
+	else
+	{
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
+	}
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
+}
+
+/*
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Return a confirmed_lsn of new replication slot.
+ */
+static XLogRecPtr
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr start_lsn)
+{
+	LogicalDecodingContext *ctx = NULL;
+	XLogRecPtr	result;
+
+	Assert(!MyReplicationSlot);
+
 	check_permissions();
 
 	CheckLogicalDecodingRequirements();
@@ -128,39 +272,177 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									start_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = CStringGetTextDatum(NameStr(MyReplicationSlot->data.name));
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
 
-	memset(nulls, 0, sizeof(nulls));
-
-	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
-
 	/* ok, slot is now fully created, mark it as persistent if needed */
 	if (!temporary)
 		ReplicationSlotPersist();
+
+	result = MyReplicationSlot->data.confirmed_flush;
+
 	ReplicationSlotRelease();
 
-	PG_RETURN_DATUM(result);
+	return result;
 }
 
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	XLogRecPtr	confirmed_flush;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	confirmed_flush = create_logical_replication_slot(NameStr(*name),
+													  NameStr(*plugin),
+													  temporary,
+													  InvalidXLogRecPtr);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = CStringGetTextDatum(NameStr(*name));
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	char		*plugin;	/* optional argument */
+	bool		temporary;	/* optional argument */
+	ReplicationSlot *saveslot = NULL;
+	XLogRecPtr	confirmed_flush;
+	XLogRecPtr	restart_lsn;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
+
+	/* Check type of replication slot */
+	if (SlotIsPhysical(MyReplicationSlot))
+	{
+		ReplicationSlotRelease();
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a physical replication slot to a logical replication slot"))));
+	}
+
+	/* Save values of the source slot */
+	restart_lsn = MyReplicationSlot->data.restart_lsn;
+	plugin = pstrdup(NameStr(MyReplicationSlot->data.plugin));
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
+
+	/* Check the optional arguments */
+	if (PG_NARGS() >= 3)
+		plugin = NameStr(*(PG_GETARG_NAME(2)));
+	if (PG_NARGS() >= 4)
+		temporary = PG_GETARG_BOOL(3);
+
+	/*
+	 * To prevent the restart_lsn WAL of the source slot from removal
+	 * during copying a new slot, we copy it while holding the source slot.
+	 * Since we are not allowed to create a new one while holding another
+	 * one, we temporarily save the acquired slot and restore it after
+	 * creation. Set callback function to ensure we release replication
+	 * slots if fail below.
+	 */
+	saveslot = MyReplicationSlot;
+	PG_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+	{
+		MyReplicationSlot = NULL;
+
+		confirmed_flush = create_logical_replication_slot(NameStr(*dst_name),
+														  plugin,
+														  temporary,
+														  restart_lsn);
+		Assert(MyReplicationSlot == NULL);
+
+		/*
+		 * Restore source slot. We must not change the saveslot to cancel the
+		 * callback function.
+		 */
+		MyReplicationSlot = saveslot;
+	}
+	PG_END_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+
+	/* Release the source slot */
+	ReplicationSlotRelease();
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = NameGetDatum(dst_name);
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
 
 /*
  * SQL function for dropping a replication slot.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index e47ddca..85c47b5 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -918,6 +918,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
@@ -960,7 +961,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	}
 	else if (cmd->kind == REPLICATION_KIND_PHYSICAL && reserve_wal)
 	{
-		ReplicationSlotReserveWal();
+		ReplicationSlotReserveWal(InvalidXLogRecPtr);
 
 		ReplicationSlotMarkDirty();
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 00b59fd..be33d7f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9787,6 +9787,20 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,immediately_reserve,temporary,slot_name,lsn}',
   prosrc => 'pg_create_physical_replication_slot' },
+{ oid => '4008', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot' },
+{ oid => '4009', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{slot_name,dst_name,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot_no_temp' },
 { oid => '3780', descr => 'drop a replication slot',
   proname => 'pg_drop_replication_slot', provolatile => 'v', proparallel => 'u',
   prorettype => 'void', proargtypes => 'name',
@@ -9807,6 +9821,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4005', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name bool',
+  proallargtypes => '{name,name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,plugin,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4006', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name',
+  proallargtypes => '{name,name,name,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,plugin,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4007', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c25ac1f..afc32ff 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 7964ae2..d5c8953 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -193,7 +193,7 @@ extern void ReplicationSlotMarkDirty(void);
 
 /* misc stuff */
 extern bool ReplicationSlotValidateName(const char *name, int elevel);
-extern void ReplicationSlotReserveWal(void);
+extern void ReplicationSlotReserveWal(XLogRecPtr requested_lsn);
 extern void ReplicationSlotsComputeRequiredXmin(bool already_locked);
 extern void ReplicationSlotsComputeRequiredLSN(void);
 extern XLogRecPtr ReplicationSlotsComputeLogicalRestartLSN(void);
-- 
2.10.5

#17Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Masahiko Sawada (#16)
1 attachment(s)
Re: Copy function for logical replication slots

On Thu, Jul 12, 2018 at 9:28 PM, Masahiko Sawada <sawada.mshk@gmail.com> wrote:

On Mon, Jul 9, 2018 at 10:34 AM, Michael Paquier <michael@paquier.xyz> wrote:

On Mon, Jul 09, 2018 at 10:06:00AM +0900, Masahiko Sawada wrote:

I think that this patch might be splitted but I will be able to send
an updated patch in the next week. As you suggestion this patch needs
more careful thoughts. I'll move this patch to the next commit fest if
I will not be able to sent it. Is that okay?

Fine by me. Thanks for the update.

Attached new version of patch incorporated the all comments I got from
Michael-san.

To prevent the WAL segment file of restart_lsn of the origin slot from
removal during creating the target slot, I've chosen a way to copy new
one while holding the origin one. One problem to implement this way is
that the current replication slot code doesn't allow us to do
straightforwardly; the code assumes that the process creating a new
slot is not holding another slot. So I've changed the copy functions
so that it save temporarily MyReplicationSlot and then restore and
release it after creation the target slot. To ensure that both the
origin and target slot are released properly in failure cases I used
PG_ENSURE_ERROR_CLEANUP(). That way, we can keep the code changes of
the logical decoding at a minimum. I've thought that we can change the
logical decoding code so that it can assumes that the process can have
more than one slots at once but it seems overkill to me.

Please review it.

The previous patch conflicts with the current HEAD. Attached updated
version patch.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v5-0001-Copy-function-for-logical-and-physical-replicatio.patchapplication/octet-stream; name=v5-0001-Copy-function-for-logical-and-physical-replicatio.patchDownload
From 78b09b3cfc8e4d9201d0a059838399413910dbed Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 9 Jul 2018 16:37:17 +0900
Subject: [PATCH v5] Copy function for logical and physical replication slot.

---
 contrib/test_decoding/expected/slot.out   | 228 ++++++++++++++++++
 contrib/test_decoding/logical.conf        |   2 +-
 contrib/test_decoding/sql/slot.sql        |  80 +++++++
 doc/src/sgml/func.sgml                    |  41 ++++
 src/backend/replication/logical/logical.c |   5 +-
 src/backend/replication/slot.c            |  89 ++++---
 src/backend/replication/slotfuncs.c       | 378 ++++++++++++++++++++++++++----
 src/backend/replication/walsender.c       |   3 +-
 src/include/catalog/pg_proc.dat           |  35 +++
 src/include/replication/logical.h         |   1 +
 src/include/replication/slot.h            |   2 +-
 11 files changed, 779 insertions(+), 85 deletions(-)

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 523621a..8775436 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -150,3 +150,231 @@ SELECT pg_drop_replication_slot('regression_slot3');
  
 (1 row)
 
+--
+-- Test copy function for logical replication slots
+--
+-- Create original logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+-- Preserve all values of the original slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Change output plugin, preserve the persisitence
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Change both output plugin and persistence
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_both', 'pgoutput', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_both', 'pgoutput', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |         slot_name          |    plugin     | temporary 
+------------+---------------+-----------+----------------------------+---------------+-----------
+ orig_slot1 | test_decoding | f         | copied_slot1_change_both   | pgoutput      | t
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin | pgoutput      | f
+ orig_slot1 | test_decoding | f         | copied_slot1_no_change     | test_decoding | f
+ orig_slot2 | test_decoding | t         | copied_slot2_change_both   | pgoutput      | f
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin | pgoutput      | t
+ orig_slot2 | test_decoding | t         | copied_slot2_no_change     | test_decoding | t
+(6 rows)
+
+-- Now we have maximum 8 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase max_replication_slots.
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_change_both');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+--
+-- Test copy function for physical replication slots
+--
+-- Create original physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', false, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot3', true, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+-- Preserve all values of the original slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot3', 'copied_slot3_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Change persistence
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot3', 'copied_slot3_temp', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+       slot_name        | slot_type | temporary 
+------------------------+-----------+-----------
+ orig_slot1             | physical  | f
+ orig_slot2             | physical  | f
+ orig_slot3             | physical  | t
+ copied_slot1_no_change | physical  | f
+ copied_slot2_no_change | physical  | f
+ copied_slot3_no_change | physical  | t
+ copied_slot1_temp      | physical  | t
+ copied_slot3_temp      | physical  | f
+(8 rows)
+
+SELECT
+    o.slot_name, c.slot_name
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |       slot_name        
+------------+------------------------
+ orig_slot2 | copied_slot2_no_change
+ orig_slot2 | copied_slot3_no_change
+ orig_slot2 | copied_slot3_temp
+ orig_slot2 | orig_slot3
+ orig_slot3 | copied_slot2_no_change
+ orig_slot3 | copied_slot3_no_change
+ orig_slot3 | copied_slot3_temp
+ orig_slot3 | orig_slot2
+(8 rows)
+
+-- Now we have maximum 8 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase max_replication_slots.
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot3_temp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/logical.conf b/contrib/test_decoding/logical.conf
index 367f706..bc09e43 100644
--- a/contrib/test_decoding/logical.conf
+++ b/contrib/test_decoding/logical.conf
@@ -1,2 +1,2 @@
 wal_level = logical
-max_replication_slots = 4
+max_replication_slots = 8
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index c8d08f8..2f98ecb 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -76,3 +76,83 @@ SELECT slot_name FROM pg_create_physical_replication_slot('regression_slot3');
 SELECT pg_replication_slot_advance('regression_slot3', '0/0'); -- invalid LSN
 SELECT pg_replication_slot_advance('regression_slot3', '0/1'); -- error
 SELECT pg_drop_replication_slot('regression_slot3');
+
+--
+-- Test copy function for logical replication slots
+--
+
+-- Create original logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+
+-- Preserve all values of the original slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+
+-- Change output plugin, preserve the persisitence
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', 'pgoutput');
+
+-- Change both output plugin and persistence
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_both', 'pgoutput', true);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_both', 'pgoutput', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Now we have maximum 8 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+SELECT pg_drop_replication_slot('copied_slot2_change_both');
+
+--
+-- Test copy function for physical replication slots
+--
+
+-- Create original physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', false, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, false);
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot3', true, true);
+
+-- Preserve all values of the original slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot3', 'copied_slot3_no_change');
+
+-- Change persistence
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot3', 'copied_slot3_temp', false);
+
+-- Check all copied slots status
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+SELECT
+    o.slot_name, c.slot_name
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Now we have maximum 8 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+SELECT pg_drop_replication_slot('copied_slot3_temp');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index edc9be9..93aff1b 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19252,6 +19252,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>temporary</parameter> <type>bool</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing physical replication slot name <parameter>src_slot_name</parameter>
+        to a physical replicaiton slot named <parameter>dst_slot_name</parameter>.
+        The copied physical slot starts to reserve WAL from the same <acronym>LSN</acronym> as the
+        source slot if the source slot already reserves WAL.
+        <parameter>temporary</parameter> is optional. If <parameter>temporary</parameter>
+        is omitted, the same value as the source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing logical replication slot name <parameter>src_slot_name</parameter>
+        to a logical replication slot named <parameter>dst_slot_name</parameter>
+        while changing the output plugin and persistence. The copied logical slot starts
+        from the same <acronym>LSN</acronym> as the source logical slot. Both <parameter>plugin</parameter> and
+        <parameter>temporary</parameter> are optional. If <parameter>plugin</parameter>
+        or <parameter>temporary</parameter> are omitted, the same values as
+        the source logical slot are used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index bb83fc9..cbf1f12 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -223,6 +223,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -266,7 +267,7 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	ReplicationSlotReserveWal(restart_lsn);
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -311,7 +312,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, restart_lsn, xmin_horizon,
 								 need_full_snapshot, false,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 19978d9..e575f3a 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -986,11 +986,13 @@ CheckSlotRequirements(void)
 /*
  * Reserve WAL for the currently active slot.
  *
- * Compute and set restart_lsn in a manner that's appropriate for the type of
- * the slot and concurrency safe.
+ * If an lsn to reserve is not requested, compute and set restart_lsn
+ * in a manner that's appropriate for the type of the slot and concurrency safe.
+ * If the reseved WAL is requested, set restart_lsn and check if the corresponding
+ * wal segment is available.
  */
 void
-ReplicationSlotReserveWal(void)
+ReplicationSlotReserveWal(XLogRecPtr requested_lsn)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
 
@@ -1001,47 +1003,57 @@ ReplicationSlotReserveWal(void)
 	 * The replication slot mechanism is used to prevent removal of required
 	 * WAL. As there is no interlock between this routine and checkpoints, WAL
 	 * segments could concurrently be removed when a now stale return value of
-	 * ReplicationSlotsComputeRequiredLSN() is used. In the unlikely case that
-	 * this happens we'll just retry.
+	 * ReplicationSlotsComputeRequiredLSN() is used. If the lsn to reserve is
+	 * not requested, in the unlikely case that this happens we'll just retry.
 	 */
 	while (true)
 	{
 		XLogSegNo	segno;
 		XLogRecPtr	restart_lsn;
 
-		/*
-		 * For logical slots log a standby snapshot and start logical decoding
-		 * at exactly that position. That allows the slot to start up more
-		 * quickly.
-		 *
-		 * That's not needed (or indeed helpful) for physical slots as they'll
-		 * start replay at the last logged checkpoint anyway. Instead return
-		 * the location of the last redo LSN. While that slightly increases
-		 * the chance that we have to retry, it's where a base backup has to
-		 * start replay at.
-		 */
-		if (!RecoveryInProgress() && SlotIsLogical(slot))
+		if (!XLogRecPtrIsInvalid(requested_lsn))
 		{
-			XLogRecPtr	flushptr;
-
-			/* start at current insert position */
-			restart_lsn = GetXLogInsertRecPtr();
+			/* Set the requested lsn */
 			SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
+			slot->data.restart_lsn = requested_lsn;
 			SpinLockRelease(&slot->mutex);
-
-			/* make sure we have enough information to start */
-			flushptr = LogStandbySnapshot();
-
-			/* and make sure it's fsynced to disk */
-			XLogFlush(flushptr);
 		}
 		else
 		{
-			restart_lsn = GetRedoRecPtr();
-			SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
-			SpinLockRelease(&slot->mutex);
+			/*
+			 * For logical slots log a standby snapshot and start logical decoding
+			 * at exactly that position. That allows the slot to start up more
+			 * quickly.
+			 *
+			 * That's not needed (or indeed helpful) for physical slots as they'll
+			 * start replay at the last logged checkpoint anyway. Instead return
+			 * the location of the last redo LSN. While that slightly increases
+			 * the chance that we have to retry, it's where a base backup has to
+			 * start replay at.
+			 */
+			if (!RecoveryInProgress() && SlotIsLogical(slot))
+			{
+				XLogRecPtr	flushptr;
+
+				/* start at current insert position */
+				restart_lsn = GetXLogInsertRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+
+				/* make sure we have enough information to start */
+				flushptr = LogStandbySnapshot();
+
+				/* and make sure it's fsynced to disk */
+				XLogFlush(flushptr);
+			}
+			else
+			{
+				restart_lsn = GetRedoRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+			}
 		}
 
 		/* prevent WAL removal as fast as possible */
@@ -1057,6 +1069,19 @@ ReplicationSlotReserveWal(void)
 		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
 		if (XLogGetLastRemovedSegno() < segno)
 			break;
+
+		/*
+		 * The requested wal lsn is no longer available. We don't want to retry
+		 * it, so raise an error.
+		 */
+		if (!XLogRecPtrIsInvalid(requested_lsn))
+		{
+			char filename[MAXFNAMELEN];
+
+			XLogFileName(filename, ThisTimeLineID, segno, wal_segment_size);
+			ereport(ERROR,
+					(errmsg("could not reserve WAL segment %s", filename)));
+		}
 	}
 }
 
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 8782bad..a5dffb3 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -17,10 +17,12 @@
 #include "miscadmin.h"
 
 #include "access/htup_details.h"
+#include "access/xlog_internal.h"
 #include "replication/decode.h"
 #include "replication/slot.h"
 #include "replication/logical.h"
 #include "replication/logicalfuncs.h"
+#include "storage/ipc.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -36,6 +38,64 @@ check_permissions(void)
 }
 
 /*
+ * Error cleanup callback for copy replication slot functions. Release
+ * both MyReplicationSlot and the saved replication slot.
+ */
+static void
+copy_replication_slot_callback(int code, Datum arg)
+{
+	ReplicationSlot	*savedslot = (ReplicationSlot *) DatumGetPointer(arg);
+
+	if (MyReplicationSlot)
+		ReplicationSlotRelease();
+
+	/* Release the saved slot if exist while preventing double releasing */
+	if (savedslot && savedslot != MyReplicationSlot)
+	{
+		MyReplicationSlot = savedslot;
+		ReplicationSlotRelease();
+	}
+}
+
+/*
+ * Helper function for creating a new physical replication slot with
+ * given arguments. Return a restart_lsn of new replication slot or
+ * InvalidXLogRecPtr if WAL reservation is not required.
+ */
+static XLogRecPtr
+create_physical_replication_slot(char *name, bool immediately_reserve,
+								 bool temporary, XLogRecPtr restart_lsn)
+{
+	XLogRecPtr	result = InvalidXLogRecPtr;
+
+	Assert(!MyReplicationSlot);
+
+	check_permissions();
+
+	CheckSlotRequirements();
+
+	/* acquire replication slot, this will check for conflicting names */
+	ReplicationSlotCreate(name, false,
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+
+	if (immediately_reserve)
+	{
+		/* Reserve WAL as the user asked for it */
+		ReplicationSlotReserveWal(restart_lsn);
+
+		/* Write this slot to disk */
+		ReplicationSlotMarkDirty();
+		ReplicationSlotSave();
+
+		result = MyReplicationSlot->data.restart_lsn;
+	}
+
+	ReplicationSlotRelease();
+
+	return result;
+}
+
+/*
  * SQL function for creating a new physical (streaming replication)
  * replication slot.
  */
@@ -47,75 +107,159 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 	bool		temporary = PG_GETARG_BOOL(2);
 	Datum		values[2];
 	bool		nulls[2];
+	XLogRecPtr	result_lsn;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
 	Datum		result;
 
-	Assert(!MyReplicationSlot);
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	result_lsn = create_physical_replication_slot(NameStr(*name),
+												  immediately_reserve,
+												  temporary,
+												  InvalidXLogRecPtr);
+
+	values[0] = NameGetDatum(name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
+	else
+	{
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
+	}
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_physical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a physical replication slot.
+ */
+Datum
+pg_copy_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	bool		temporary; /* optional argument */
+	bool		immediately_reserve;
+	ReplicationSlot	*saveslot = NULL;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	result_lsn;
+	Datum		values[2];
+	bool		nulls[2];
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
 
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
 
-	check_permissions();
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
 
-	CheckSlotRequirements();
+	/* Check type of replication slot */
+	if (SlotIsLogical(MyReplicationSlot))
+	{
+		ReplicationSlotRelease();
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a logical replication slot to a physical replication slot"))));
+	}
 
-	/* acquire replication slot, this will check for conflicting names */
-	ReplicationSlotCreate(NameStr(*name), false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+	/* Save values of the source slot */
+	restart_lsn = MyReplicationSlot->data.restart_lsn;
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	nulls[0] = false;
+	/* Reserve WAL at creation if the source slot already reserves */
+	immediately_reserve = !XLogRecPtrIsInvalid(restart_lsn);
+
+	/* check the optional argument */
+	if (PG_NARGS() >= 3)
+		temporary = PG_GETARG_BOOL(2);
 
+	/*
+	 * To prevent the restart_lsn WAL of the source slot from removal
+	 * during copying a new slot, we copy it while holding the source slot.
+	 * Since we are not allowed to create a new one while holding another
+	 * one, we temporarily save the acquired slot and restore it after
+	 * creation. Set callback function to ensure we release replication
+	 * slots if fail below.
+	 */
 	if (immediately_reserve)
+		saveslot = MyReplicationSlot;
+	else
+		ReplicationSlotRelease();
+
+	PG_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
 	{
-		/* Reserve WAL as the user asked for it */
-		ReplicationSlotReserveWal();
+		if (immediately_reserve)
+			MyReplicationSlot = NULL;
 
-		/* Write this slot to disk */
-		ReplicationSlotMarkDirty();
-		ReplicationSlotSave();
+		result_lsn = create_physical_replication_slot(NameStr(*dst_name),
+													  immediately_reserve,
+													  temporary,
+													  restart_lsn);
+		Assert(MyReplicationSlot == NULL);
 
-		values[1] = LSNGetDatum(MyReplicationSlot->data.restart_lsn);
-		nulls[1] = false;
+		/*
+		 * Restore source slot, if saved. We must not change the saveslot
+		 * to cancel the callback function.
+		 */
+		if (saveslot)
+			MyReplicationSlot = saveslot;
 	}
+	PG_END_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+
+	/* Release the source slot, if not yet */
+	if (immediately_reserve)
+		ReplicationSlotRelease();
+
+	values[0] = NameGetDatum(dst_name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
 	else
 	{
-		nulls[1] = true;
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
 	}
 
 	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
-
-	ReplicationSlotRelease();
 
-	PG_RETURN_DATUM(result);
+	PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
 }
 
-
 /*
- * SQL function for creating a new logical replication slot.
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Return a confirmed_lsn of new replication slot.
  */
-Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+static XLogRecPtr
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr start_lsn)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
 	LogicalDecodingContext *ctx = NULL;
-
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
-	Datum		values[2];
-	bool		nulls[2];
+	XLogRecPtr	result;
 
 	Assert(!MyReplicationSlot);
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
 	check_permissions();
 
 	CheckLogicalDecodingRequirements();
@@ -128,39 +272,177 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									start_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
 
-	memset(nulls, 0, sizeof(nulls));
-
-	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
-
 	/* ok, slot is now fully created, mark it as persistent if needed */
 	if (!temporary)
 		ReplicationSlotPersist();
+
+	result = MyReplicationSlot->data.confirmed_flush;
+
 	ReplicationSlotRelease();
 
-	PG_RETURN_DATUM(result);
+	return result;
 }
 
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	XLogRecPtr	confirmed_flush;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	confirmed_flush = create_logical_replication_slot(NameStr(*name),
+													  NameStr(*plugin),
+													  temporary,
+													  InvalidXLogRecPtr);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = CStringGetTextDatum(NameStr(*name));
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	char		*plugin;	/* optional argument */
+	bool		temporary;	/* optional argument */
+	ReplicationSlot *saveslot = NULL;
+	XLogRecPtr	confirmed_flush;
+	XLogRecPtr	restart_lsn;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
+
+	/* Check type of replication slot */
+	if (SlotIsPhysical(MyReplicationSlot))
+	{
+		ReplicationSlotRelease();
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a physical replication slot to a logical replication slot"))));
+	}
+
+	/* Save values of the source slot */
+	restart_lsn = MyReplicationSlot->data.restart_lsn;
+	plugin = pstrdup(NameStr(MyReplicationSlot->data.plugin));
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
+
+	/* Check the optional arguments */
+	if (PG_NARGS() >= 3)
+		plugin = NameStr(*(PG_GETARG_NAME(2)));
+	if (PG_NARGS() >= 4)
+		temporary = PG_GETARG_BOOL(3);
+
+	/*
+	 * To prevent the restart_lsn WAL of the source slot from removal
+	 * during copying a new slot, we copy it while holding the source slot.
+	 * Since we are not allowed to create a new one while holding another
+	 * one, we temporarily save the acquired slot and restore it after
+	 * creation. Set callback function to ensure we release replication
+	 * slots if fail below.
+	 */
+	saveslot = MyReplicationSlot;
+	PG_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+	{
+		MyReplicationSlot = NULL;
+
+		confirmed_flush = create_logical_replication_slot(NameStr(*dst_name),
+														  plugin,
+														  temporary,
+														  restart_lsn);
+		Assert(MyReplicationSlot == NULL);
+
+		/*
+		 * Restore source slot. We must not change the saveslot to cancel the
+		 * callback function.
+		 */
+		MyReplicationSlot = saveslot;
+	}
+	PG_END_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+
+	/* Release the source slot */
+	ReplicationSlotRelease();
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = NameGetDatum(dst_name);
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
 
 /*
  * SQL function for dropping a replication slot.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index c83ff3b..22b8def 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -925,6 +925,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
@@ -967,7 +968,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	}
 	else if (cmd->kind == REPLICATION_KIND_PHYSICAL && reserve_wal)
 	{
-		ReplicationSlotReserveWal();
+		ReplicationSlotReserveWal(InvalidXLogRecPtr);
 
 		ReplicationSlotMarkDirty();
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index a146510..d7ce594 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9787,6 +9787,20 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,immediately_reserve,temporary,slot_name,lsn}',
   prosrc => 'pg_create_physical_replication_slot' },
+{ oid => '4008', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot' },
+{ oid => '4009', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{slot_name,dst_name,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot_no_temp' },
 { oid => '3780', descr => 'drop a replication slot',
   proname => 'pg_drop_replication_slot', provolatile => 'v', proparallel => 'u',
   prorettype => 'void', proargtypes => 'name',
@@ -9807,6 +9821,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4005', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name bool',
+  proallargtypes => '{name,name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,plugin,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4006', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name',
+  proallargtypes => '{name,name,name,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,plugin,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4007', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c25ac1f..afc32ff 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 7964ae2..d5c8953 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -193,7 +193,7 @@ extern void ReplicationSlotMarkDirty(void);
 
 /* misc stuff */
 extern bool ReplicationSlotValidateName(const char *name, int elevel);
-extern void ReplicationSlotReserveWal(void);
+extern void ReplicationSlotReserveWal(XLogRecPtr requested_lsn);
 extern void ReplicationSlotsComputeRequiredXmin(bool already_locked);
 extern void ReplicationSlotsComputeRequiredLSN(void);
 extern XLogRecPtr ReplicationSlotsComputeLogicalRestartLSN(void);
-- 
2.10.5

#18Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#17)
Re: Copy function for logical replication slots

On Tue, Aug 14, 2018 at 01:38:23PM +0900, Masahiko Sawada wrote:

On Thu, Jul 12, 2018 at 9:28 PM, Masahiko Sawada <sawada.mshk@gmail.com> wrote:

Attached new version of patch incorporated the all comments I got from
Michael-san.

To prevent the WAL segment file of restart_lsn of the origin slot from
removal during creating the target slot, I've chosen a way to copy new
one while holding the origin one. One problem to implement this way is
that the current replication slot code doesn't allow us to do
straightforwardly; the code assumes that the process creating a new
slot is not holding another slot. So I've changed the copy functions
so that it save temporarily MyReplicationSlot and then restore and
release it after creation the target slot. To ensure that both the
origin and target slot are released properly in failure cases I used
PG_ENSURE_ERROR_CLEANUP(). That way, we can keep the code changes of
the logical decoding at a minimum. I've thought that we can change the
logical decoding code so that it can assumes that the process can have
more than one slots at once but it seems overkill to me.

Yeah, we may be able to live with this trick. For other processes, the
process doing the copy would be seen as holding the slot so the
checkpointer would not advance the oldest LSN to retain.

The previous patch conflicts with the current HEAD. Attached updated
version patch.

+-- Now we have maximum 8 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1',
'failed'); -- error
+ERROR:  all replication slots are in use

installcheck is going to fail on an instance which does not use exactly
max_replication_slots = 8. That lacks flexibility, and you could have
the same coverage by copying, then immediately dropping each new slot.

+ to a physical replicaiton slot named <parameter>dst_slot_name</parameter>.
s/replicaiton/replicaton/.

+        The copied physical slot starts to reserve WAL from the same
<acronym>LSN</acronym> as the
+        source slot if the source slot already reserves WAL.
Or restricting this case?  In what is it useful to allow a copy from a
slot which has done nothing yet?  This would also simplify the acquire
and release logic of the source slot.
+   /* Check type of replication slot */
+   if (SlotIsLogical(MyReplicationSlot))
+   {
+       ReplicationSlotRelease();
+       ereport(ERROR,
+               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                (errmsg("cannot copy a logical replication slot to a
physical replication slot"))));
+   }
No need to call ReplicationSlotRelease for an ERROR code path.

Does it actually make sense to allow copy of temporary slots or change
their persistence? Those don't live across sessions so they'd need to
be copied in the same session which created them.
--
Michael

#19Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#18)
Re: Copy function for logical replication slots

On Mon, Aug 20, 2018 at 2:53 PM, Michael Paquier <michael@paquier.xyz> wrote:

On Tue, Aug 14, 2018 at 01:38:23PM +0900, Masahiko Sawada wrote:

On Thu, Jul 12, 2018 at 9:28 PM, Masahiko Sawada <sawada.mshk@gmail.com> wrote:

Attached new version of patch incorporated the all comments I got from
Michael-san.

To prevent the WAL segment file of restart_lsn of the origin slot from
removal during creating the target slot, I've chosen a way to copy new
one while holding the origin one. One problem to implement this way is
that the current replication slot code doesn't allow us to do
straightforwardly; the code assumes that the process creating a new
slot is not holding another slot. So I've changed the copy functions
so that it save temporarily MyReplicationSlot and then restore and
release it after creation the target slot. To ensure that both the
origin and target slot are released properly in failure cases I used
PG_ENSURE_ERROR_CLEANUP(). That way, we can keep the code changes of
the logical decoding at a minimum. I've thought that we can change the
logical decoding code so that it can assumes that the process can have
more than one slots at once but it seems overkill to me.

Yeah, we may be able to live with this trick. For other processes, the
process doing the copy would be seen as holding the slot so the
checkpointer would not advance the oldest LSN to retain.

The previous patch conflicts with the current HEAD. Attached updated
version patch.

Thank you for reviewing this patch.

+-- Now we have maximum 8 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1',
'failed'); -- error
+ERROR:  all replication slots are in use

installcheck is going to fail on an instance which does not use exactly
max_replication_slots = 8. That lacks flexibility, and you could have
the same coverage by copying, then immediately dropping each new slot.

Will fix.

+ to a physical replicaiton slot named <parameter>dst_slot_name</parameter>.
s/replicaiton/replicaton/.

+        The copied physical slot starts to reserve WAL from the same
<acronym>LSN</acronym> as the
+        source slot if the source slot already reserves WAL.
Or restricting this case?  In what is it useful to allow a copy from a
slot which has done nothing yet?  This would also simplify the acquire
and release logic of the source slot.

I think the copying from a slot that already reserved WAL would be
helpful for backup cases (maybe you suggested?). Also, either way we
need to make a safe logic of acquring and releasing the source slot
for logical slots cases. Or you meant to restrict the case where the
copying a slot that doesn't reserve WAL?

+   /* Check type of replication slot */
+   if (SlotIsLogical(MyReplicationSlot))
+   {
+       ReplicationSlotRelease();
+       ereport(ERROR,
+               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                (errmsg("cannot copy a logical replication slot to a
physical replication slot"))));
+   }
No need to call ReplicationSlotRelease for an ERROR code path.

Will fix.

Does it actually make sense to allow copy of temporary slots or change
their persistence? Those don't live across sessions so they'd need to
be copied in the same session which created them.

I think the copying of temporary slots would be an impracticable
feature but the changing their persistence might be helpful for some
cases, especially copying from persistent to temporary.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

#20Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#19)
Re: Copy function for logical replication slots

On Tue, Aug 28, 2018 at 04:14:04PM +0900, Masahiko Sawada wrote:

I think the copying from a slot that already reserved WAL would be
helpful for backup cases (maybe you suggested?). Also, either way we
need to make a safe logic of acquring and releasing the source slot
for logical slots cases. Or you meant to restrict the case where the
copying a slot that doesn't reserve WAL?

I mean the latter, as-known-as there is no actual point in being able to
copy WAL which does *not* reserve WAL.

Does it actually make sense to allow copy of temporary slots or change
their persistence? Those don't live across sessions so they'd need to
be copied in the same session which created them.

I think the copying of temporary slots would be an impracticable
feature but the changing their persistence might be helpful for some
cases, especially copying from persistent to temporary.

The session doing the copy of a permanent slot to the temporary slot has
to be the one also consuming it as the session which created the slot
owns it, and the slot would be dropped when the session ends. For
logical slots perhaps you have something in mind? Like copying a slot
which is not active to check where it is currently replaying, and using
the copy for sanity checks?
--
Michael

#21Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#20)
Re: Copy function for logical replication slots

On Tue, Aug 28, 2018 at 10:34 PM, Michael Paquier <michael@paquier.xyz> wrote:

On Tue, Aug 28, 2018 at 04:14:04PM +0900, Masahiko Sawada wrote:

I think the copying from a slot that already reserved WAL would be
helpful for backup cases (maybe you suggested?). Also, either way we
need to make a safe logic of acquring and releasing the source slot
for logical slots cases. Or you meant to restrict the case where the
copying a slot that doesn't reserve WAL?

I mean the latter, as-known-as there is no actual point in being able to
copy WAL which does *not* reserve WAL.

Agreed. I'll restrict that case in the next version

Does it actually make sense to allow copy of temporary slots or change
their persistence? Those don't live across sessions so they'd need to
be copied in the same session which created them.

I think the copying of temporary slots would be an impracticable
feature but the changing their persistence might be helpful for some
cases, especially copying from persistent to temporary.

The session doing the copy of a permanent slot to the temporary slot has
to be the one also consuming it as the session which created the slot
owns it, and the slot would be dropped when the session ends. For
logical slots perhaps you have something in mind? Like copying a slot
which is not active to check where it is currently replaying, and using
the copy for sanity checks?

Yeah, I imagined such case. If we want to do backup/logical decoding
from the same point as the source permanent slot we might want to use
temporary slots so that it will be dropped surely after that.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

#22Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Masahiko Sawada (#21)
1 attachment(s)
Re: Copy function for logical replication slots

On Wed, Aug 29, 2018 at 9:39 AM, Masahiko Sawada <sawada.mshk@gmail.com> wrote:

On Tue, Aug 28, 2018 at 10:34 PM, Michael Paquier <michael@paquier.xyz> wrote:

On Tue, Aug 28, 2018 at 04:14:04PM +0900, Masahiko Sawada wrote:

I think the copying from a slot that already reserved WAL would be
helpful for backup cases (maybe you suggested?). Also, either way we
need to make a safe logic of acquring and releasing the source slot
for logical slots cases. Or you meant to restrict the case where the
copying a slot that doesn't reserve WAL?

I mean the latter, as-known-as there is no actual point in being able to
copy WAL which does *not* reserve WAL.

Agreed. I'll restrict that case in the next version

Does it actually make sense to allow copy of temporary slots or change
their persistence? Those don't live across sessions so they'd need to
be copied in the same session which created them.

I think the copying of temporary slots would be an impracticable
feature but the changing their persistence might be helpful for some
cases, especially copying from persistent to temporary.

The session doing the copy of a permanent slot to the temporary slot has
to be the one also consuming it as the session which created the slot
owns it, and the slot would be dropped when the session ends. For
logical slots perhaps you have something in mind? Like copying a slot
which is not active to check where it is currently replaying, and using
the copy for sanity checks?

Yeah, I imagined such case. If we want to do backup/logical decoding
from the same point as the source permanent slot we might want to use
temporary slots so that it will be dropped surely after that.

Attached a new version patch incorporated the all comments I got.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v6-0001-Copy-function-for-logical-and-physical-replicatio.patchapplication/octet-stream; name=v6-0001-Copy-function-for-logical-and-physical-replicatio.patchDownload
From f6839911858f8a7385e4ae2ce8a545435442b77a Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Tue, 28 Aug 2018 16:14:32 +0900
Subject: [PATCH v6] Copy function for logical and physical replication slots.

---
 contrib/test_decoding/expected/slot.out   | 234 ++++++++++++++++++
 contrib/test_decoding/sql/slot.sql        |  94 ++++++++
 doc/src/sgml/func.sgml                    |  41 ++++
 src/backend/replication/logical/logical.c |   5 +-
 src/backend/replication/slot.c            |  89 ++++---
 src/backend/replication/slotfuncs.c       | 378 ++++++++++++++++++++++++++----
 src/backend/replication/walsender.c       |   3 +-
 src/include/catalog/pg_proc.dat           |  35 +++
 src/include/replication/logical.h         |   1 +
 src/include/replication/slot.h            |   2 +-
 10 files changed, 798 insertions(+), 84 deletions(-)

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 523621a..40b200e 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -150,3 +150,237 @@ SELECT pg_drop_replication_slot('regression_slot3');
  
 (1 row)
 
+--
+-- Test copy functions for logical replication slots
+--
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', 'pgoutput', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin      | pgoutput      | f
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin_temp | pgoutput      | t
+ orig_slot1 | test_decoding | f         | copied_slot1_no_change          | test_decoding | f
+(3 rows)
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase max_replication_slots.
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', 'pgoutput', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin      | pgoutput      | t
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin_temp | pgoutput      | f
+ orig_slot2 | test_decoding | t         | copied_slot2_no_change          | test_decoding | t
+(3 rows)
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+ERROR:  cannot copy a logical replication slot to a physical replication slot
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+--
+-- Test copy functions for physical replication slots
+--
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+       slot_name        | slot_type | temporary 
+------------------------+-----------+-----------
+ orig_slot1             | physical  | f
+ orig_slot2             | physical  | f
+ copied_slot1_no_change | physical  | f
+ copied_slot1_temp      | physical  | t
+(4 rows)
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  cannot copy a physical replication slot to a logical replication slot
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+ERROR:  cannot copy a physical replication slot that doesn't reserve WAL
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  | temporary |       slot_name        | temporary 
+------------+-----------+------------------------+-----------
+ orig_slot2 | t         | copied_slot2_no_change | t
+ orig_slot2 | t         | copied_slot2_notemp    | f
+(2 rows)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index c8d08f8..c14937c 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -76,3 +76,97 @@ SELECT slot_name FROM pg_create_physical_replication_slot('regression_slot3');
 SELECT pg_replication_slot_advance('regression_slot3', '0/0'); -- invalid LSN
 SELECT pg_replication_slot_advance('regression_slot3', '0/1'); -- error
 SELECT pg_drop_replication_slot('regression_slot3');
+
+--
+-- Test copy functions for logical replication slots
+--
+
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', 'pgoutput', true);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', 'pgoutput', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+
+--
+-- Test copy functions for physical replication slots
+--
+
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index edc9be9..9679fac 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19252,6 +19252,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>temporary</parameter> <type>bool</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing physical replication slot name <parameter>src_slot_name</parameter>
+        to a physical replication slot named <parameter>dst_slot_name</parameter>.
+        The copied physical slot starts to reserve WAL from the same <acronym>LSN</acronym> as the
+        source slot.
+        <parameter>temporary</parameter> is optional. If <parameter>temporary</parameter>
+        is omitted, the same value as the source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing logical replication slot name <parameter>src_slot_name</parameter>
+        to a logical replication slot named <parameter>dst_slot_name</parameter>
+        while changing the output plugin and persistence. The copied logical slot starts
+        from the same <acronym>LSN</acronym> as the source logical slot. Both <parameter>plugin</parameter> and
+        <parameter>temporary</parameter> are optional. If <parameter>plugin</parameter>
+        or <parameter>temporary</parameter> are omitted, the same values as
+        the source logical slot are used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index bb83fc9..cbf1f12 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -223,6 +223,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -266,7 +267,7 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	ReplicationSlotReserveWal(restart_lsn);
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -311,7 +312,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, restart_lsn, xmin_horizon,
 								 need_full_snapshot, false,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 19978d9..e575f3a 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -986,11 +986,13 @@ CheckSlotRequirements(void)
 /*
  * Reserve WAL for the currently active slot.
  *
- * Compute and set restart_lsn in a manner that's appropriate for the type of
- * the slot and concurrency safe.
+ * If an lsn to reserve is not requested, compute and set restart_lsn
+ * in a manner that's appropriate for the type of the slot and concurrency safe.
+ * If the reseved WAL is requested, set restart_lsn and check if the corresponding
+ * wal segment is available.
  */
 void
-ReplicationSlotReserveWal(void)
+ReplicationSlotReserveWal(XLogRecPtr requested_lsn)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
 
@@ -1001,47 +1003,57 @@ ReplicationSlotReserveWal(void)
 	 * The replication slot mechanism is used to prevent removal of required
 	 * WAL. As there is no interlock between this routine and checkpoints, WAL
 	 * segments could concurrently be removed when a now stale return value of
-	 * ReplicationSlotsComputeRequiredLSN() is used. In the unlikely case that
-	 * this happens we'll just retry.
+	 * ReplicationSlotsComputeRequiredLSN() is used. If the lsn to reserve is
+	 * not requested, in the unlikely case that this happens we'll just retry.
 	 */
 	while (true)
 	{
 		XLogSegNo	segno;
 		XLogRecPtr	restart_lsn;
 
-		/*
-		 * For logical slots log a standby snapshot and start logical decoding
-		 * at exactly that position. That allows the slot to start up more
-		 * quickly.
-		 *
-		 * That's not needed (or indeed helpful) for physical slots as they'll
-		 * start replay at the last logged checkpoint anyway. Instead return
-		 * the location of the last redo LSN. While that slightly increases
-		 * the chance that we have to retry, it's where a base backup has to
-		 * start replay at.
-		 */
-		if (!RecoveryInProgress() && SlotIsLogical(slot))
+		if (!XLogRecPtrIsInvalid(requested_lsn))
 		{
-			XLogRecPtr	flushptr;
-
-			/* start at current insert position */
-			restart_lsn = GetXLogInsertRecPtr();
+			/* Set the requested lsn */
 			SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
+			slot->data.restart_lsn = requested_lsn;
 			SpinLockRelease(&slot->mutex);
-
-			/* make sure we have enough information to start */
-			flushptr = LogStandbySnapshot();
-
-			/* and make sure it's fsynced to disk */
-			XLogFlush(flushptr);
 		}
 		else
 		{
-			restart_lsn = GetRedoRecPtr();
-			SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
-			SpinLockRelease(&slot->mutex);
+			/*
+			 * For logical slots log a standby snapshot and start logical decoding
+			 * at exactly that position. That allows the slot to start up more
+			 * quickly.
+			 *
+			 * That's not needed (or indeed helpful) for physical slots as they'll
+			 * start replay at the last logged checkpoint anyway. Instead return
+			 * the location of the last redo LSN. While that slightly increases
+			 * the chance that we have to retry, it's where a base backup has to
+			 * start replay at.
+			 */
+			if (!RecoveryInProgress() && SlotIsLogical(slot))
+			{
+				XLogRecPtr	flushptr;
+
+				/* start at current insert position */
+				restart_lsn = GetXLogInsertRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+
+				/* make sure we have enough information to start */
+				flushptr = LogStandbySnapshot();
+
+				/* and make sure it's fsynced to disk */
+				XLogFlush(flushptr);
+			}
+			else
+			{
+				restart_lsn = GetRedoRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+			}
 		}
 
 		/* prevent WAL removal as fast as possible */
@@ -1057,6 +1069,19 @@ ReplicationSlotReserveWal(void)
 		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
 		if (XLogGetLastRemovedSegno() < segno)
 			break;
+
+		/*
+		 * The requested wal lsn is no longer available. We don't want to retry
+		 * it, so raise an error.
+		 */
+		if (!XLogRecPtrIsInvalid(requested_lsn))
+		{
+			char filename[MAXFNAMELEN];
+
+			XLogFileName(filename, ThisTimeLineID, segno, wal_segment_size);
+			ereport(ERROR,
+					(errmsg("could not reserve WAL segment %s", filename)));
+		}
 	}
 }
 
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 8782bad..adac6b9 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -17,10 +17,12 @@
 #include "miscadmin.h"
 
 #include "access/htup_details.h"
+#include "access/xlog_internal.h"
 #include "replication/decode.h"
 #include "replication/slot.h"
 #include "replication/logical.h"
 #include "replication/logicalfuncs.h"
+#include "storage/ipc.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -36,6 +38,64 @@ check_permissions(void)
 }
 
 /*
+ * Error cleanup callback for copy replication slot functions. Release
+ * both MyReplicationSlot and the saved replication slot.
+ */
+static void
+copy_replication_slot_callback(int code, Datum arg)
+{
+	ReplicationSlot	*savedslot = (ReplicationSlot *) DatumGetPointer(arg);
+
+	if (MyReplicationSlot)
+		ReplicationSlotRelease();
+
+	/* Release the saved slot if exist while preventing double releasing */
+	if (savedslot && savedslot != MyReplicationSlot)
+	{
+		MyReplicationSlot = savedslot;
+		ReplicationSlotRelease();
+	}
+}
+
+/*
+ * Helper function for creating a new physical replication slot with
+ * given arguments. Return a restart_lsn of new replication slot or
+ * InvalidXLogRecPtr if WAL reservation is not required.
+ */
+static XLogRecPtr
+create_physical_replication_slot(char *name, bool immediately_reserve,
+								 bool temporary, XLogRecPtr restart_lsn)
+{
+	XLogRecPtr	result = InvalidXLogRecPtr;
+
+	Assert(!MyReplicationSlot);
+
+	check_permissions();
+
+	CheckSlotRequirements();
+
+	/* acquire replication slot, this will check for conflicting names */
+	ReplicationSlotCreate(name, false,
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+
+	if (immediately_reserve)
+	{
+		/* Reserve WAL as the user asked for it */
+		ReplicationSlotReserveWal(restart_lsn);
+
+		/* Write this slot to disk */
+		ReplicationSlotMarkDirty();
+		ReplicationSlotSave();
+
+		result = MyReplicationSlot->data.restart_lsn;
+	}
+
+	ReplicationSlotRelease();
+
+	return result;
+}
+
+/*
  * SQL function for creating a new physical (streaming replication)
  * replication slot.
  */
@@ -47,75 +107,162 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 	bool		temporary = PG_GETARG_BOOL(2);
 	Datum		values[2];
 	bool		nulls[2];
+	XLogRecPtr	result_lsn;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
 	Datum		result;
 
-	Assert(!MyReplicationSlot);
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	result_lsn = create_physical_replication_slot(NameStr(*name),
+												  immediately_reserve,
+												  temporary,
+												  InvalidXLogRecPtr);
+
+	values[0] = NameGetDatum(name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
+	else
+	{
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
+	}
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_physical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a physical replication slot.
+ */
+Datum
+pg_copy_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	bool		temporary; /* optional argument */
+	bool		immediately_reserve;
+	ReplicationSlot	*saveslot = NULL;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	result_lsn;
+	Datum		values[2];
+	bool		nulls[2];
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
 
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
 
-	check_permissions();
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
 
-	CheckSlotRequirements();
+	/* Check type of replication slot */
+	if (SlotIsLogical(MyReplicationSlot))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a logical replication slot to a physical replication slot"))));
 
-	/* acquire replication slot, this will check for conflicting names */
-	ReplicationSlotCreate(NameStr(*name), false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+	/* Copying non-reserved slot doesn't make sense */
+	if (XLogRecPtrIsInvalid(MyReplicationSlot->data.restart_lsn))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a physical replication slot that doesn't reserve WAL"))));
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	nulls[0] = false;
+	/* Save values of the source slot */
+	restart_lsn = MyReplicationSlot->data.restart_lsn;
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
 
+	/* Reserve WAL at creation if the source slot already reserves */
+	immediately_reserve = !XLogRecPtrIsInvalid(restart_lsn);
+
+	/* check the optional argument */
+	if (PG_NARGS() >= 3)
+		temporary = PG_GETARG_BOOL(2);
+
+	/*
+	 * To prevent the restart_lsn WAL of the source slot from removal
+	 * during copying a new slot, we copy it while holding the source slot.
+	 * Since we are not allowed to create a new one while holding another
+	 * one, we temporarily save the acquired slot and restore it after
+	 * creation. Set callback function to ensure we release replication
+	 * slots if fail below.
+	 */
 	if (immediately_reserve)
+		saveslot = MyReplicationSlot;
+	else
+		ReplicationSlotRelease();
+
+	PG_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
 	{
-		/* Reserve WAL as the user asked for it */
-		ReplicationSlotReserveWal();
+		if (immediately_reserve)
+			MyReplicationSlot = NULL;
 
-		/* Write this slot to disk */
-		ReplicationSlotMarkDirty();
-		ReplicationSlotSave();
+		result_lsn = create_physical_replication_slot(NameStr(*dst_name),
+													  immediately_reserve,
+													  temporary,
+													  restart_lsn);
+		Assert(MyReplicationSlot == NULL);
 
-		values[1] = LSNGetDatum(MyReplicationSlot->data.restart_lsn);
-		nulls[1] = false;
+		/*
+		 * Restore source slot, if saved. We must not change the saveslot
+		 * to cancel the callback function.
+		 */
+		if (saveslot)
+			MyReplicationSlot = saveslot;
 	}
+	PG_END_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+
+	/* Release the source slot, if not yet */
+	if (immediately_reserve)
+		ReplicationSlotRelease();
+
+	values[0] = NameGetDatum(dst_name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
 	else
 	{
-		nulls[1] = true;
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
 	}
 
 	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
 
-	ReplicationSlotRelease();
-
-	PG_RETURN_DATUM(result);
+	PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
 }
 
-
 /*
- * SQL function for creating a new logical replication slot.
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Return a confirmed_lsn of new replication slot.
  */
-Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+static XLogRecPtr
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr start_lsn)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
 	LogicalDecodingContext *ctx = NULL;
-
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
-	Datum		values[2];
-	bool		nulls[2];
+	XLogRecPtr	result;
 
 	Assert(!MyReplicationSlot);
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
 	check_permissions();
 
 	CheckLogicalDecodingRequirements();
@@ -128,39 +275,174 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									start_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
 
-	memset(nulls, 0, sizeof(nulls));
-
-	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
-
 	/* ok, slot is now fully created, mark it as persistent if needed */
 	if (!temporary)
 		ReplicationSlotPersist();
+
+	result = MyReplicationSlot->data.confirmed_flush;
+
 	ReplicationSlotRelease();
 
-	PG_RETURN_DATUM(result);
+	return result;
 }
 
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	XLogRecPtr	confirmed_flush;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	confirmed_flush = create_logical_replication_slot(NameStr(*name),
+													  NameStr(*plugin),
+													  temporary,
+													  InvalidXLogRecPtr);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = NameGetDatum(name);
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	char		*plugin;	/* optional argument */
+	bool		temporary;	/* optional argument */
+	ReplicationSlot *saveslot = NULL;
+	XLogRecPtr	confirmed_flush;
+	XLogRecPtr	restart_lsn;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
+
+	/* Check type of replication slot */
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a physical replication slot to a logical replication slot"))));
+
+	/* Save values of the source slot */
+	restart_lsn = MyReplicationSlot->data.restart_lsn;
+	plugin = pstrdup(NameStr(MyReplicationSlot->data.plugin));
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
+
+	/* Check the optional arguments */
+	if (PG_NARGS() >= 3)
+		plugin = NameStr(*(PG_GETARG_NAME(2)));
+	if (PG_NARGS() >= 4)
+		temporary = PG_GETARG_BOOL(3);
+
+	/*
+	 * To prevent the restart_lsn WAL of the source slot from removal
+	 * during copying a new slot, we copy it while holding the source slot.
+	 * Since we are not allowed to create a new one while holding another
+	 * one, we temporarily save the acquired slot and restore it after
+	 * creation. Set callback function to ensure we release replication
+	 * slots if fail below.
+	 */
+	saveslot = MyReplicationSlot;
+	PG_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+	{
+		MyReplicationSlot = NULL;
+
+		confirmed_flush = create_logical_replication_slot(NameStr(*dst_name),
+														  plugin,
+														  temporary,
+														  restart_lsn);
+		Assert(MyReplicationSlot == NULL);
+
+		/*
+		 * Restore source slot. We must not change the saveslot to cancel the
+		 * callback function.
+		 */
+		MyReplicationSlot = saveslot;
+	}
+	PG_END_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+
+	/* Release the source slot */
+	ReplicationSlotRelease();
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = NameGetDatum(dst_name);
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
 
 /*
  * SQL function for dropping a replication slot.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index c83ff3b..22b8def 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -925,6 +925,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
@@ -967,7 +968,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	}
 	else if (cmd->kind == REPLICATION_KIND_PHYSICAL && reserve_wal)
 	{
-		ReplicationSlotReserveWal();
+		ReplicationSlotReserveWal(InvalidXLogRecPtr);
 
 		ReplicationSlotMarkDirty();
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index a146510..d7ce594 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9787,6 +9787,20 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,immediately_reserve,temporary,slot_name,lsn}',
   prosrc => 'pg_create_physical_replication_slot' },
+{ oid => '4008', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot' },
+{ oid => '4009', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{slot_name,dst_name,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot_no_temp' },
 { oid => '3780', descr => 'drop a replication slot',
   proname => 'pg_drop_replication_slot', provolatile => 'v', proparallel => 'u',
   prorettype => 'void', proargtypes => 'name',
@@ -9807,6 +9821,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4005', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name bool',
+  proallargtypes => '{name,name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,plugin,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4006', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name',
+  proallargtypes => '{name,name,name,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,plugin,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4007', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c25ac1f..afc32ff 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 7964ae2..d5c8953 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -193,7 +193,7 @@ extern void ReplicationSlotMarkDirty(void);
 
 /* misc stuff */
 extern bool ReplicationSlotValidateName(const char *name, int elevel);
-extern void ReplicationSlotReserveWal(void);
+extern void ReplicationSlotReserveWal(XLogRecPtr requested_lsn);
 extern void ReplicationSlotsComputeRequiredXmin(bool already_locked);
 extern void ReplicationSlotsComputeRequiredLSN(void);
 extern XLogRecPtr ReplicationSlotsComputeLogicalRestartLSN(void);
-- 
2.10.5

#23Petr Jelinek
petr.jelinek@2ndquadrant.com
In reply to: Masahiko Sawada (#22)
Re: Copy function for logical replication slots

Hi,

On 31/08/2018 07:03, Masahiko Sawada wrote:

Attached a new version patch incorporated the all comments I got.

This looks pretty reasonable.

I am personally not big fan of the C wrappers for overloaded functions,
but that's what we need to do for opr_sanity to pass so I guess we'll
have to use them.

The more serious thing is:

+	if (MyReplicationSlot)
+		ReplicationSlotRelease();
+
+	/* Release the saved slot if exist while preventing double releasing */
+	if (savedslot && savedslot != MyReplicationSlot)

This won't work as intended as the ReplicationSlotRelease() will set
MyReplicationSlot to NULL, you might need to set aside MyReplicationSlot
to yet another temp variable inside this function prior to releasing it.

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

#24Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Petr Jelinek (#23)
1 attachment(s)
Re: Copy function for logical replication slots

On Sun, Nov 25, 2018 at 12:27 AM Petr Jelinek
<petr.jelinek@2ndquadrant.com> wrote:

Hi,

On 31/08/2018 07:03, Masahiko Sawada wrote:

Attached a new version patch incorporated the all comments I got.

This looks pretty reasonable.

Thank you for looking at this patch.

I am personally not big fan of the C wrappers for overloaded functions,
but that's what we need to do for opr_sanity to pass so I guess we'll
have to use them.

The more serious thing is:

+     if (MyReplicationSlot)
+             ReplicationSlotRelease();
+
+     /* Release the saved slot if exist while preventing double releasing */
+     if (savedslot && savedslot != MyReplicationSlot)

This won't work as intended as the ReplicationSlotRelease() will set
MyReplicationSlot to NULL, you might need to set aside MyReplicationSlot
to yet another temp variable inside this function prior to releasing it.

You're right. I've fixed it by checking if we need to release the
saved slot before releasing the origin slot. Is that right?
Attached an updated patch.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v7-0001-Copy-function-for-logical-and-physical-replicatio.patchapplication/octet-stream; name=v7-0001-Copy-function-for-logical-and-physical-replicatio.patchDownload
From 2c2663fab675ceb80ea75ad82ddd5b1437d67711 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Tue, 28 Aug 2018 16:14:32 +0900
Subject: [PATCH v7] Copy function for logical and physical replication slots.

---
 contrib/test_decoding/expected/slot.out   | 234 ++++++++++++++++++
 contrib/test_decoding/sql/slot.sql        |  94 ++++++++
 doc/src/sgml/func.sgml                    |  41 ++++
 src/backend/replication/logical/logical.c |   5 +-
 src/backend/replication/slot.c            |  89 ++++---
 src/backend/replication/slotfuncs.c       | 380 ++++++++++++++++++++++++++----
 src/backend/replication/walsender.c       |   3 +-
 src/include/catalog/pg_proc.dat           |  35 +++
 src/include/replication/logical.h         |   1 +
 src/include/replication/slot.h            |   2 +-
 10 files changed, 800 insertions(+), 84 deletions(-)

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 523621a..40b200e 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -150,3 +150,237 @@ SELECT pg_drop_replication_slot('regression_slot3');
  
 (1 row)
 
+--
+-- Test copy functions for logical replication slots
+--
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', 'pgoutput', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin      | pgoutput      | f
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin_temp | pgoutput      | t
+ orig_slot1 | test_decoding | f         | copied_slot1_no_change          | test_decoding | f
+(3 rows)
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase max_replication_slots.
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', 'pgoutput', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin      | pgoutput      | t
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin_temp | pgoutput      | f
+ orig_slot2 | test_decoding | t         | copied_slot2_no_change          | test_decoding | t
+(3 rows)
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+ERROR:  cannot copy a logical replication slot to a physical replication slot
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+--
+-- Test copy functions for physical replication slots
+--
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+       slot_name        | slot_type | temporary 
+------------------------+-----------+-----------
+ orig_slot1             | physical  | f
+ orig_slot2             | physical  | f
+ copied_slot1_no_change | physical  | f
+ copied_slot1_temp      | physical  | t
+(4 rows)
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  cannot copy a physical replication slot to a logical replication slot
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+ERROR:  cannot copy a physical replication slot that doesn't reserve WAL
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  | temporary |       slot_name        | temporary 
+------------+-----------+------------------------+-----------
+ orig_slot2 | t         | copied_slot2_no_change | t
+ orig_slot2 | t         | copied_slot2_notemp    | f
+(2 rows)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index c8d08f8..c14937c 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -76,3 +76,97 @@ SELECT slot_name FROM pg_create_physical_replication_slot('regression_slot3');
 SELECT pg_replication_slot_advance('regression_slot3', '0/0'); -- invalid LSN
 SELECT pg_replication_slot_advance('regression_slot3', '0/1'); -- error
 SELECT pg_drop_replication_slot('regression_slot3');
+
+--
+-- Test copy functions for logical replication slots
+--
+
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', 'pgoutput', true);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', 'pgoutput', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+
+--
+-- Test copy functions for physical replication slots
+--
+
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 09c77db..29c7347 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19512,6 +19512,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>temporary</parameter> <type>bool</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing physical replication slot name <parameter>src_slot_name</parameter>
+        to a physical replication slot named <parameter>dst_slot_name</parameter>.
+        The copied physical slot starts to reserve WAL from the same <acronym>LSN</acronym> as the
+        source slot.
+        <parameter>temporary</parameter> is optional. If <parameter>temporary</parameter>
+        is omitted, the same value as the source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing logical replication slot name <parameter>src_slot_name</parameter>
+        to a logical replication slot named <parameter>dst_slot_name</parameter>
+        while changing the output plugin and persistence. The copied logical slot starts
+        from the same <acronym>LSN</acronym> as the source logical slot. Both <parameter>plugin</parameter> and
+        <parameter>temporary</parameter> are optional. If <parameter>plugin</parameter>
+        or <parameter>temporary</parameter> are omitted, the same values as
+        the source logical slot are used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 9f99e4f..642b379 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -228,6 +228,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -271,7 +272,7 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	ReplicationSlotReserveWal(restart_lsn);
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -316,7 +317,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, restart_lsn, xmin_horizon,
 								 need_full_snapshot, false,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 1f2e713..1015482 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -990,11 +990,13 @@ CheckSlotRequirements(void)
 /*
  * Reserve WAL for the currently active slot.
  *
- * Compute and set restart_lsn in a manner that's appropriate for the type of
- * the slot and concurrency safe.
+ * If an lsn to reserve is not requested, compute and set restart_lsn
+ * in a manner that's appropriate for the type of the slot and concurrency safe.
+ * If the reseved WAL is requested, set restart_lsn and check if the corresponding
+ * wal segment is available.
  */
 void
-ReplicationSlotReserveWal(void)
+ReplicationSlotReserveWal(XLogRecPtr requested_lsn)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
 
@@ -1005,47 +1007,57 @@ ReplicationSlotReserveWal(void)
 	 * The replication slot mechanism is used to prevent removal of required
 	 * WAL. As there is no interlock between this routine and checkpoints, WAL
 	 * segments could concurrently be removed when a now stale return value of
-	 * ReplicationSlotsComputeRequiredLSN() is used. In the unlikely case that
-	 * this happens we'll just retry.
+	 * ReplicationSlotsComputeRequiredLSN() is used. If the lsn to reserve is
+	 * not requested, in the unlikely case that this happens we'll just retry.
 	 */
 	while (true)
 	{
 		XLogSegNo	segno;
 		XLogRecPtr	restart_lsn;
 
-		/*
-		 * For logical slots log a standby snapshot and start logical decoding
-		 * at exactly that position. That allows the slot to start up more
-		 * quickly.
-		 *
-		 * That's not needed (or indeed helpful) for physical slots as they'll
-		 * start replay at the last logged checkpoint anyway. Instead return
-		 * the location of the last redo LSN. While that slightly increases
-		 * the chance that we have to retry, it's where a base backup has to
-		 * start replay at.
-		 */
-		if (!RecoveryInProgress() && SlotIsLogical(slot))
+		if (!XLogRecPtrIsInvalid(requested_lsn))
 		{
-			XLogRecPtr	flushptr;
-
-			/* start at current insert position */
-			restart_lsn = GetXLogInsertRecPtr();
+			/* Set the requested lsn */
 			SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
+			slot->data.restart_lsn = requested_lsn;
 			SpinLockRelease(&slot->mutex);
-
-			/* make sure we have enough information to start */
-			flushptr = LogStandbySnapshot();
-
-			/* and make sure it's fsynced to disk */
-			XLogFlush(flushptr);
 		}
 		else
 		{
-			restart_lsn = GetRedoRecPtr();
-			SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
-			SpinLockRelease(&slot->mutex);
+			/*
+			 * For logical slots log a standby snapshot and start logical decoding
+			 * at exactly that position. That allows the slot to start up more
+			 * quickly.
+			 *
+			 * That's not needed (or indeed helpful) for physical slots as they'll
+			 * start replay at the last logged checkpoint anyway. Instead return
+			 * the location of the last redo LSN. While that slightly increases
+			 * the chance that we have to retry, it's where a base backup has to
+			 * start replay at.
+			 */
+			if (!RecoveryInProgress() && SlotIsLogical(slot))
+			{
+				XLogRecPtr	flushptr;
+
+				/* start at current insert position */
+				restart_lsn = GetXLogInsertRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+
+				/* make sure we have enough information to start */
+				flushptr = LogStandbySnapshot();
+
+				/* and make sure it's fsynced to disk */
+				XLogFlush(flushptr);
+			}
+			else
+			{
+				restart_lsn = GetRedoRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+			}
 		}
 
 		/* prevent WAL removal as fast as possible */
@@ -1061,6 +1073,19 @@ ReplicationSlotReserveWal(void)
 		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
 		if (XLogGetLastRemovedSegno() < segno)
 			break;
+
+		/*
+		 * The requested wal lsn is no longer available. We don't want to retry
+		 * it, so raise an error.
+		 */
+		if (!XLogRecPtrIsInvalid(requested_lsn))
+		{
+			char filename[MAXFNAMELEN];
+
+			XLogFileName(filename, ThisTimeLineID, segno, wal_segment_size);
+			ereport(ERROR,
+					(errmsg("could not reserve WAL segment %s", filename)));
+		}
 	}
 }
 
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 8782bad..0683dab 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -17,10 +17,12 @@
 #include "miscadmin.h"
 
 #include "access/htup_details.h"
+#include "access/xlog_internal.h"
 #include "replication/decode.h"
 #include "replication/slot.h"
 #include "replication/logical.h"
 #include "replication/logicalfuncs.h"
+#include "storage/ipc.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -36,6 +38,66 @@ check_permissions(void)
 }
 
 /*
+ * Error cleanup callback for copy replication slot functions. Release
+ * both MyReplicationSlot and the saved replication slot.
+ */
+static void
+copy_replication_slot_callback(int code, Datum arg)
+{
+	ReplicationSlot	*savedslot = (ReplicationSlot *) DatumGetPointer(arg);
+	bool	release_saved_slot = (savedslot && savedslot != MyReplicationSlot);
+
+	if (MyReplicationSlot)
+		ReplicationSlotRelease();
+
+	/* Release the saved slot if exist while preventing double releasing */
+	if (release_saved_slot)
+	{
+		Assert(MyReplicationSlot == NULL);
+		MyReplicationSlot = savedslot;
+		ReplicationSlotRelease();
+	}
+}
+
+/*
+ * Helper function for creating a new physical replication slot with
+ * given arguments. Return a restart_lsn of new replication slot or
+ * InvalidXLogRecPtr if WAL reservation is not required.
+ */
+static XLogRecPtr
+create_physical_replication_slot(char *name, bool immediately_reserve,
+								 bool temporary, XLogRecPtr restart_lsn)
+{
+	XLogRecPtr	result = InvalidXLogRecPtr;
+
+	Assert(!MyReplicationSlot);
+
+	check_permissions();
+
+	CheckSlotRequirements();
+
+	/* acquire replication slot, this will check for conflicting names */
+	ReplicationSlotCreate(name, false,
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+
+	if (immediately_reserve)
+	{
+		/* Reserve WAL as the user asked for it */
+		ReplicationSlotReserveWal(restart_lsn);
+
+		/* Write this slot to disk */
+		ReplicationSlotMarkDirty();
+		ReplicationSlotSave();
+
+		result = MyReplicationSlot->data.restart_lsn;
+	}
+
+	ReplicationSlotRelease();
+
+	return result;
+}
+
+/*
  * SQL function for creating a new physical (streaming replication)
  * replication slot.
  */
@@ -47,75 +109,162 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 	bool		temporary = PG_GETARG_BOOL(2);
 	Datum		values[2];
 	bool		nulls[2];
+	XLogRecPtr	result_lsn;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
 	Datum		result;
 
-	Assert(!MyReplicationSlot);
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	result_lsn = create_physical_replication_slot(NameStr(*name),
+												  immediately_reserve,
+												  temporary,
+												  InvalidXLogRecPtr);
+
+	values[0] = NameGetDatum(name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
+	else
+	{
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
+	}
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_physical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a physical replication slot.
+ */
+Datum
+pg_copy_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	bool		temporary; /* optional argument */
+	bool		immediately_reserve;
+	ReplicationSlot	*saveslot = NULL;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	result_lsn;
+	Datum		values[2];
+	bool		nulls[2];
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
 
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
 
-	check_permissions();
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
 
-	CheckSlotRequirements();
+	/* Check type of replication slot */
+	if (SlotIsLogical(MyReplicationSlot))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a logical replication slot to a physical replication slot"))));
 
-	/* acquire replication slot, this will check for conflicting names */
-	ReplicationSlotCreate(NameStr(*name), false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+	/* Copying non-reserved slot doesn't make sense */
+	if (XLogRecPtrIsInvalid(MyReplicationSlot->data.restart_lsn))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a physical replication slot that doesn't reserve WAL"))));
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	nulls[0] = false;
+	/* Save values of the source slot */
+	restart_lsn = MyReplicationSlot->data.restart_lsn;
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
 
+	/* Reserve WAL at creation if the source slot already reserves */
+	immediately_reserve = !XLogRecPtrIsInvalid(restart_lsn);
+
+	/* check the optional argument */
+	if (PG_NARGS() >= 3)
+		temporary = PG_GETARG_BOOL(2);
+
+	/*
+	 * To prevent the restart_lsn WAL of the source slot from removal
+	 * during copying a new slot, we copy it while holding the source slot.
+	 * Since we are not allowed to create a new one while holding another
+	 * one, we temporarily save the acquired slot and restore it after
+	 * creation. Set callback function to ensure we release replication
+	 * slots if fail below.
+	 */
 	if (immediately_reserve)
+		saveslot = MyReplicationSlot;
+	else
+		ReplicationSlotRelease();
+
+	PG_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
 	{
-		/* Reserve WAL as the user asked for it */
-		ReplicationSlotReserveWal();
+		if (immediately_reserve)
+			MyReplicationSlot = NULL;
 
-		/* Write this slot to disk */
-		ReplicationSlotMarkDirty();
-		ReplicationSlotSave();
+		result_lsn = create_physical_replication_slot(NameStr(*dst_name),
+													  immediately_reserve,
+													  temporary,
+													  restart_lsn);
+		Assert(MyReplicationSlot == NULL);
 
-		values[1] = LSNGetDatum(MyReplicationSlot->data.restart_lsn);
-		nulls[1] = false;
+		/*
+		 * Restore source slot, if saved. We must not change the saveslot
+		 * to cancel the callback function.
+		 */
+		if (saveslot)
+			MyReplicationSlot = saveslot;
 	}
+	PG_END_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+
+	/* Release the source slot, if not yet */
+	if (immediately_reserve)
+		ReplicationSlotRelease();
+
+	values[0] = NameGetDatum(dst_name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
 	else
 	{
-		nulls[1] = true;
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
 	}
 
 	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
 
-	ReplicationSlotRelease();
-
-	PG_RETURN_DATUM(result);
+	PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
 }
 
-
 /*
- * SQL function for creating a new logical replication slot.
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Return a confirmed_lsn of new replication slot.
  */
-Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+static XLogRecPtr
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr start_lsn)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
 	LogicalDecodingContext *ctx = NULL;
-
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
-	Datum		values[2];
-	bool		nulls[2];
+	XLogRecPtr	result;
 
 	Assert(!MyReplicationSlot);
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
 	check_permissions();
 
 	CheckLogicalDecodingRequirements();
@@ -128,39 +277,174 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									start_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
 
-	memset(nulls, 0, sizeof(nulls));
-
-	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
-
 	/* ok, slot is now fully created, mark it as persistent if needed */
 	if (!temporary)
 		ReplicationSlotPersist();
+
+	result = MyReplicationSlot->data.confirmed_flush;
+
 	ReplicationSlotRelease();
 
-	PG_RETURN_DATUM(result);
+	return result;
 }
 
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	XLogRecPtr	confirmed_flush;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	confirmed_flush = create_logical_replication_slot(NameStr(*name),
+													  NameStr(*plugin),
+													  temporary,
+													  InvalidXLogRecPtr);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = NameGetDatum(name);
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	char		*plugin;	/* optional argument */
+	bool		temporary;	/* optional argument */
+	ReplicationSlot *saveslot = NULL;
+	XLogRecPtr	confirmed_flush;
+	XLogRecPtr	restart_lsn;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
+
+	/* Check type of replication slot */
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a physical replication slot to a logical replication slot"))));
+
+	/* Save values of the source slot */
+	restart_lsn = MyReplicationSlot->data.restart_lsn;
+	plugin = pstrdup(NameStr(MyReplicationSlot->data.plugin));
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
+
+	/* Check the optional arguments */
+	if (PG_NARGS() >= 3)
+		plugin = NameStr(*(PG_GETARG_NAME(2)));
+	if (PG_NARGS() >= 4)
+		temporary = PG_GETARG_BOOL(3);
+
+	/*
+	 * To prevent the restart_lsn WAL of the source slot from removal
+	 * during copying a new slot, we copy it while holding the source slot.
+	 * Since we are not allowed to create a new one while holding another
+	 * one, we temporarily save the acquired slot and restore it after
+	 * creation. Set callback function to ensure we release replication
+	 * slots if fail below.
+	 */
+	saveslot = MyReplicationSlot;
+	PG_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+	{
+		MyReplicationSlot = NULL;
+
+		confirmed_flush = create_logical_replication_slot(NameStr(*dst_name),
+														  plugin,
+														  temporary,
+														  restart_lsn);
+		Assert(MyReplicationSlot == NULL);
+
+		/*
+		 * Restore source slot. We must not change the saveslot to cancel the
+		 * callback function.
+		 */
+		MyReplicationSlot = saveslot;
+	}
+	PG_END_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+
+	/* Release the source slot */
+	ReplicationSlotRelease();
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = NameGetDatum(dst_name);
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
 
 /*
  * SQL function for dropping a replication slot.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 46edb52..a9e1656 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -930,6 +930,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
@@ -972,7 +973,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	}
 	else if (cmd->kind == REPLICATION_KIND_PHYSICAL && reserve_wal)
 	{
-		ReplicationSlotReserveWal();
+		ReplicationSlotReserveWal(InvalidXLogRecPtr);
 
 		ReplicationSlotMarkDirty();
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 034a41e..7240991 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9603,6 +9603,20 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,immediately_reserve,temporary,slot_name,lsn}',
   prosrc => 'pg_create_physical_replication_slot' },
+{ oid => '4008', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot' },
+{ oid => '4009', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{slot_name,dst_name,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot_no_temp' },
 { oid => '3780', descr => 'drop a replication slot',
   proname => 'pg_drop_replication_slot', provolatile => 'v', proparallel => 'u',
   prorettype => 'void', proargtypes => 'name',
@@ -9623,6 +9637,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4005', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name bool',
+  proallargtypes => '{name,name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,plugin,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4006', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name',
+  proallargtypes => '{name,name,name,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,plugin,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4007', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c25ac1f..afc32ff 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 7964ae2..d5c8953 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -193,7 +193,7 @@ extern void ReplicationSlotMarkDirty(void);
 
 /* misc stuff */
 extern bool ReplicationSlotValidateName(const char *name, int elevel);
-extern void ReplicationSlotReserveWal(void);
+extern void ReplicationSlotReserveWal(XLogRecPtr requested_lsn);
 extern void ReplicationSlotsComputeRequiredXmin(bool already_locked);
 extern void ReplicationSlotsComputeRequiredLSN(void);
 extern XLogRecPtr ReplicationSlotsComputeLogicalRestartLSN(void);
-- 
2.10.5

#25Petr Jelinek
petr.jelinek@2ndquadrant.com
In reply to: Masahiko Sawada (#24)
Re: Copy function for logical replication slots

Hi,

On 26/11/2018 01:29, Masahiko Sawada wrote:

On Sun, Nov 25, 2018 at 12:27 AM Petr Jelinek
<petr.jelinek@2ndquadrant.com> wrote:

The more serious thing is:

+     if (MyReplicationSlot)
+             ReplicationSlotRelease();
+
+     /* Release the saved slot if exist while preventing double releasing */
+     if (savedslot && savedslot != MyReplicationSlot)

This won't work as intended as the ReplicationSlotRelease() will set
MyReplicationSlot to NULL, you might need to set aside MyReplicationSlot
to yet another temp variable inside this function prior to releasing it.

You're right. I've fixed it by checking if we need to release the
saved slot before releasing the origin slot. Is that right?
Attached an updated patch.

Sounds good.

I do have one more minor gripe after reading though again:

+
+               /*
+                * The requested wal lsn is no longer available. We don't want to retry
+                * it, so raise an error.
+                */
+               if (!XLogRecPtrIsInvalid(requested_lsn))
+               {
+                       char filename[MAXFNAMELEN];
+
+                       XLogFileName(filename, ThisTimeLineID, segno, wal_segment_size);
+                       ereport(ERROR,
+                                       (errmsg("could not reserve WAL segment %s", filename)));
+               }

I would reword the comment to something like "The caller has requested a
specific wal lsn which we failed to reserve. We can't retry here as the
requested wal is no longer available." (It took me a while to understand
this part).

Also the ereport should have errcode as it's going to be thrown to user
sessions and it might be better if the error itself used same wording as
CheckXLogRemoved() and XLogRead() for consistency. What do you think?

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

#26Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Petr Jelinek (#25)
1 attachment(s)
Re: Copy function for logical replication slots

On Tue, Nov 27, 2018 at 3:46 AM Petr Jelinek
<petr.jelinek@2ndquadrant.com> wrote:

Hi,

On 26/11/2018 01:29, Masahiko Sawada wrote:

On Sun, Nov 25, 2018 at 12:27 AM Petr Jelinek
<petr.jelinek@2ndquadrant.com> wrote:

The more serious thing is:

+     if (MyReplicationSlot)
+             ReplicationSlotRelease();
+
+     /* Release the saved slot if exist while preventing double releasing */
+     if (savedslot && savedslot != MyReplicationSlot)

This won't work as intended as the ReplicationSlotRelease() will set
MyReplicationSlot to NULL, you might need to set aside MyReplicationSlot
to yet another temp variable inside this function prior to releasing it.

You're right. I've fixed it by checking if we need to release the
saved slot before releasing the origin slot. Is that right?
Attached an updated patch.

Sounds good.

I do have one more minor gripe after reading though again:

Thank you for the review comment and sorry for the late response.

+
+               /*
+                * The requested wal lsn is no longer available. We don't want to retry
+                * it, so raise an error.
+                */
+               if (!XLogRecPtrIsInvalid(requested_lsn))
+               {
+                       char filename[MAXFNAMELEN];
+
+                       XLogFileName(filename, ThisTimeLineID, segno, wal_segment_size);
+                       ereport(ERROR,
+                                       (errmsg("could not reserve WAL segment %s", filename)));
+               }

I would reword the comment to something like "The caller has requested a
specific wal lsn which we failed to reserve. We can't retry here as the
requested wal is no longer available." (It took me a while to understand
this part).

Also the ereport should have errcode as it's going to be thrown to user
sessions and it might be better if the error itself used same wording as
CheckXLogRemoved() and XLogRead() for consistency. What do you think?

I agreed your both comments. I've changed the above comment and
ereport. Attached the updated version patch.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v8-0001-Copy-function-for-logical-and-physical-replicatio.patchapplication/octet-stream; name=v8-0001-Copy-function-for-logical-and-physical-replicatio.patchDownload
From a5f9714cb4b17ffd24fbbec0e449e0a01e86236f Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Tue, 28 Aug 2018 16:14:32 +0900
Subject: [PATCH v8] Copy function for logical and physical replication slots.

---
 contrib/test_decoding/expected/slot.out   | 234 ++++++++++++++++++
 contrib/test_decoding/sql/slot.sql        |  94 ++++++++
 doc/src/sgml/func.sgml                    |  41 ++++
 src/backend/replication/logical/logical.c |   5 +-
 src/backend/replication/slot.c            |  91 ++++---
 src/backend/replication/slotfuncs.c       | 380 ++++++++++++++++++++++++++----
 src/backend/replication/walsender.c       |   3 +-
 src/include/catalog/pg_proc.dat           |  35 +++
 src/include/replication/logical.h         |   1 +
 src/include/replication/slot.h            |   2 +-
 10 files changed, 802 insertions(+), 84 deletions(-)

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 523621a..40b200e 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -150,3 +150,237 @@ SELECT pg_drop_replication_slot('regression_slot3');
  
 (1 row)
 
+--
+-- Test copy functions for logical replication slots
+--
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', 'pgoutput', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin      | pgoutput      | f
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin_temp | pgoutput      | t
+ orig_slot1 | test_decoding | f         | copied_slot1_no_change          | test_decoding | f
+(3 rows)
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase max_replication_slots.
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', 'pgoutput', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin      | pgoutput      | t
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin_temp | pgoutput      | f
+ orig_slot2 | test_decoding | t         | copied_slot2_no_change          | test_decoding | t
+(3 rows)
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+ERROR:  cannot copy a logical replication slot to a physical replication slot
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+--
+-- Test copy functions for physical replication slots
+--
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+       slot_name        | slot_type | temporary 
+------------------------+-----------+-----------
+ orig_slot1             | physical  | f
+ orig_slot2             | physical  | f
+ copied_slot1_no_change | physical  | f
+ copied_slot1_temp      | physical  | t
+(4 rows)
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  cannot copy a physical replication slot to a logical replication slot
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+ERROR:  cannot copy a physical replication slot that doesn't reserve WAL
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  | temporary |       slot_name        | temporary 
+------------+-----------+------------------------+-----------
+ orig_slot2 | t         | copied_slot2_no_change | t
+ orig_slot2 | t         | copied_slot2_notemp    | f
+(2 rows)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index c8d08f8..c14937c 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -76,3 +76,97 @@ SELECT slot_name FROM pg_create_physical_replication_slot('regression_slot3');
 SELECT pg_replication_slot_advance('regression_slot3', '0/0'); -- invalid LSN
 SELECT pg_replication_slot_advance('regression_slot3', '0/1'); -- error
 SELECT pg_drop_replication_slot('regression_slot3');
+
+--
+-- Test copy functions for logical replication slots
+--
+
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', 'pgoutput', true);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', 'pgoutput', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+
+--
+-- Test copy functions for physical replication slots
+--
+
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 4930ec1..9887553 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19505,6 +19505,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>temporary</parameter> <type>bool</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing physical replication slot name <parameter>src_slot_name</parameter>
+        to a physical replication slot named <parameter>dst_slot_name</parameter>.
+        The copied physical slot starts to reserve WAL from the same <acronym>LSN</acronym> as the
+        source slot.
+        <parameter>temporary</parameter> is optional. If <parameter>temporary</parameter>
+        is omitted, the same value as the source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing logical replication slot name <parameter>src_slot_name</parameter>
+        to a logical replication slot named <parameter>dst_slot_name</parameter>
+        while changing the output plugin and persistence. The copied logical slot starts
+        from the same <acronym>LSN</acronym> as the source logical slot. Both <parameter>plugin</parameter> and
+        <parameter>temporary</parameter> are optional. If <parameter>plugin</parameter>
+        or <parameter>temporary</parameter> are omitted, the same values as
+        the source logical slot are used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 6e5bc12..ae15800 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -228,6 +228,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -271,7 +272,7 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	ReplicationSlotReserveWal(restart_lsn);
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -316,7 +317,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, restart_lsn, xmin_horizon,
 								 need_full_snapshot, false,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 33b23b6..98d1525 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -990,11 +990,13 @@ CheckSlotRequirements(void)
 /*
  * Reserve WAL for the currently active slot.
  *
- * Compute and set restart_lsn in a manner that's appropriate for the type of
- * the slot and concurrency safe.
+ * If an lsn to reserve is not requested, compute and set restart_lsn
+ * in a manner that's appropriate for the type of the slot and concurrency safe.
+ * If the reseved WAL is requested, set restart_lsn and check if the corresponding
+ * wal segment is available.
  */
 void
-ReplicationSlotReserveWal(void)
+ReplicationSlotReserveWal(XLogRecPtr requested_lsn)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
 
@@ -1005,47 +1007,57 @@ ReplicationSlotReserveWal(void)
 	 * The replication slot mechanism is used to prevent removal of required
 	 * WAL. As there is no interlock between this routine and checkpoints, WAL
 	 * segments could concurrently be removed when a now stale return value of
-	 * ReplicationSlotsComputeRequiredLSN() is used. In the unlikely case that
-	 * this happens we'll just retry.
+	 * ReplicationSlotsComputeRequiredLSN() is used. If the lsn to reserve is
+	 * not requested, in the unlikely case that this happens we'll just retry.
 	 */
 	while (true)
 	{
 		XLogSegNo	segno;
 		XLogRecPtr	restart_lsn;
 
-		/*
-		 * For logical slots log a standby snapshot and start logical decoding
-		 * at exactly that position. That allows the slot to start up more
-		 * quickly.
-		 *
-		 * That's not needed (or indeed helpful) for physical slots as they'll
-		 * start replay at the last logged checkpoint anyway. Instead return
-		 * the location of the last redo LSN. While that slightly increases
-		 * the chance that we have to retry, it's where a base backup has to
-		 * start replay at.
-		 */
-		if (!RecoveryInProgress() && SlotIsLogical(slot))
+		if (!XLogRecPtrIsInvalid(requested_lsn))
 		{
-			XLogRecPtr	flushptr;
-
-			/* start at current insert position */
-			restart_lsn = GetXLogInsertRecPtr();
+			/* Set the requested lsn */
 			SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
+			slot->data.restart_lsn = requested_lsn;
 			SpinLockRelease(&slot->mutex);
-
-			/* make sure we have enough information to start */
-			flushptr = LogStandbySnapshot();
-
-			/* and make sure it's fsynced to disk */
-			XLogFlush(flushptr);
 		}
 		else
 		{
-			restart_lsn = GetRedoRecPtr();
-			SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
-			SpinLockRelease(&slot->mutex);
+			/*
+			 * For logical slots log a standby snapshot and start logical decoding
+			 * at exactly that position. That allows the slot to start up more
+			 * quickly.
+			 *
+			 * That's not needed (or indeed helpful) for physical slots as they'll
+			 * start replay at the last logged checkpoint anyway. Instead return
+			 * the location of the last redo LSN. While that slightly increases
+			 * the chance that we have to retry, it's where a base backup has to
+			 * start replay at.
+			 */
+			if (!RecoveryInProgress() && SlotIsLogical(slot))
+			{
+				XLogRecPtr	flushptr;
+
+				/* start at current insert position */
+				restart_lsn = GetXLogInsertRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+
+				/* make sure we have enough information to start */
+				flushptr = LogStandbySnapshot();
+
+				/* and make sure it's fsynced to disk */
+				XLogFlush(flushptr);
+			}
+			else
+			{
+				restart_lsn = GetRedoRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+			}
 		}
 
 		/* prevent WAL removal as fast as possible */
@@ -1061,6 +1073,21 @@ ReplicationSlotReserveWal(void)
 		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
 		if (XLogGetLastRemovedSegno() < segno)
 			break;
+
+		/*
+		 * The caller has requested a specific wal which we failed to reserve.
+		 * We can't retry here as the requested wal is no longer available.
+		 */
+		if (!XLogRecPtrIsInvalid(requested_lsn))
+		{
+			char filename[MAXFNAMELEN];
+
+			XLogFileName(filename, ThisTimeLineID, segno, wal_segment_size);
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_FILE),
+					 errmsg("requested WAL segment %s has already been removed",
+							filename)));
+		}
 	}
 }
 
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 224dd92..01897ff 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -17,10 +17,12 @@
 #include "miscadmin.h"
 
 #include "access/htup_details.h"
+#include "access/xlog_internal.h"
 #include "replication/decode.h"
 #include "replication/slot.h"
 #include "replication/logical.h"
 #include "replication/logicalfuncs.h"
+#include "storage/ipc.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/pg_lsn.h"
@@ -36,6 +38,66 @@ check_permissions(void)
 }
 
 /*
+ * Error cleanup callback for copy replication slot functions. Release
+ * both MyReplicationSlot and the saved replication slot.
+ */
+static void
+copy_replication_slot_callback(int code, Datum arg)
+{
+	ReplicationSlot	*savedslot = (ReplicationSlot *) DatumGetPointer(arg);
+	bool	release_saved_slot = (savedslot && savedslot != MyReplicationSlot);
+
+	if (MyReplicationSlot)
+		ReplicationSlotRelease();
+
+	/* Release the saved slot if exist while preventing double releasing */
+	if (release_saved_slot)
+	{
+		Assert(MyReplicationSlot == NULL);
+		MyReplicationSlot = savedslot;
+		ReplicationSlotRelease();
+	}
+}
+
+/*
+ * Helper function for creating a new physical replication slot with
+ * given arguments. Return a restart_lsn of new replication slot or
+ * InvalidXLogRecPtr if WAL reservation is not required.
+ */
+static XLogRecPtr
+create_physical_replication_slot(char *name, bool immediately_reserve,
+								 bool temporary, XLogRecPtr restart_lsn)
+{
+	XLogRecPtr	result = InvalidXLogRecPtr;
+
+	Assert(!MyReplicationSlot);
+
+	check_permissions();
+
+	CheckSlotRequirements();
+
+	/* acquire replication slot, this will check for conflicting names */
+	ReplicationSlotCreate(name, false,
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+
+	if (immediately_reserve)
+	{
+		/* Reserve WAL as the user asked for it */
+		ReplicationSlotReserveWal(restart_lsn);
+
+		/* Write this slot to disk */
+		ReplicationSlotMarkDirty();
+		ReplicationSlotSave();
+
+		result = MyReplicationSlot->data.restart_lsn;
+	}
+
+	ReplicationSlotRelease();
+
+	return result;
+}
+
+/*
  * SQL function for creating a new physical (streaming replication)
  * replication slot.
  */
@@ -47,75 +109,162 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 	bool		temporary = PG_GETARG_BOOL(2);
 	Datum		values[2];
 	bool		nulls[2];
+	XLogRecPtr	result_lsn;
 	TupleDesc	tupdesc;
 	HeapTuple	tuple;
 	Datum		result;
 
-	Assert(!MyReplicationSlot);
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	result_lsn = create_physical_replication_slot(NameStr(*name),
+												  immediately_reserve,
+												  temporary,
+												  InvalidXLogRecPtr);
+
+	values[0] = NameGetDatum(name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
+	else
+	{
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
+	}
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_physical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a physical replication slot.
+ */
+Datum
+pg_copy_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	bool		temporary; /* optional argument */
+	bool		immediately_reserve;
+	ReplicationSlot	*saveslot = NULL;
+	XLogRecPtr	restart_lsn;
+	XLogRecPtr	result_lsn;
+	Datum		values[2];
+	bool		nulls[2];
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
 
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
 
-	check_permissions();
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
 
-	CheckSlotRequirements();
+	/* Check type of replication slot */
+	if (SlotIsLogical(MyReplicationSlot))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a logical replication slot to a physical replication slot"))));
 
-	/* acquire replication slot, this will check for conflicting names */
-	ReplicationSlotCreate(NameStr(*name), false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+	/* Copying non-reserved slot doesn't make sense */
+	if (XLogRecPtrIsInvalid(MyReplicationSlot->data.restart_lsn))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a physical replication slot that doesn't reserve WAL"))));
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	nulls[0] = false;
+	/* Save values of the source slot */
+	restart_lsn = MyReplicationSlot->data.restart_lsn;
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
 
+	/* Reserve WAL at creation if the source slot already reserves */
+	immediately_reserve = !XLogRecPtrIsInvalid(restart_lsn);
+
+	/* check the optional argument */
+	if (PG_NARGS() >= 3)
+		temporary = PG_GETARG_BOOL(2);
+
+	/*
+	 * To prevent the restart_lsn WAL of the source slot from removal
+	 * during copying a new slot, we copy it while holding the source slot.
+	 * Since we are not allowed to create a new one while holding another
+	 * one, we temporarily save the acquired slot and restore it after
+	 * creation. Set callback function to ensure we release replication
+	 * slots if fail below.
+	 */
 	if (immediately_reserve)
+		saveslot = MyReplicationSlot;
+	else
+		ReplicationSlotRelease();
+
+	PG_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
 	{
-		/* Reserve WAL as the user asked for it */
-		ReplicationSlotReserveWal();
+		if (immediately_reserve)
+			MyReplicationSlot = NULL;
 
-		/* Write this slot to disk */
-		ReplicationSlotMarkDirty();
-		ReplicationSlotSave();
+		result_lsn = create_physical_replication_slot(NameStr(*dst_name),
+													  immediately_reserve,
+													  temporary,
+													  restart_lsn);
+		Assert(MyReplicationSlot == NULL);
 
-		values[1] = LSNGetDatum(MyReplicationSlot->data.restart_lsn);
-		nulls[1] = false;
+		/*
+		 * Restore source slot, if saved. We must not change the saveslot
+		 * to cancel the callback function.
+		 */
+		if (saveslot)
+			MyReplicationSlot = saveslot;
 	}
+	PG_END_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+
+	/* Release the source slot, if not yet */
+	if (immediately_reserve)
+		ReplicationSlotRelease();
+
+	values[0] = NameGetDatum(dst_name);
+	nulls[0] = false;
+
+	if (XLogRecPtrIsInvalid(result_lsn))
+		nulls[1] = true;
 	else
 	{
-		nulls[1] = true;
+		values[1] = LSNGetDatum(result_lsn);
+		nulls[1] = false;
 	}
 
 	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
 
-	ReplicationSlotRelease();
-
-	PG_RETURN_DATUM(result);
+	PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
 }
 
-
 /*
- * SQL function for creating a new logical replication slot.
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Return a confirmed_lsn of new replication slot.
  */
-Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+static XLogRecPtr
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr start_lsn)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
 	LogicalDecodingContext *ctx = NULL;
-
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
-	Datum		values[2];
-	bool		nulls[2];
+	XLogRecPtr	result;
 
 	Assert(!MyReplicationSlot);
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
 	check_permissions();
 
 	CheckLogicalDecodingRequirements();
@@ -128,39 +277,174 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									start_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
 
-	memset(nulls, 0, sizeof(nulls));
-
-	tuple = heap_form_tuple(tupdesc, values, nulls);
-	result = HeapTupleGetDatum(tuple);
-
 	/* ok, slot is now fully created, mark it as persistent if needed */
 	if (!temporary)
 		ReplicationSlotPersist();
+
+	result = MyReplicationSlot->data.confirmed_flush;
+
 	ReplicationSlotRelease();
 
-	PG_RETURN_DATUM(result);
+	return result;
 }
 
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	XLogRecPtr	confirmed_flush;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	confirmed_flush = create_logical_replication_slot(NameStr(*name),
+													  NameStr(*plugin),
+													  temporary,
+													  InvalidXLogRecPtr);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = NameGetDatum(name);
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return pg_copy_logical_replication_slot(fcinfo);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		src_name = PG_GETARG_NAME(0);
+	Name		dst_name = PG_GETARG_NAME(1);
+	char		*plugin;	/* optional argument */
+	bool		temporary;	/* optional argument */
+	ReplicationSlot *saveslot = NULL;
+	XLogRecPtr	confirmed_flush;
+	XLogRecPtr	restart_lsn;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Acquire the source slot so we own it */
+	ReplicationSlotAcquire(NameStr(*src_name), true);
+
+	/* Check type of replication slot */
+	if (SlotIsPhysical(MyReplicationSlot))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a physical replication slot to a logical replication slot"))));
+
+	/* Save values of the source slot */
+	restart_lsn = MyReplicationSlot->data.restart_lsn;
+	plugin = pstrdup(NameStr(MyReplicationSlot->data.plugin));
+	temporary = (MyReplicationSlot->data.persistency == RS_TEMPORARY);
+
+	/* Check the optional arguments */
+	if (PG_NARGS() >= 3)
+		plugin = NameStr(*(PG_GETARG_NAME(2)));
+	if (PG_NARGS() >= 4)
+		temporary = PG_GETARG_BOOL(3);
+
+	/*
+	 * To prevent the restart_lsn WAL of the source slot from removal
+	 * during copying a new slot, we copy it while holding the source slot.
+	 * Since we are not allowed to create a new one while holding another
+	 * one, we temporarily save the acquired slot and restore it after
+	 * creation. Set callback function to ensure we release replication
+	 * slots if fail below.
+	 */
+	saveslot = MyReplicationSlot;
+	PG_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+	{
+		MyReplicationSlot = NULL;
+
+		confirmed_flush = create_logical_replication_slot(NameStr(*dst_name),
+														  plugin,
+														  temporary,
+														  restart_lsn);
+		Assert(MyReplicationSlot == NULL);
+
+		/*
+		 * Restore source slot. We must not change the saveslot to cancel the
+		 * callback function.
+		 */
+		MyReplicationSlot = saveslot;
+	}
+	PG_END_ENSURE_ERROR_CLEANUP(copy_replication_slot_callback, (Datum) PointerGetDatum(saveslot));
+
+	/* Release the source slot */
+	ReplicationSlotRelease();
+
+	memset(nulls, 0, sizeof(nulls));
+
+	values[0] = NameGetDatum(dst_name);
+	values[1] = LSNGetDatum(confirmed_flush);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_POINTER(HeapTupleGetDatum(tuple));
+}
 
 /*
  * SQL function for dropping a replication slot.
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 2d2eb23..63a9d03 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -930,6 +930,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
@@ -972,7 +973,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	}
 	else if (cmd->kind == REPLICATION_KIND_PHYSICAL && reserve_wal)
 	{
-		ReplicationSlotReserveWal();
+		ReplicationSlotReserveWal(InvalidXLogRecPtr);
 
 		ReplicationSlotMarkDirty();
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3ecc2e1..622c917 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9656,6 +9656,20 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,immediately_reserve,temporary,slot_name,lsn}',
   prosrc => 'pg_create_physical_replication_slot' },
+{ oid => '4008', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot' },
+{ oid => '4009', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{slot_name,dst_name,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot_no_temp' },
 { oid => '3780', descr => 'drop a replication slot',
   proname => 'pg_drop_replication_slot', provolatile => 'v', proparallel => 'u',
   prorettype => 'void', proargtypes => 'name',
@@ -9676,6 +9690,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4005', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name bool',
+  proallargtypes => '{name,name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,plugin,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4006', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name name',
+  proallargtypes => '{name,name,name,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,plugin,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4007', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c8ffc4c..0a2a63a 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index a8f1d66..3b652d2 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -193,7 +193,7 @@ extern void ReplicationSlotMarkDirty(void);
 
 /* misc stuff */
 extern bool ReplicationSlotValidateName(const char *name, int elevel);
-extern void ReplicationSlotReserveWal(void);
+extern void ReplicationSlotReserveWal(XLogRecPtr requested_lsn);
 extern void ReplicationSlotsComputeRequiredXmin(bool already_locked);
 extern void ReplicationSlotsComputeRequiredLSN(void);
 extern XLogRecPtr ReplicationSlotsComputeLogicalRestartLSN(void);
-- 
2.10.5

#27Petr Jelinek
petr.jelinek@2ndquadrant.com
In reply to: Masahiko Sawada (#26)
Re: Copy function for logical replication slots

Hi,

On 15/01/2019 02:56, Masahiko Sawada wrote:

On Tue, Nov 27, 2018 at 3:46 AM Petr Jelinek
<petr.jelinek@2ndquadrant.com> wrote:

+
+               /*
+                * The requested wal lsn is no longer available. We don't want to retry
+                * it, so raise an error.
+                */
+               if (!XLogRecPtrIsInvalid(requested_lsn))
+               {
+                       char filename[MAXFNAMELEN];
+
+                       XLogFileName(filename, ThisTimeLineID, segno, wal_segment_size);
+                       ereport(ERROR,
+                                       (errmsg("could not reserve WAL segment %s", filename)));
+               }

I would reword the comment to something like "The caller has requested a
specific wal lsn which we failed to reserve. We can't retry here as the
requested wal is no longer available." (It took me a while to understand
this part).

Also the ereport should have errcode as it's going to be thrown to user
sessions and it might be better if the error itself used same wording as
CheckXLogRemoved() and XLogRead() for consistency. What do you think?

I agreed your both comments. I've changed the above comment and
ereport. Attached the updated version patch.

I went through this again and I am pretty much happy with the current
version. So I am going to mark it as RFC.

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

#28Andres Freund
andres@anarazel.de
In reply to: Masahiko Sawada (#26)
Re: Copy function for logical replication slots

Hi,

On 2019-01-15 10:56:04 +0900, Masahiko Sawada wrote:

+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>temporary</parameter> <type>bool</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing physical replication slot name <parameter>src_slot_name</parameter>
+        to a physical replication slot named <parameter>dst_slot_name</parameter>.
+        The copied physical slot starts to reserve WAL from the same <acronym>LSN</acronym> as the
+        source slot.
+        <parameter>temporary</parameter> is optional. If <parameter>temporary</parameter>
+        is omitted, the same value as the source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing logical replication slot name <parameter>src_slot_name</parameter>
+        to a logical replication slot named <parameter>dst_slot_name</parameter>
+        while changing the output plugin and persistence. The copied logical slot starts
+        from the same <acronym>LSN</acronym> as the source logical slot. Both <parameter>plugin</parameter> and
+        <parameter>temporary</parameter> are optional. If <parameter>plugin</parameter>
+        or <parameter>temporary</parameter> are omitted, the same values as
+        the source logical slot are used.
+       </entry>
+      </row>

Would it make sense to move the differing options to the end of the
argument list? Right now we have a few common params, then a different
one, and then another common one?

@@ -271,7 +272,7 @@ CreateInitDecodingContext(char *plugin,
StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
SpinLockRelease(&slot->mutex);

-	ReplicationSlotReserveWal();
+	ReplicationSlotReserveWal(restart_lsn);

Why do we even need to call this? It ought to be guaranteed that there's
sufficient WAL, right? And somehow it seems harder to understand to me
that the reserve routine gets an LSN.

/*
* Reserve WAL for the currently active slot.
*
- * Compute and set restart_lsn in a manner that's appropriate for the type of
- * the slot and concurrency safe.
+ * If an lsn to reserve is not requested, compute and set restart_lsn
+ * in a manner that's appropriate for the type of the slot and concurrency safe.
+ * If the reseved WAL is requested, set restart_lsn and check if the corresponding
+ * wal segment is available.
*/
void
-ReplicationSlotReserveWal(void)
+ReplicationSlotReserveWal(XLogRecPtr requested_lsn)
{
ReplicationSlot *slot = MyReplicationSlot;
@@ -1005,47 +1007,57 @@ ReplicationSlotReserveWal(void)
* The replication slot mechanism is used to prevent removal of required
* WAL. As there is no interlock between this routine and checkpoints, WAL
* segments could concurrently be removed when a now stale return value of
-	 * ReplicationSlotsComputeRequiredLSN() is used. In the unlikely case that
-	 * this happens we'll just retry.
+	 * ReplicationSlotsComputeRequiredLSN() is used. If the lsn to reserve is
+	 * not requested, in the unlikely case that this happens we'll just retry.
*/
while (true)
{
XLogSegNo	segno;
XLogRecPtr	restart_lsn;
-		/*
-		 * For logical slots log a standby snapshot and start logical decoding
-		 * at exactly that position. That allows the slot to start up more
-		 * quickly.
-		 *
-		 * That's not needed (or indeed helpful) for physical slots as they'll
-		 * start replay at the last logged checkpoint anyway. Instead return
-		 * the location of the last redo LSN. While that slightly increases
-		 * the chance that we have to retry, it's where a base backup has to
-		 * start replay at.
-		 */
-		if (!RecoveryInProgress() && SlotIsLogical(slot))
+		if (!XLogRecPtrIsInvalid(requested_lsn))
{
-			XLogRecPtr	flushptr;
-
-			/* start at current insert position */
-			restart_lsn = GetXLogInsertRecPtr();
+			/* Set the requested lsn */
SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
+			slot->data.restart_lsn = requested_lsn;
SpinLockRelease(&slot->mutex);
-
-			/* make sure we have enough information to start */
-			flushptr = LogStandbySnapshot();
-
-			/* and make sure it's fsynced to disk */
-			XLogFlush(flushptr);
}
else
{
-			restart_lsn = GetRedoRecPtr();
-			SpinLockAcquire(&slot->mutex);
-			slot->data.restart_lsn = restart_lsn;
-			SpinLockRelease(&slot->mutex);
+			/*
+			 * For logical slots log a standby snapshot and start logical decoding
+			 * at exactly that position. That allows the slot to start up more
+			 * quickly.
+			 *
+			 * That's not needed (or indeed helpful) for physical slots as they'll
+			 * start replay at the last logged checkpoint anyway. Instead return
+			 * the location of the last redo LSN. While that slightly increases
+			 * the chance that we have to retry, it's where a base backup has to
+			 * start replay at.
+			 */
+			if (!RecoveryInProgress() && SlotIsLogical(slot))
+			{
+				XLogRecPtr	flushptr;
+
+				/* start at current insert position */
+				restart_lsn = GetXLogInsertRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+
+				/* make sure we have enough information to start */
+				flushptr = LogStandbySnapshot();
+
+				/* and make sure it's fsynced to disk */
+				XLogFlush(flushptr);
+			}
+			else
+			{
+				restart_lsn = GetRedoRecPtr();
+				SpinLockAcquire(&slot->mutex);
+				slot->data.restart_lsn = restart_lsn;
+				SpinLockRelease(&slot->mutex);
+			}
}

/* prevent WAL removal as fast as possible */
@@ -1061,6 +1073,21 @@ ReplicationSlotReserveWal(void)
XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
if (XLogGetLastRemovedSegno() < segno)
break;

This seems like it's harder to understand than before. The loop (and
most of the rest of the function) doesn't make sense for the copy case,
so I think it'd be better to just move this into a separate function
that just verifies that all the WAL is there.

+		/*
+		 * The caller has requested a specific wal which we failed to reserve.
+		 * We can't retry here as the requested wal is no longer available.
+		 */
+		if (!XLogRecPtrIsInvalid(requested_lsn))
+		{
+			char filename[MAXFNAMELEN];
+
+			XLogFileName(filename, ThisTimeLineID, segno, wal_segment_size);
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_FILE),
+					 errmsg("requested WAL segment %s has already been removed",
+							filename)));
+		}
}
}

This ought to be unreachable, right?

+/*
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
+{
+	return pg_copy_physical_replication_slot(fcinfo);
+}

You could avoid this by just defining the wrapper on the SQL level, but
I'm ok with this.

+	/*
+	 * To prevent the restart_lsn WAL of the source slot from removal
+	 * during copying a new slot, we copy it while holding the source slot.
+	 * Since we are not allowed to create a new one while holding another
+	 * one, we temporarily save the acquired slot and restore it after
+	 * creation. Set callback function to ensure we release replication
+	 * slots if fail below.
+	 */
if (immediately_reserve)
+		saveslot = MyReplicationSlot;
+	else
+		ReplicationSlotRelease();

Yikes, this is mightily ugly.

Stupid question, but couldn't we optimize this to something like:

/*
* First copy current data of the slot. Then install those in the
* new slot. The src slot could have progressed while installing,
* but the installed values prevent global horizons from progressing
* further. Therefore a second copy is sufficiently up2date.
*/
SpinLockAcquire(&src->mutex);
copy_lsn = src->data.restart_lsn;
copy_xid = ...;
SpinLockRelease(&src->mutex);

/* install copied values */

SpinLockAcquire(&src->mutex);
/* copy data of slot again */
SpinLockRelease(&src->mutex);

/* install again */

?

Greetings,

Andres Freund

#29Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Andres Freund (#28)
Re: Copy function for logical replication slots

On Sat, Feb 16, 2019 at 12:34 PM Andres Freund <andres@anarazel.de> wrote:

Hi,

Thank you for your comment.

On 2019-01-15 10:56:04 +0900, Masahiko Sawada wrote:

+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>temporary</parameter> <type>bool</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing physical replication slot name <parameter>src_slot_name</parameter>
+        to a physical replication slot named <parameter>dst_slot_name</parameter>.
+        The copied physical slot starts to reserve WAL from the same <acronym>LSN</acronym> as the
+        source slot.
+        <parameter>temporary</parameter> is optional. If <parameter>temporary</parameter>
+        is omitted, the same value as the source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <optional>, <parameter>plugin</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing logical replication slot name <parameter>src_slot_name</parameter>
+        to a logical replication slot named <parameter>dst_slot_name</parameter>
+        while changing the output plugin and persistence. The copied logical slot starts
+        from the same <acronym>LSN</acronym> as the source logical slot. Both <parameter>plugin</parameter> and
+        <parameter>temporary</parameter> are optional. If <parameter>plugin</parameter>
+        or <parameter>temporary</parameter> are omitted, the same values as
+        the source logical slot are used.
+       </entry>
+      </row>

Would it make sense to move the differing options to the end of the
argument list? Right now we have a few common params, then a different
one, and then another common one?

Agreed, will fix.

@@ -271,7 +272,7 @@ CreateInitDecodingContext(char *plugin,
StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
SpinLockRelease(&slot->mutex);

-     ReplicationSlotReserveWal();
+     ReplicationSlotReserveWal(restart_lsn);

Why do we even need to call this? It ought to be guaranteed that there's
sufficient WAL, right? And somehow it seems harder to understand to me
that the reserve routine gets an LSN.

That's right in copy function cases. I'll change it so that
CreateInitDecodingContext() sets the start lsn without WAL reservation
routine if the passed-in restart_lsn is a valid value. So the caller
must guarantee that the lsn is available.

/*
* Reserve WAL for the currently active slot.
*
- * Compute and set restart_lsn in a manner that's appropriate for the type of
- * the slot and concurrency safe.
+ * If an lsn to reserve is not requested, compute and set restart_lsn
+ * in a manner that's appropriate for the type of the slot and concurrency safe.
+ * If the reseved WAL is requested, set restart_lsn and check if the corresponding
+ * wal segment is available.
*/
void
-ReplicationSlotReserveWal(void)
+ReplicationSlotReserveWal(XLogRecPtr requested_lsn)
{
ReplicationSlot *slot = MyReplicationSlot;
@@ -1005,47 +1007,57 @@ ReplicationSlotReserveWal(void)
* The replication slot mechanism is used to prevent removal of required
* WAL. As there is no interlock between this routine and checkpoints, WAL
* segments could concurrently be removed when a now stale return value of
-      * ReplicationSlotsComputeRequiredLSN() is used. In the unlikely case that
-      * this happens we'll just retry.
+      * ReplicationSlotsComputeRequiredLSN() is used. If the lsn to reserve is
+      * not requested, in the unlikely case that this happens we'll just retry.
*/
while (true)
{
XLogSegNo       segno;
XLogRecPtr      restart_lsn;
-             /*
-              * For logical slots log a standby snapshot and start logical decoding
-              * at exactly that position. That allows the slot to start up more
-              * quickly.
-              *
-              * That's not needed (or indeed helpful) for physical slots as they'll
-              * start replay at the last logged checkpoint anyway. Instead return
-              * the location of the last redo LSN. While that slightly increases
-              * the chance that we have to retry, it's where a base backup has to
-              * start replay at.
-              */
-             if (!RecoveryInProgress() && SlotIsLogical(slot))
+             if (!XLogRecPtrIsInvalid(requested_lsn))
{
-                     XLogRecPtr      flushptr;
-
-                     /* start at current insert position */
-                     restart_lsn = GetXLogInsertRecPtr();
+                     /* Set the requested lsn */
SpinLockAcquire(&slot->mutex);
-                     slot->data.restart_lsn = restart_lsn;
+                     slot->data.restart_lsn = requested_lsn;
SpinLockRelease(&slot->mutex);
-
-                     /* make sure we have enough information to start */
-                     flushptr = LogStandbySnapshot();
-
-                     /* and make sure it's fsynced to disk */
-                     XLogFlush(flushptr);
}
else
{
-                     restart_lsn = GetRedoRecPtr();
-                     SpinLockAcquire(&slot->mutex);
-                     slot->data.restart_lsn = restart_lsn;
-                     SpinLockRelease(&slot->mutex);
+                     /*
+                      * For logical slots log a standby snapshot and start logical decoding
+                      * at exactly that position. That allows the slot to start up more
+                      * quickly.
+                      *
+                      * That's not needed (or indeed helpful) for physical slots as they'll
+                      * start replay at the last logged checkpoint anyway. Instead return
+                      * the location of the last redo LSN. While that slightly increases
+                      * the chance that we have to retry, it's where a base backup has to
+                      * start replay at.
+                      */
+                     if (!RecoveryInProgress() && SlotIsLogical(slot))
+                     {
+                             XLogRecPtr      flushptr;
+
+                             /* start at current insert position */
+                             restart_lsn = GetXLogInsertRecPtr();
+                             SpinLockAcquire(&slot->mutex);
+                             slot->data.restart_lsn = restart_lsn;
+                             SpinLockRelease(&slot->mutex);
+
+                             /* make sure we have enough information to start */
+                             flushptr = LogStandbySnapshot();
+
+                             /* and make sure it's fsynced to disk */
+                             XLogFlush(flushptr);
+                     }
+                     else
+                     {
+                             restart_lsn = GetRedoRecPtr();
+                             SpinLockAcquire(&slot->mutex);
+                             slot->data.restart_lsn = restart_lsn;
+                             SpinLockRelease(&slot->mutex);
+                     }
}

/* prevent WAL removal as fast as possible */
@@ -1061,6 +1073,21 @@ ReplicationSlotReserveWal(void)
XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
if (XLogGetLastRemovedSegno() < segno)
break;

This seems like it's harder to understand than before. The loop (and
most of the rest of the function) doesn't make sense for the copy case,
so I think it'd be better to just move this into a separate function
that just verifies that all the WAL is there.

Agreed, will fix.

+             /*
+              * The caller has requested a specific wal which we failed to reserve.
+              * We can't retry here as the requested wal is no longer available.
+              */
+             if (!XLogRecPtrIsInvalid(requested_lsn))
+             {
+                     char filename[MAXFNAMELEN];
+
+                     XLogFileName(filename, ThisTimeLineID, segno, wal_segment_size);
+                     ereport(ERROR,
+                                     (errcode(ERRCODE_UNDEFINED_FILE),
+                                      errmsg("requested WAL segment %s has already been removed",
+                                                     filename)));
+             }
}
}

This ought to be unreachable, right?

Right.

+/*
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
+{
+     return pg_copy_physical_replication_slot(fcinfo);
+}

You could avoid this by just defining the wrapper on the SQL level, but
I'm ok with this.

+     /*
+      * To prevent the restart_lsn WAL of the source slot from removal
+      * during copying a new slot, we copy it while holding the source slot.
+      * Since we are not allowed to create a new one while holding another
+      * one, we temporarily save the acquired slot and restore it after
+      * creation. Set callback function to ensure we release replication
+      * slots if fail below.
+      */
if (immediately_reserve)
+             saveslot = MyReplicationSlot;
+     else
+             ReplicationSlotRelease();

Yikes, this is mightily ugly.

Stupid question, but couldn't we optimize this to something like:

/*
* First copy current data of the slot. Then install those in the
* new slot. The src slot could have progressed while installing,
* but the installed values prevent global horizons from progressing
* further. Therefore a second copy is sufficiently up2date.
*/
SpinLockAcquire(&src->mutex);
copy_lsn = src->data.restart_lsn;
copy_xid = ...;
SpinLockRelease(&src->mutex);

/* install copied values */

SpinLockAcquire(&src->mutex);
/* copy data of slot again */
SpinLockRelease(&src->mutex);

/* install again */

?

With this optimization since we don't need to acquire the source slot
we can copy even from a slot that has already been acquired by
someone, which is great. However is it possible that once released the
first spinlock of the source slot it could be dropped and the global
horizons can progress before installing the copied values?

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

#30Andres Freund
andres@anarazel.de
In reply to: Masahiko Sawada (#29)
Re: Copy function for logical replication slots

Hi,

On 2019-02-18 16:57:07 +0900, Masahiko Sawada wrote:

Stupid question, but couldn't we optimize this to something like:

/*
* First copy current data of the slot. Then install those in the
* new slot. The src slot could have progressed while installing,
* but the installed values prevent global horizons from progressing
* further. Therefore a second copy is sufficiently up2date.
*/
SpinLockAcquire(&src->mutex);
copy_lsn = src->data.restart_lsn;
copy_xid = ...;
SpinLockRelease(&src->mutex);

/* install copied values */

SpinLockAcquire(&src->mutex);
/* copy data of slot again */
SpinLockRelease(&src->mutex);

/* install again */

?

With this optimization since we don't need to acquire the source slot
we can copy even from a slot that has already been acquired by
someone, which is great. However is it possible that once released the
first spinlock of the source slot it could be dropped and the global
horizons can progress before installing the copied values?

Well, I'd not thought we'd do it without acquiring the other slot. But
that still seems to be easy enough to address, we just need to recheck
whether the slot still exists (with the right name) the second time we
acquire the spinlock?

Greetings,

Andres Freund

#31Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Andres Freund (#30)
1 attachment(s)
Re: Copy function for logical replication slots

On Tue, Feb 19, 2019 at 1:28 AM Andres Freund <andres@anarazel.de> wrote:

Hi,

On 2019-02-18 16:57:07 +0900, Masahiko Sawada wrote:

Stupid question, but couldn't we optimize this to something like:

/*
* First copy current data of the slot. Then install those in the
* new slot. The src slot could have progressed while installing,
* but the installed values prevent global horizons from progressing
* further. Therefore a second copy is sufficiently up2date.
*/
SpinLockAcquire(&src->mutex);
copy_lsn = src->data.restart_lsn;
copy_xid = ...;
SpinLockRelease(&src->mutex);

/* install copied values */

SpinLockAcquire(&src->mutex);
/* copy data of slot again */
SpinLockRelease(&src->mutex);

/* install again */

?

With this optimization since we don't need to acquire the source slot
we can copy even from a slot that has already been acquired by
someone, which is great. However is it possible that once released the
first spinlock of the source slot it could be dropped and the global
horizons can progress before installing the copied values?

Well, I'd not thought we'd do it without acquiring the other slot. But
that still seems to be easy enough to address, we just need to recheck
whether the slot still exists (with the right name) the second time we
acquire the spinlock?

Yeah, I think that would work. The attached patch takes this
direction. Please review it.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v9-0001-Add-copy-function-for-replication-slots.patchapplication/octet-stream; name=v9-0001-Add-copy-function-for-replication-slots.patchDownload
From aeddceda6d9485b7d4dcdafc025a061038148ae7 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 18 Feb 2019 18:45:56 +0900
Subject: [PATCH v9] Add copy function for replication slots

Add new user functions to copy both physical and logical replication
slots while changing properties.

We don't hold the source slot during copy. To prevent the WAL of
destination slot from removal, we copy the source slot data
first. Then install those in the new slot. The source slot could have
progressed while installing, but the installed valuses prevent global
horizons progressing further. Then copy the source slot again, and
do existance check and installing up-to-date values to the destination
slot again.
---
 contrib/test_decoding/expected/slot.out   | 234 +++++++++++++++++++
 contrib/test_decoding/sql/slot.sql        |  94 ++++++++
 doc/src/sgml/func.sgml                    |  41 ++++
 src/backend/replication/logical/logical.c |  15 +-
 src/backend/replication/slotfuncs.c       | 363 +++++++++++++++++++++++++++---
 src/backend/replication/walsender.c       |   1 +
 src/include/catalog/pg_proc.dat           |  35 +++
 src/include/replication/logical.h         |   1 +
 8 files changed, 745 insertions(+), 39 deletions(-)

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 523621a..85cbbcd 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -150,3 +150,237 @@ SELECT pg_drop_replication_slot('regression_slot3');
  
 (1 row)
 
+--
+-- Test copy functions for logical replication slots
+--
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', false, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', true, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin      | pgoutput      | f
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin_temp | pgoutput      | t
+ orig_slot1 | test_decoding | f         | copied_slot1_no_change          | test_decoding | f
+(3 rows)
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase max_replication_slots.
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', true, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', false, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin      | pgoutput      | t
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin_temp | pgoutput      | f
+ orig_slot2 | test_decoding | t         | copied_slot2_no_change          | test_decoding | t
+(3 rows)
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+ERROR:  cannot copy a replication slot to the different type of replication slot
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+--
+-- Test copy functions for physical replication slots
+--
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+       slot_name        | slot_type | temporary 
+------------------------+-----------+-----------
+ orig_slot1             | physical  | f
+ orig_slot2             | physical  | f
+ copied_slot1_no_change | physical  | f
+ copied_slot1_temp      | physical  | t
+(4 rows)
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  cannot copy a replication slot to the different type of replication slot
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+ERROR:  cannot copy a replication slot that doesn't reserve WAL
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  | temporary |       slot_name        | temporary 
+------------+-----------+------------------------+-----------
+ orig_slot2 | t         | copied_slot2_no_change | t
+ orig_slot2 | t         | copied_slot2_notemp    | f
+(2 rows)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index c8d08f8..87b4307 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -76,3 +76,97 @@ SELECT slot_name FROM pg_create_physical_replication_slot('regression_slot3');
 SELECT pg_replication_slot_advance('regression_slot3', '0/0'); -- invalid LSN
 SELECT pg_replication_slot_advance('regression_slot3', '0/1'); -- error
 SELECT pg_drop_replication_slot('regression_slot3');
+
+--
+-- Test copy functions for logical replication slots
+--
+
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', false, 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', true, 'pgoutput');
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', true, 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', false, 'pgoutput');
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+
+--
+-- Test copy functions for physical replication slots
+--
+
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 86ff4e5..687caa8 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19505,6 +19505,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing physical replication slot name <parameter>src_slot_name</parameter>
+        to a physical replication slot named <parameter>dst_slot_name</parameter>.
+        The copied physical slot starts to reserve WAL from the same <acronym>LSN</acronym> as the
+        source slot.
+        <parameter>temporary</parameter> is optional. If <parameter>temporary</parameter>
+        is omitted, the same value as the source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type> <optional>, <parameter>plugin</parameter> <type>name</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing logical replication slot name <parameter>src_slot_name</parameter>
+        to a logical replication slot named <parameter>dst_slot_name</parameter>
+        while changing the output plugin and persistence. The copied logical slot starts
+        from the same <acronym>LSN</acronym> as the source logical slot. Both
+        <parameter>temporary</parameter> and <parameter>plugin</parameter> are optional.
+        If <parameter>temporary</parameter> or <parameter>plugin</parameter> are omitted,
+        the same values as the source logical slot are used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 6e5bc12..73d0c9e 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -221,6 +221,9 @@ StartupDecodingContext(List *output_plugin_options,
  * as the decoding context because further memory contexts will be created
  * inside it.
  *
+ * If restart_lsn is a valid value, we start decoding from the given lsn without
+ * WAL reservation routine. So the caller must guarantee that the lsn is available.
+ *
  * Returns an initialized decoding context after calling the output plugin's
  * startup function.
  */
@@ -228,6 +231,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -271,7 +275,14 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	if (XLogRecPtrIsInvalid(restart_lsn))
+		ReplicationSlotReserveWal();
+	else
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+	}
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -316,7 +327,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, restart_lsn, xmin_horizon,
 								 need_full_snapshot, false,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 224dd92..c1bec2e 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -17,6 +17,7 @@
 #include "miscadmin.h"
 
 #include "access/htup_details.h"
+#include "access/xlog_internal.h"
 #include "replication/decode.h"
 #include "replication/slot.h"
 #include "replication/logical.h"
@@ -36,6 +37,38 @@ check_permissions(void)
 }
 
 /*
+ * Helper function for creating a new physical replication slot with
+ * given arguments. Note that this function doesn't release the created
+ * slot.
+ *
+ * If restart_lsn is a valid value, we use it without WAL reservation
+ * routine. So the caller must guarantee that the lsn is available.
+ */
+static void
+create_physical_replication_slot(char *name, bool immediately_reserve,
+								 bool temporary, XLogRecPtr restart_lsn)
+{
+	Assert(!MyReplicationSlot);
+
+	/* acquire replication slot, this will check for conflicting names */
+	ReplicationSlotCreate(name, false,
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+
+	if (immediately_reserve)
+	{
+		/* Reserve WAL as the user asked for it */
+		if (XLogRecPtrIsInvalid(restart_lsn))
+			ReplicationSlotReserveWal();
+		else
+			MyReplicationSlot->data.restart_lsn = restart_lsn;
+
+		/* Write this slot to disk */
+		ReplicationSlotMarkDirty();
+		ReplicationSlotSave();
+	}
+}
+
+/*
  * SQL function for creating a new physical (streaming replication)
  * replication slot.
  */
@@ -51,8 +84,6 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 	HeapTuple	tuple;
 	Datum		result;
 
-	Assert(!MyReplicationSlot);
-
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
 
@@ -60,22 +91,16 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 
 	CheckSlotRequirements();
 
-	/* acquire replication slot, this will check for conflicting names */
-	ReplicationSlotCreate(NameStr(*name), false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+	create_physical_replication_slot(NameStr(*name),
+									 immediately_reserve,
+									 temporary,
+									 InvalidXLogRecPtr);
 
 	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
 	nulls[0] = false;
 
 	if (immediately_reserve)
 	{
-		/* Reserve WAL as the user asked for it */
-		ReplicationSlotReserveWal();
-
-		/* Write this slot to disk */
-		ReplicationSlotMarkDirty();
-		ReplicationSlotSave();
-
 		values[1] = LSNGetDatum(MyReplicationSlot->data.restart_lsn);
 		nulls[1] = false;
 	}
@@ -94,32 +119,18 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 
 
 /*
- * SQL function for creating a new logical replication slot.
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Note that this function doesn't release the created
+ * slot.
  */
-Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+static void
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr restart_lsn)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
 	LogicalDecodingContext *ctx = NULL;
 
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
-	Datum		values[2];
-	bool		nulls[2];
-
 	Assert(!MyReplicationSlot);
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
-	check_permissions();
-
-	CheckLogicalDecodingRequirements();
-
 	/*
 	 * Acquire a logical decoding slot, this will check for conflicting names.
 	 * Initially create persistent slot as ephemeral - that allows us to
@@ -128,25 +139,54 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									restart_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
+}
+
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	Datum		result;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	check_permissions();
+
+	CheckLogicalDecodingRequirements();
+
+	create_logical_replication_slot(NameStr(*name),
+									NameStr(*plugin),
+									temporary,
+									InvalidXLogRecPtr);
+
+	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
+	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
 
 	memset(nulls, 0, sizeof(nulls));
 
@@ -558,3 +598,252 @@ pg_replication_slot_advance(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Common operation of copying a replication slot.
+ */
+static Datum
+copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
+{
+	Name			src_name = PG_GETARG_NAME(0);
+	Name			dst_name = PG_GETARG_NAME(1);
+	ReplicationSlot *src = NULL;
+	XLogRecPtr		confirmed_flush = InvalidXLogRecPtr;
+	XLogRecPtr		src_restart_lsn;
+	bool			src_islogical;
+	bool			temporary;
+	bool			reserve_wal;
+	char			*plugin;
+	Datum			values[2];
+	bool			nulls[2];
+	Datum			result;
+	int				i;
+	TupleDesc		tupdesc;
+	HeapTuple		tuple;
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	check_permissions();
+
+	if (logical_slot)
+		CheckLogicalDecodingRequirements();
+	else
+		CheckSlotRequirements();
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	/*
+	 * To prevent the WAL of the destination slot from removal during copy,
+	 * we copy current data of the source slot first. Then install those in
+	 * the new slot. The source slot could have progressed while installing,
+	 * but the installed values prevent global horizons from progressing
+	 * further. Therefore a second copy is sufficiently up-to-date.
+	 */
+	for (i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		if (s->in_use && strcmp(NameStr(s->data.name), NameStr(*src_name)) == 0)
+		{
+			SpinLockAcquire(&s->mutex);
+			src_islogical = SlotIsLogical(s);
+			src_restart_lsn = s->data.restart_lsn;
+			temporary = s->data.persistency == RS_TEMPORARY;
+			plugin = logical_slot ? pstrdup(NameStr(s->data.plugin)) : NULL;
+			SpinLockRelease(&s->mutex);
+
+			src = s;
+			break;
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	if (src == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("replication slot \"%s\" does not exist", NameStr(*src_name))));
+
+	/* Check type of replication slot */
+	if (src_islogical != logical_slot)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a replication slot to the different type of replication slot"))));
+
+	/* Copying non-reserved slot doesn't make sense */
+	if (XLogRecPtrIsInvalid(src_restart_lsn))
+	{
+		Assert(!logical_slot);
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a replication slot that doesn't reserve WAL"))));
+	}
+
+	/* Overwrite by optional arguments */
+	if (PG_NARGS() >= 3)
+		temporary = PG_GETARG_BOOL(2);
+	if (PG_NARGS() >= 4)
+	{
+		Assert(logical_slot);
+		plugin = NameStr(*(PG_GETARG_NAME(3)));
+	}
+
+	/* Reserve WAL at creation if the source slot already reserves */
+	reserve_wal = !XLogRecPtrIsInvalid(src_restart_lsn);
+
+	/* Create new slot and acquire it */
+	if (logical_slot)
+		create_logical_replication_slot(NameStr(*dst_name),
+										plugin,
+										temporary,
+										src_restart_lsn);
+	else
+		create_physical_replication_slot(NameStr(*dst_name),
+										 reserve_wal,
+										 temporary,
+										 src_restart_lsn);
+
+	if (reserve_wal)
+	{
+		TransactionId	copy_effective_xmin;
+		TransactionId	copy_effective_catalog_xmin;
+		TransactionId	copy_xmin;
+		TransactionId	copy_catalog_xmin;
+		XLogRecPtr		copy_restart_lsn;
+		bool			copy_islogical;
+		char			*copy_name;
+
+		/* Copy data of source slot again */
+		SpinLockAcquire(&src->mutex);
+		copy_effective_xmin = src->effective_xmin;
+		copy_effective_catalog_xmin = src->effective_catalog_xmin;
+
+		copy_xmin = src->data.xmin;
+		copy_catalog_xmin = src->data.catalog_xmin;
+		copy_restart_lsn = src->data.restart_lsn;
+
+		/* for existence check */
+		copy_name = pstrdup(NameStr(src->data.name));
+		copy_islogical = SlotIsLogical(src);
+		SpinLockRelease(&src->mutex);
+
+		/*
+		 * Check if the source slot still exists. Since erroring out will
+		 * release and drop the destination slot we don't need to release
+		 * it here.
+		 */
+		if (XLogRecPtrIsInvalid(copy_restart_lsn) ||
+			copy_restart_lsn < src_restart_lsn ||
+			src_islogical != copy_islogical ||
+			strcmp(copy_name, NameStr(*src_name)) != 0)
+			ereport(ERROR,
+					(errmsg("could not copy logical replication slot \"%s\"",
+							NameStr(*src_name)),
+					 errdetail("The source replication slot has been dropped during copy")));
+
+		/* Install copied values again */
+		SpinLockAcquire(&MyReplicationSlot->mutex);
+		MyReplicationSlot->effective_xmin = copy_effective_xmin;
+		MyReplicationSlot->effective_catalog_xmin = copy_effective_catalog_xmin;
+
+		MyReplicationSlot->data.xmin = copy_xmin;
+		MyReplicationSlot->data.catalog_xmin = copy_catalog_xmin;
+		MyReplicationSlot->data.restart_lsn = copy_restart_lsn;
+		SpinLockRelease(&MyReplicationSlot->mutex);
+
+		ReplicationSlotMarkDirty();
+		ReplicationSlotsComputeRequiredXmin(false);
+		ReplicationSlotsComputeRequiredLSN();
+		ReplicationSlotSave();
+
+		/* Check if the restart_lsn is available */
+#ifdef USE_ASSERT_CHECKING
+		{
+			XLogSegNo	segno;
+
+			XLByteToSeg(copy_restart_lsn, segno, wal_segment_size);
+			Assert(XLogGetLastRemovedSegno() < segno);
+		}
+#endif
+	}
+
+	/* The destination slot is now fully created, mark it as persistent if needed */
+	if (logical_slot && !temporary)
+		ReplicationSlotPersist();
+
+	values[0] = NameGetDatum(dst_name);
+	nulls[0] = false;
+
+	if (!XLogRecPtrIsInvalid(MyReplicationSlot->data.confirmed_flush))
+	{
+		values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
+		nulls[1] = false;
+	}
+	else
+		nulls[1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	ReplicationSlotRelease();
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, false);
+}
+
+/*
+ * SQL function for copying a physical replication slot.
+ */
+Datum
+pg_copy_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, false);
+}
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 9b143f3..03c61f3 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -930,6 +930,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index a4e173b..3acc2b2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9676,6 +9676,20 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,immediately_reserve,temporary,slot_name,lsn}',
   prosrc => 'pg_create_physical_replication_slot' },
+{ oid => '4008', descr => 'copy a physical replication slot while changing temporality',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot' },
+{ oid => '4009', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot_no_temp' },
 { oid => '3780', descr => 'drop a replication slot',
   proname => 'pg_drop_replication_slot', provolatile => 'v', proparallel => 'u',
   prorettype => 'void', proargtypes => 'name',
@@ -9696,6 +9710,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4005', descr => 'copy a logical replication slot while changing temporality and plugin',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool name',
+  proallargtypes => '{name,name,bool,name,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,plugin,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4006', descr => 'copy a logical replication slot while changing temporality',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4007', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c8ffc4c..0a2a63a 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
-- 
2.10.5

#32Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#31)
Re: Copy function for logical replication slots

On Tue, Feb 19, 2019 at 05:09:33PM +0900, Masahiko Sawada wrote:

On Tue, Feb 19, 2019 at 1:28 AM Andres Freund <andres@anarazel.de> wrote:

Well, I'd not thought we'd do it without acquiring the other slot. But
that still seems to be easy enough to address, we just need to recheck
whether the slot still exists (with the right name) the second time we
acquire the spinlock?

Yeah, I think that would work. The attached patch takes this
direction. Please review it.

+     if (XLogRecPtrIsInvalid(copy_restart_lsn) ||
+              copy_restart_lsn < src_restart_lsn ||
+              src_islogical != copy_islogical ||
+              strcmp(copy_name, NameStr(*src_name)) != 0)
+              ereport(ERROR,
+                              (errmsg("could not copy logical replication slot \"%s\"",
+                               NameStr(*src_name)),
+                               errdetail("The source replication slot has been dropped during copy")));
+
+      /* Install copied values again */
+      SpinLockAcquire(&MyReplicationSlot->mutex);

Worth worrying about this window not reduced to zero? If the slot is
dropped between both then the same issue would arise.
--
Michael

#33Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#32)
Re: Copy function for logical replication slots

On Wed, Feb 20, 2019 at 12:26 PM Michael Paquier <michael@paquier.xyz> wrote:

On Tue, Feb 19, 2019 at 05:09:33PM +0900, Masahiko Sawada wrote:

On Tue, Feb 19, 2019 at 1:28 AM Andres Freund <andres@anarazel.de> wrote:

Well, I'd not thought we'd do it without acquiring the other slot. But
that still seems to be easy enough to address, we just need to recheck
whether the slot still exists (with the right name) the second time we
acquire the spinlock?

Yeah, I think that would work. The attached patch takes this
direction. Please review it.

+     if (XLogRecPtrIsInvalid(copy_restart_lsn) ||
+              copy_restart_lsn < src_restart_lsn ||
+              src_islogical != copy_islogical ||
+              strcmp(copy_name, NameStr(*src_name)) != 0)
+              ereport(ERROR,
+                              (errmsg("could not copy logical replication slot \"%s\"",
+                               NameStr(*src_name)),
+                               errdetail("The source replication slot has been dropped during copy")));
+
+      /* Install copied values again */
+      SpinLockAcquire(&MyReplicationSlot->mutex);

Worth worrying about this window not reduced to zero? If the slot is
dropped between both then the same issue would arise.

You meant that the destination slot (i.e. MyReplicationSlot) could be
dropped before acquiring its lock? Since we're holding the new slot it
cannot be dropped.

BTW, XLogRecPtrIsInvalid(copy_restart_lsn) || copy_restart_lsn <
src_restart_lsn is redundant, the former should be removed.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

#34Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Masahiko Sawada (#33)
1 attachment(s)
Re: Copy function for logical replication slots

On Wed, Feb 20, 2019 at 1:00 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:

BTW, XLogRecPtrIsInvalid(copy_restart_lsn) || copy_restart_lsn <
src_restart_lsn is redundant, the former should be removed.

So attached the updated version patch.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v10-0001-Add-copy-function-for-replication-slots.patchapplication/octet-stream; name=v10-0001-Add-copy-function-for-replication-slots.patchDownload
From 9a5ab86400faf832432502ab91c7bb440f0f9108 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 18 Feb 2019 18:45:56 +0900
Subject: [PATCH v10] Add copy function for replication slots

Add new user functions to copy both physical and logical replication
slots while changing properties.

We don't hold the source slot during copy. To prevent the WAL of
destination slot from removal, we copy the source slot data
first. Then install those in the new slot. The source slot could have
progressed while installing, but the installed valuses prevent global
horizons progressing further. Then copy the source slot again, and
do existance check and installing up-to-date values to the destination
slot again.
---
 contrib/test_decoding/expected/slot.out   | 234 +++++++++++++++++++
 contrib/test_decoding/sql/slot.sql        |  94 ++++++++
 doc/src/sgml/func.sgml                    |  41 ++++
 src/backend/replication/logical/logical.c |  15 +-
 src/backend/replication/slotfuncs.c       | 365 +++++++++++++++++++++++++++---
 src/backend/replication/walsender.c       |   1 +
 src/include/catalog/pg_proc.dat           |  35 +++
 src/include/replication/logical.h         |   1 +
 8 files changed, 747 insertions(+), 39 deletions(-)

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 523621a..85cbbcd 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -150,3 +150,237 @@ SELECT pg_drop_replication_slot('regression_slot3');
  
 (1 row)
 
+--
+-- Test copy functions for logical replication slots
+--
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', false, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', true, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin      | pgoutput      | f
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin_temp | pgoutput      | t
+ orig_slot1 | test_decoding | f         | copied_slot1_no_change          | test_decoding | f
+(3 rows)
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase max_replication_slots.
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', true, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', false, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin      | pgoutput      | t
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin_temp | pgoutput      | f
+ orig_slot2 | test_decoding | t         | copied_slot2_no_change          | test_decoding | t
+(3 rows)
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+ERROR:  cannot copy a replication slot to the different type of replication slot
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+--
+-- Test copy functions for physical replication slots
+--
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+       slot_name        | slot_type | temporary 
+------------------------+-----------+-----------
+ orig_slot1             | physical  | f
+ orig_slot2             | physical  | f
+ copied_slot1_no_change | physical  | f
+ copied_slot1_temp      | physical  | t
+(4 rows)
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  cannot copy a replication slot to the different type of replication slot
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+ERROR:  cannot copy a replication slot that doesn't reserve WAL
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  | temporary |       slot_name        | temporary 
+------------+-----------+------------------------+-----------
+ orig_slot2 | t         | copied_slot2_no_change | t
+ orig_slot2 | t         | copied_slot2_notemp    | f
+(2 rows)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index c8d08f8..87b4307 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -76,3 +76,97 @@ SELECT slot_name FROM pg_create_physical_replication_slot('regression_slot3');
 SELECT pg_replication_slot_advance('regression_slot3', '0/0'); -- invalid LSN
 SELECT pg_replication_slot_advance('regression_slot3', '0/1'); -- error
 SELECT pg_drop_replication_slot('regression_slot3');
+
+--
+-- Test copy functions for logical replication slots
+--
+
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', false, 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', true, 'pgoutput');
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', true, 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', false, 'pgoutput');
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+
+--
+-- Test copy functions for physical replication slots
+--
+
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 86ff4e5..687caa8 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19505,6 +19505,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing physical replication slot name <parameter>src_slot_name</parameter>
+        to a physical replication slot named <parameter>dst_slot_name</parameter>.
+        The copied physical slot starts to reserve WAL from the same <acronym>LSN</acronym> as the
+        source slot.
+        <parameter>temporary</parameter> is optional. If <parameter>temporary</parameter>
+        is omitted, the same value as the source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type> <optional>, <parameter>plugin</parameter> <type>name</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing logical replication slot name <parameter>src_slot_name</parameter>
+        to a logical replication slot named <parameter>dst_slot_name</parameter>
+        while changing the output plugin and persistence. The copied logical slot starts
+        from the same <acronym>LSN</acronym> as the source logical slot. Both
+        <parameter>temporary</parameter> and <parameter>plugin</parameter> are optional.
+        If <parameter>temporary</parameter> or <parameter>plugin</parameter> are omitted,
+        the same values as the source logical slot are used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 6e5bc12..73d0c9e 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -221,6 +221,9 @@ StartupDecodingContext(List *output_plugin_options,
  * as the decoding context because further memory contexts will be created
  * inside it.
  *
+ * If restart_lsn is a valid value, we start decoding from the given lsn without
+ * WAL reservation routine. So the caller must guarantee that the lsn is available.
+ *
  * Returns an initialized decoding context after calling the output plugin's
  * startup function.
  */
@@ -228,6 +231,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -271,7 +275,14 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	if (XLogRecPtrIsInvalid(restart_lsn))
+		ReplicationSlotReserveWal();
+	else
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+	}
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -316,7 +327,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, restart_lsn, xmin_horizon,
 								 need_full_snapshot, false,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 224dd92..05966a1 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -17,6 +17,7 @@
 #include "miscadmin.h"
 
 #include "access/htup_details.h"
+#include "access/xlog_internal.h"
 #include "replication/decode.h"
 #include "replication/slot.h"
 #include "replication/logical.h"
@@ -36,6 +37,38 @@ check_permissions(void)
 }
 
 /*
+ * Helper function for creating a new physical replication slot with
+ * given arguments. Note that this function doesn't release the created
+ * slot.
+ *
+ * If restart_lsn is a valid value, we use it without WAL reservation
+ * routine. So the caller must guarantee that the lsn is available.
+ */
+static void
+create_physical_replication_slot(char *name, bool immediately_reserve,
+								 bool temporary, XLogRecPtr restart_lsn)
+{
+	Assert(!MyReplicationSlot);
+
+	/* acquire replication slot, this will check for conflicting names */
+	ReplicationSlotCreate(name, false,
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+
+	if (immediately_reserve)
+	{
+		/* Reserve WAL as the user asked for it */
+		if (XLogRecPtrIsInvalid(restart_lsn))
+			ReplicationSlotReserveWal();
+		else
+			MyReplicationSlot->data.restart_lsn = restart_lsn;
+
+		/* Write this slot to disk */
+		ReplicationSlotMarkDirty();
+		ReplicationSlotSave();
+	}
+}
+
+/*
  * SQL function for creating a new physical (streaming replication)
  * replication slot.
  */
@@ -51,8 +84,6 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 	HeapTuple	tuple;
 	Datum		result;
 
-	Assert(!MyReplicationSlot);
-
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
 
@@ -60,22 +91,16 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 
 	CheckSlotRequirements();
 
-	/* acquire replication slot, this will check for conflicting names */
-	ReplicationSlotCreate(NameStr(*name), false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+	create_physical_replication_slot(NameStr(*name),
+									 immediately_reserve,
+									 temporary,
+									 InvalidXLogRecPtr);
 
 	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
 	nulls[0] = false;
 
 	if (immediately_reserve)
 	{
-		/* Reserve WAL as the user asked for it */
-		ReplicationSlotReserveWal();
-
-		/* Write this slot to disk */
-		ReplicationSlotMarkDirty();
-		ReplicationSlotSave();
-
 		values[1] = LSNGetDatum(MyReplicationSlot->data.restart_lsn);
 		nulls[1] = false;
 	}
@@ -94,32 +119,18 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 
 
 /*
- * SQL function for creating a new logical replication slot.
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Note that this function doesn't release the created
+ * slot.
  */
-Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+static void
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr restart_lsn)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
 	LogicalDecodingContext *ctx = NULL;
 
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
-	Datum		values[2];
-	bool		nulls[2];
-
 	Assert(!MyReplicationSlot);
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
-	check_permissions();
-
-	CheckLogicalDecodingRequirements();
-
 	/*
 	 * Acquire a logical decoding slot, this will check for conflicting names.
 	 * Initially create persistent slot as ephemeral - that allows us to
@@ -128,25 +139,54 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									restart_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
+}
+
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	Datum		result;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	check_permissions();
+
+	CheckLogicalDecodingRequirements();
+
+	create_logical_replication_slot(NameStr(*name),
+									NameStr(*plugin),
+									temporary,
+									InvalidXLogRecPtr);
+
+	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
+	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
 
 	memset(nulls, 0, sizeof(nulls));
 
@@ -558,3 +598,254 @@ pg_replication_slot_advance(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Common operation of copying a replication slot.
+ */
+static Datum
+copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
+{
+	Name			src_name = PG_GETARG_NAME(0);
+	Name			dst_name = PG_GETARG_NAME(1);
+	ReplicationSlot *src = NULL;
+	XLogRecPtr		confirmed_flush = InvalidXLogRecPtr;
+	XLogRecPtr		src_restart_lsn;
+	bool			src_islogical;
+	bool			temporary;
+	bool			reserve_wal;
+	char			*plugin;
+	Datum			values[2];
+	bool			nulls[2];
+	Datum			result;
+	int				i;
+	TupleDesc		tupdesc;
+	HeapTuple		tuple;
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	check_permissions();
+
+	if (logical_slot)
+		CheckLogicalDecodingRequirements();
+	else
+		CheckSlotRequirements();
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	/*
+	 * To prevent the WAL of the destination slot from removal during copy,
+	 * we copy current data of the source slot first. Then install those in
+	 * the new slot. The source slot could have progressed while installing,
+	 * but the installed values prevent global horizons from progressing
+	 * further. Therefore a second copy is sufficiently up-to-date.
+	 */
+	for (i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		if (s->in_use && strcmp(NameStr(s->data.name), NameStr(*src_name)) == 0)
+		{
+			SpinLockAcquire(&s->mutex);
+			src_islogical = SlotIsLogical(s);
+			src_restart_lsn = s->data.restart_lsn;
+			temporary = s->data.persistency == RS_TEMPORARY;
+			plugin = logical_slot ? pstrdup(NameStr(s->data.plugin)) : NULL;
+			SpinLockRelease(&s->mutex);
+
+			src = s;
+			break;
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	if (src == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("replication slot \"%s\" does not exist", NameStr(*src_name))));
+
+	/* Check type of replication slot */
+	if (src_islogical != logical_slot)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a replication slot to the different type of replication slot"))));
+
+	/* Copying non-reserved slot doesn't make sense */
+	if (XLogRecPtrIsInvalid(src_restart_lsn))
+	{
+		Assert(!logical_slot);
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a replication slot that doesn't reserve WAL"))));
+	}
+
+	/* Overwrite by optional arguments */
+	if (PG_NARGS() >= 3)
+		temporary = PG_GETARG_BOOL(2);
+	if (PG_NARGS() >= 4)
+	{
+		Assert(logical_slot);
+		plugin = NameStr(*(PG_GETARG_NAME(3)));
+	}
+
+	/* Reserve WAL at creation if the source slot already reserves */
+	reserve_wal = !XLogRecPtrIsInvalid(src_restart_lsn);
+
+	/* Create new slot and acquire it */
+	if (logical_slot)
+		create_logical_replication_slot(NameStr(*dst_name),
+										plugin,
+										temporary,
+										src_restart_lsn);
+	else
+		create_physical_replication_slot(NameStr(*dst_name),
+										 reserve_wal,
+										 temporary,
+										 src_restart_lsn);
+
+	if (reserve_wal)
+	{
+		TransactionId	copy_effective_xmin;
+		TransactionId	copy_effective_catalog_xmin;
+		TransactionId	copy_xmin;
+		TransactionId	copy_catalog_xmin;
+		XLogRecPtr		copy_restart_lsn;
+		bool			copy_islogical;
+		char			*copy_name;
+
+		/* Copy data of source slot again */
+		SpinLockAcquire(&src->mutex);
+		copy_effective_xmin = src->effective_xmin;
+		copy_effective_catalog_xmin = src->effective_catalog_xmin;
+
+		copy_xmin = src->data.xmin;
+		copy_catalog_xmin = src->data.catalog_xmin;
+		copy_restart_lsn = src->data.restart_lsn;
+
+		/* for existence check */
+		copy_name = pstrdup(NameStr(src->data.name));
+		copy_islogical = SlotIsLogical(src);
+		SpinLockRelease(&src->mutex);
+
+		/*
+		 * Check if the source slot still exists and is valid. We regards it as
+		 * invalid if the type of replication slot or name has been changed or
+		 * the restart_lsn could have gone backward. The restart_lsn of second
+		 * copy can go backward when the source slot is dropped and copied during
+		 * installation. Since erroring out will release and drop the destination
+		 * slot we don't need to release it here.
+		 */
+		if (copy_restart_lsn < src_restart_lsn ||
+			src_islogical != copy_islogical ||
+			strcmp(copy_name, NameStr(*src_name)) != 0)
+			ereport(ERROR,
+					(errmsg("could not copy logical replication slot \"%s\"",
+							NameStr(*src_name)),
+					 errdetail("The source replication slot has been dropped during copy")));
+
+		/* Install copied values again */
+		SpinLockAcquire(&MyReplicationSlot->mutex);
+		MyReplicationSlot->effective_xmin = copy_effective_xmin;
+		MyReplicationSlot->effective_catalog_xmin = copy_effective_catalog_xmin;
+
+		MyReplicationSlot->data.xmin = copy_xmin;
+		MyReplicationSlot->data.catalog_xmin = copy_catalog_xmin;
+		MyReplicationSlot->data.restart_lsn = copy_restart_lsn;
+		SpinLockRelease(&MyReplicationSlot->mutex);
+
+		ReplicationSlotMarkDirty();
+		ReplicationSlotsComputeRequiredXmin(false);
+		ReplicationSlotsComputeRequiredLSN();
+		ReplicationSlotSave();
+
+		/* Check if the restart_lsn is available */
+#ifdef USE_ASSERT_CHECKING
+		{
+			XLogSegNo	segno;
+
+			XLByteToSeg(copy_restart_lsn, segno, wal_segment_size);
+			Assert(XLogGetLastRemovedSegno() < segno);
+		}
+#endif
+	}
+
+	/* The destination slot is now fully created, mark it as persistent if needed */
+	if (logical_slot && !temporary)
+		ReplicationSlotPersist();
+
+	values[0] = NameGetDatum(dst_name);
+	nulls[0] = false;
+
+	if (!XLogRecPtrIsInvalid(MyReplicationSlot->data.confirmed_flush))
+	{
+		values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
+		nulls[1] = false;
+	}
+	else
+		nulls[1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	ReplicationSlotRelease();
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, false);
+}
+
+/*
+ * SQL function for copying a physical replication slot.
+ */
+Datum
+pg_copy_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, false);
+}
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 9b143f3..03c61f3 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -930,6 +930,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index a4e173b..3acc2b2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9676,6 +9676,20 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,immediately_reserve,temporary,slot_name,lsn}',
   prosrc => 'pg_create_physical_replication_slot' },
+{ oid => '4008', descr => 'copy a physical replication slot while changing temporality',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot' },
+{ oid => '4009', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot_no_temp' },
 { oid => '3780', descr => 'drop a replication slot',
   proname => 'pg_drop_replication_slot', provolatile => 'v', proparallel => 'u',
   prorettype => 'void', proargtypes => 'name',
@@ -9696,6 +9710,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4005', descr => 'copy a logical replication slot while changing temporality and plugin',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool name',
+  proallargtypes => '{name,name,bool,name,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,plugin,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4006', descr => 'copy a logical replication slot while changing temporality',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4007', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c8ffc4c..0a2a63a 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
-- 
2.10.5

#35Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Masahiko Sawada (#34)
1 attachment(s)
Re: Copy function for logical replication slots

On Mon, Feb 25, 2019 at 4:50 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:

On Wed, Feb 20, 2019 at 1:00 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:

BTW, XLogRecPtrIsInvalid(copy_restart_lsn) || copy_restart_lsn <
src_restart_lsn is redundant, the former should be removed.

So attached the updated version patch.

Since the patch failed to build, attached the updated version patch.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v11-0001-Add-copy-function-for-replication-slots.patchapplication/octet-stream; name=v11-0001-Add-copy-function-for-replication-slots.patchDownload
From c41558f0bb3f9c4dc8749b0dca26c23f803a28c0 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 18 Feb 2019 18:45:56 +0900
Subject: [PATCH v11] Add copy function for replication slots

Add new user functions to copy both physical and logical replication
slots while changing properties.

We don't hold the source slot during copy. To prevent the WAL of
destination slot from removal, we copy the source slot data
first. Then install those in the new slot. The source slot could have
progressed while installing, but the installed valuses prevent global
horizons progressing further. Then copy the source slot again, and
do existance check and installing up-to-date values to the destination
slot again.
---
 contrib/test_decoding/expected/slot.out   | 234 +++++++++++++++++++
 contrib/test_decoding/sql/slot.sql        |  94 ++++++++
 doc/src/sgml/func.sgml                    |  41 ++++
 src/backend/replication/logical/logical.c |  16 +-
 src/backend/replication/slotfuncs.c       | 363 +++++++++++++++++++++++++++---
 src/backend/replication/walsender.c       |   1 +
 src/include/catalog/pg_proc.dat           |  35 +++
 src/include/replication/logical.h         |   1 +
 8 files changed, 746 insertions(+), 39 deletions(-)

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 523621a..85cbbcd 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -150,3 +150,237 @@ SELECT pg_drop_replication_slot('regression_slot3');
  
 (1 row)
 
+--
+-- Test copy functions for logical replication slots
+--
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', false, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', true, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin      | pgoutput      | f
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin_temp | pgoutput      | t
+ orig_slot1 | test_decoding | f         | copied_slot1_no_change          | test_decoding | f
+(3 rows)
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase max_replication_slots.
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', true, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', false, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin      | pgoutput      | t
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin_temp | pgoutput      | f
+ orig_slot2 | test_decoding | t         | copied_slot2_no_change          | test_decoding | t
+(3 rows)
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+ERROR:  cannot copy a replication slot to the different type of replication slot
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+--
+-- Test copy functions for physical replication slots
+--
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+       slot_name        | slot_type | temporary 
+------------------------+-----------+-----------
+ orig_slot1             | physical  | f
+ orig_slot2             | physical  | f
+ copied_slot1_no_change | physical  | f
+ copied_slot1_temp      | physical  | t
+(4 rows)
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  cannot copy a replication slot to the different type of replication slot
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+ERROR:  cannot copy a replication slot that doesn't reserve WAL
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  | temporary |       slot_name        | temporary 
+------------+-----------+------------------------+-----------
+ orig_slot2 | t         | copied_slot2_no_change | t
+ orig_slot2 | t         | copied_slot2_notemp    | f
+(2 rows)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index c8d08f8..87b4307 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -76,3 +76,97 @@ SELECT slot_name FROM pg_create_physical_replication_slot('regression_slot3');
 SELECT pg_replication_slot_advance('regression_slot3', '0/0'); -- invalid LSN
 SELECT pg_replication_slot_advance('regression_slot3', '0/1'); -- error
 SELECT pg_drop_replication_slot('regression_slot3');
+
+--
+-- Test copy functions for logical replication slots
+--
+
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', false, 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', true, 'pgoutput');
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', true, 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', false, 'pgoutput');
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+
+--
+-- Test copy functions for physical replication slots
+--
+
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 03859a7..52936d0 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19516,6 +19516,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing physical replication slot name <parameter>src_slot_name</parameter>
+        to a physical replication slot named <parameter>dst_slot_name</parameter>.
+        The copied physical slot starts to reserve WAL from the same <acronym>LSN</acronym> as the
+        source slot.
+        <parameter>temporary</parameter> is optional. If <parameter>temporary</parameter>
+        is omitted, the same value as the source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type> <optional>, <parameter>plugin</parameter> <type>name</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing logical replication slot name <parameter>src_slot_name</parameter>
+        to a logical replication slot named <parameter>dst_slot_name</parameter>
+        while changing the output plugin and persistence. The copied logical slot starts
+        from the same <acronym>LSN</acronym> as the source logical slot. Both
+        <parameter>temporary</parameter> and <parameter>plugin</parameter> are optional.
+        If <parameter>temporary</parameter> or <parameter>plugin</parameter> are omitted,
+        the same values as the source logical slot are used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 6e5bc12..30f8482 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -221,6 +221,10 @@ StartupDecodingContext(List *output_plugin_options,
  * as the decoding context because further memory contexts will be created
  * inside it.
  *
+ * If restart_lsn is a valid value, we start decoding from the given lsn
+ * without WAL reservation routine. So the caller must guarantee that WAL
+ * is available.
+ *
  * Returns an initialized decoding context after calling the output plugin's
  * startup function.
  */
@@ -228,6 +232,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -271,7 +276,14 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	if (XLogRecPtrIsInvalid(restart_lsn))
+		ReplicationSlotReserveWal();
+	else
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+	}
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -316,7 +328,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, restart_lsn, xmin_horizon,
 								 need_full_snapshot, false,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 224dd92..560051d 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -17,6 +17,7 @@
 #include "miscadmin.h"
 
 #include "access/htup_details.h"
+#include "access/xlog_internal.h"
 #include "replication/decode.h"
 #include "replication/slot.h"
 #include "replication/logical.h"
@@ -36,6 +37,38 @@ check_permissions(void)
 }
 
 /*
+ * Helper function for creating a new physical replication slot with
+ * given arguments. Note that this function doesn't release the created
+ * slot.
+ *
+ * If restart_lsn is a valid value, we use it without WAL reservation
+ * routine. So the caller must guarantee that WAL is available.
+ */
+static void
+create_physical_replication_slot(char *name, bool immediately_reserve,
+								 bool temporary, XLogRecPtr restart_lsn)
+{
+	Assert(!MyReplicationSlot);
+
+	/* acquire replication slot, this will check for conflicting names */
+	ReplicationSlotCreate(name, false,
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+
+	if (immediately_reserve)
+	{
+		/* Reserve WAL as the user asked for it */
+		if (XLogRecPtrIsInvalid(restart_lsn))
+			ReplicationSlotReserveWal();
+		else
+			MyReplicationSlot->data.restart_lsn = restart_lsn;
+
+		/* Write this slot to disk */
+		ReplicationSlotMarkDirty();
+		ReplicationSlotSave();
+	}
+}
+
+/*
  * SQL function for creating a new physical (streaming replication)
  * replication slot.
  */
@@ -51,8 +84,6 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 	HeapTuple	tuple;
 	Datum		result;
 
-	Assert(!MyReplicationSlot);
-
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
 
@@ -60,22 +91,16 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 
 	CheckSlotRequirements();
 
-	/* acquire replication slot, this will check for conflicting names */
-	ReplicationSlotCreate(NameStr(*name), false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+	create_physical_replication_slot(NameStr(*name),
+									 immediately_reserve,
+									 temporary,
+									 InvalidXLogRecPtr);
 
 	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
 	nulls[0] = false;
 
 	if (immediately_reserve)
 	{
-		/* Reserve WAL as the user asked for it */
-		ReplicationSlotReserveWal();
-
-		/* Write this slot to disk */
-		ReplicationSlotMarkDirty();
-		ReplicationSlotSave();
-
 		values[1] = LSNGetDatum(MyReplicationSlot->data.restart_lsn);
 		nulls[1] = false;
 	}
@@ -94,32 +119,18 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 
 
 /*
- * SQL function for creating a new logical replication slot.
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Note that this function doesn't release the created
+ * slot.
  */
-Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+static void
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr restart_lsn)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
 	LogicalDecodingContext *ctx = NULL;
 
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
-	Datum		values[2];
-	bool		nulls[2];
-
 	Assert(!MyReplicationSlot);
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
-	check_permissions();
-
-	CheckLogicalDecodingRequirements();
-
 	/*
 	 * Acquire a logical decoding slot, this will check for conflicting names.
 	 * Initially create persistent slot as ephemeral - that allows us to
@@ -128,25 +139,54 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									restart_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
+}
+
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	Datum		result;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	check_permissions();
+
+	CheckLogicalDecodingRequirements();
+
+	create_logical_replication_slot(NameStr(*name),
+									NameStr(*plugin),
+									temporary,
+									InvalidXLogRecPtr);
+
+	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
+	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
 
 	memset(nulls, 0, sizeof(nulls));
 
@@ -558,3 +598,252 @@ pg_replication_slot_advance(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Helper function of copying a replication slot.
+ */
+static Datum
+copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
+{
+	Name			src_name = PG_GETARG_NAME(0);
+	Name			dst_name = PG_GETARG_NAME(1);
+	ReplicationSlot *src = NULL;
+	XLogRecPtr		src_restart_lsn;
+	bool			src_islogical;
+	bool			temporary;
+	char			*plugin;
+	Datum			values[2];
+	bool			nulls[2];
+	Datum			result;
+	int				i;
+	TupleDesc		tupdesc;
+	HeapTuple		tuple;
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	check_permissions();
+
+	if (logical_slot)
+		CheckLogicalDecodingRequirements();
+	else
+		CheckSlotRequirements();
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	/*
+	 * To prevent the WAL of the destination slot from removal during copy,
+	 * we copy current data of the source slot first. Then install those in
+	 * the new slot. The source slot could have progressed while installing,
+	 * but the installed values prevent global horizons from progressing
+	 * further. Therefore a second copy is sufficiently up-to-date.
+	 */
+	for (i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		if (s->in_use && strcmp(NameStr(s->data.name), NameStr(*src_name)) == 0)
+		{
+			SpinLockAcquire(&s->mutex);
+			src_islogical = SlotIsLogical(s);
+			src_restart_lsn = s->data.restart_lsn;
+			temporary = s->data.persistency == RS_TEMPORARY;
+			plugin = logical_slot ? pstrdup(NameStr(s->data.plugin)) : NULL;
+			SpinLockRelease(&s->mutex);
+
+			src = s;
+			break;
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	if (src == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("replication slot \"%s\" does not exist", NameStr(*src_name))));
+
+	/* Check type of replication slot */
+	if (src_islogical != logical_slot)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a replication slot to the different type of replication slot"))));
+
+	/* Copying non-reserved slot doesn't make sense */
+	if (XLogRecPtrIsInvalid(src_restart_lsn))
+	{
+		Assert(!logical_slot);
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a replication slot that doesn't reserve WAL"))));
+	}
+
+	/* Overwrite by optional arguments */
+	if (PG_NARGS() >= 3)
+		temporary = PG_GETARG_BOOL(2);
+	if (PG_NARGS() >= 4)
+	{
+		Assert(logical_slot);
+		plugin = NameStr(*(PG_GETARG_NAME(3)));
+	}
+
+	/* Create new slot and acquire it */
+	if (logical_slot)
+		create_logical_replication_slot(NameStr(*dst_name),
+										plugin,
+										temporary,
+										src_restart_lsn);
+	else
+		create_physical_replication_slot(NameStr(*dst_name),
+										 true,
+										 temporary,
+										 src_restart_lsn);
+
+	/*
+	 * Update the destination slot with the second copy of the source slot
+	 * to reserve WAL.
+	 */
+	{
+		TransactionId	copy_effective_xmin;
+		TransactionId	copy_effective_catalog_xmin;
+		TransactionId	copy_xmin;
+		TransactionId	copy_catalog_xmin;
+		XLogRecPtr		copy_restart_lsn;
+		bool			copy_islogical;
+		char			*copy_name;
+
+		/* Copy data of source slot again */
+		SpinLockAcquire(&src->mutex);
+		copy_effective_xmin = src->effective_xmin;
+		copy_effective_catalog_xmin = src->effective_catalog_xmin;
+
+		copy_xmin = src->data.xmin;
+		copy_catalog_xmin = src->data.catalog_xmin;
+		copy_restart_lsn = src->data.restart_lsn;
+
+		/* for existence check */
+		copy_name = pstrdup(NameStr(src->data.name));
+		copy_islogical = SlotIsLogical(src);
+		SpinLockRelease(&src->mutex);
+
+		/*
+		 * Check if the source slot still exists and is valid. We regards it as
+		 * invalid if the type of replication slot or name has been changed, or
+		 * the restart_lsn either is invalid or have gone backward. The restart_lsn
+		 * of second copy can go backward when the source slot is dropped and
+		 * copied form another old slot during installation. Since erroring out will
+		 * release and drop the destination slot we don't need to release it here.
+		 */
+		if (copy_restart_lsn < src_restart_lsn ||
+			src_islogical != copy_islogical ||
+			strcmp(copy_name, NameStr(*src_name)) != 0)
+			ereport(ERROR,
+					(errmsg("could not copy logical replication slot \"%s\"",
+							NameStr(*src_name)),
+					 errdetail("The source replication slot has been dropped during copy")));
+
+		/* Install copied values again */
+		SpinLockAcquire(&MyReplicationSlot->mutex);
+		MyReplicationSlot->effective_xmin = copy_effective_xmin;
+		MyReplicationSlot->effective_catalog_xmin = copy_effective_catalog_xmin;
+
+		MyReplicationSlot->data.xmin = copy_xmin;
+		MyReplicationSlot->data.catalog_xmin = copy_catalog_xmin;
+		MyReplicationSlot->data.restart_lsn = copy_restart_lsn;
+		SpinLockRelease(&MyReplicationSlot->mutex);
+
+		ReplicationSlotMarkDirty();
+		ReplicationSlotsComputeRequiredXmin(false);
+		ReplicationSlotsComputeRequiredLSN();
+		ReplicationSlotSave();
+
+		/* Check if the restart_lsn is available */
+#ifdef USE_ASSERT_CHECKING
+		{
+			XLogSegNo	segno;
+
+			XLByteToSeg(copy_restart_lsn, segno, wal_segment_size);
+			Assert(XLogGetLastRemovedSegno() < segno);
+		}
+#endif
+	}
+
+	/* The destination slot is now fully created, mark it as persistent if needed */
+	if (logical_slot && !temporary)
+		ReplicationSlotPersist();
+
+	values[0] = NameGetDatum(dst_name);
+	nulls[0] = false;
+
+	if (!XLogRecPtrIsInvalid(MyReplicationSlot->data.confirmed_flush))
+	{
+		values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
+		nulls[1] = false;
+	}
+	else
+		nulls[1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	ReplicationSlotRelease();
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, false);
+}
+
+/*
+ * SQL function for copying a physical replication slot.
+ */
+Datum
+pg_copy_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, false);
+}
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 9b143f3..03c61f3 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -930,6 +930,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2e9d6cf..3895d71 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9676,6 +9676,20 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,immediately_reserve,temporary,slot_name,lsn}',
   prosrc => 'pg_create_physical_replication_slot' },
+{ oid => '4008', descr => 'copy a physical replication slot while changing temporality',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot' },
+{ oid => '4009', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot_no_temp' },
 { oid => '3780', descr => 'drop a replication slot',
   proname => 'pg_drop_replication_slot', provolatile => 'v', proparallel => 'u',
   prorettype => 'void', proargtypes => 'name',
@@ -9696,6 +9710,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4005', descr => 'copy a logical replication slot while changing temporality and plugin',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool name',
+  proallargtypes => '{name,name,bool,name,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,plugin,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4006', descr => 'copy a logical replication slot while changing temporality',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4007', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c8ffc4c..0a2a63a 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
-- 
2.10.5

#36David Steele
david@pgmasters.net
In reply to: Masahiko Sawada (#35)
Re: Re: Copy function for logical replication slots

On 3/11/19 5:16 AM, Masahiko Sawada wrote:

On Mon, Feb 25, 2019 at 4:50 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:

On Wed, Feb 20, 2019 at 1:00 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:

BTW, XLogRecPtrIsInvalid(copy_restart_lsn) || copy_restart_lsn <
src_restart_lsn is redundant, the former should be removed.

So attached the updated version patch.

Since the patch failed to build, attached the updated version patch.

This patch is failing testing so marked Waiting on Author.

Regards,
--
-David
david@pgmasters.net

#37Masahiko Sawada
sawada.mshk@gmail.com
In reply to: David Steele (#36)
1 attachment(s)
Re: Re: Copy function for logical replication slots

On Mon, Mar 25, 2019 at 5:26 PM David Steele <david@pgmasters.net> wrote:

On 3/11/19 5:16 AM, Masahiko Sawada wrote:

On Mon, Feb 25, 2019 at 4:50 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:

On Wed, Feb 20, 2019 at 1:00 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:

BTW, XLogRecPtrIsInvalid(copy_restart_lsn) || copy_restart_lsn <
src_restart_lsn is redundant, the former should be removed.

So attached the updated version patch.

Since the patch failed to build, attached the updated version patch.

This patch is failing testing so marked Waiting on Author.

Thank you!

OIDs for new replication copy function are conflicted with newly added
jsonpath functions. I've attached the updated patch.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

Attachments:

v12-0001-Add-copy-function-for-replication-slots.patchapplication/octet-stream; name=v12-0001-Add-copy-function-for-replication-slots.patchDownload
From 01f034b15e9caf774ccafd33ee34eb70b8786bbe Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 18 Feb 2019 18:45:56 +0900
Subject: [PATCH v12] Add copy function for replication slots

Add new user functions to copy both physical and logical replication
slots while changing properties.

We don't hold the source slot during copy. To prevent the WAL of
destination slot from removal, we copy the source slot data
first. Then install those in the new slot. The source slot could have
progressed while installing, but the installed valuses prevent global
horizons progressing further. Then copy the source slot again, and
do existance check and installing up-to-date values to the destination
slot again.
---
 contrib/test_decoding/expected/slot.out   | 234 +++++++++++++++++++
 contrib/test_decoding/sql/slot.sql        |  94 ++++++++
 doc/src/sgml/func.sgml                    |  41 ++++
 src/backend/replication/logical/logical.c |  16 +-
 src/backend/replication/slotfuncs.c       | 363 +++++++++++++++++++++++++++---
 src/backend/replication/walsender.c       |   1 +
 src/include/catalog/pg_proc.dat           |  35 +++
 src/include/replication/logical.h         |   1 +
 8 files changed, 746 insertions(+), 39 deletions(-)

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 523621a..85cbbcd 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -150,3 +150,237 @@ SELECT pg_drop_replication_slot('regression_slot3');
  
 (1 row)
 
+--
+-- Test copy functions for logical replication slots
+--
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', false, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', true, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin      | pgoutput      | f
+ orig_slot1 | test_decoding | f         | copied_slot1_change_plugin_temp | pgoutput      | t
+ orig_slot1 | test_decoding | f         | copied_slot1_no_change          | test_decoding | f
+(3 rows)
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase max_replication_slots.
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', true, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', false, 'pgoutput');
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  |    plugin     | temporary |            slot_name            |    plugin     | temporary 
+------------+---------------+-----------+---------------------------------+---------------+-----------
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin      | pgoutput      | t
+ orig_slot2 | test_decoding | t         | copied_slot2_change_plugin_temp | pgoutput      | f
+ orig_slot2 | test_decoding | t         | copied_slot2_no_change          | test_decoding | t
+(3 rows)
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+ERROR:  cannot copy a replication slot to the different type of replication slot
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+--
+-- Test copy functions for physical replication slots
+--
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+       slot_name        | slot_type | temporary 
+------------------------+-----------+-----------
+ orig_slot1             | physical  | f
+ orig_slot2             | physical  | f
+ copied_slot1_no_change | physical  | f
+ copied_slot1_temp      | physical  | t
+(4 rows)
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+ERROR:  cannot copy a replication slot to the different type of replication slot
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+ERROR:  cannot copy a replication slot that doesn't reserve WAL
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+ ?column? 
+----------
+ copy
+(1 row)
+
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+ ?column? 
+----------
+ copy
+(1 row)
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+ slot_name  | temporary |       slot_name        | temporary 
+------------+-----------+------------------------+-----------
+ orig_slot2 | t         | copied_slot2_no_change | t
+ orig_slot2 | t         | copied_slot2_notemp    | f
+(2 rows)
+
+SELECT pg_drop_replication_slot('orig_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index c8d08f8..87b4307 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -76,3 +76,97 @@ SELECT slot_name FROM pg_create_physical_replication_slot('regression_slot3');
 SELECT pg_replication_slot_advance('regression_slot3', '0/0'); -- invalid LSN
 SELECT pg_replication_slot_advance('regression_slot3', '0/1'); -- error
 SELECT pg_drop_replication_slot('regression_slot3');
+
+--
+-- Test copy functions for logical replication slots
+--
+
+-- Create and copy logical slots
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot1', 'test_decoding', false);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin', false, 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'copied_slot1_change_plugin_temp', true, 'pgoutput');
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Now we have maximum 4 replication slots. Check slots are properly
+-- released even when raise error during creating the target slot.
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+SELECT pg_drop_replication_slot('copied_slot1_change_plugin');
+
+-- Test based on the temporary logical slot
+SELECT 'init' FROM pg_create_logical_replication_slot('orig_slot2', 'test_decoding', true);
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin', true, 'pgoutput');
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot2', 'copied_slot2_change_plugin_temp', false, 'pgoutput');
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.plugin, o.temporary, c.slot_name, c.plugin, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn  AND o.confirmed_flush_lsn = c.confirmed_flush_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+-- Cannot copy a logical slot to a physical slot
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'failed'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('copied_slot2_change_plugin_temp');
+
+--
+-- Test copy functions for physical replication slots
+--
+
+-- Create and copy physical slots
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot1', true);
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', false);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot1', 'copied_slot1_temp', true);
+
+-- Check all copied slots status. Since all slots don't reserve WAL we check only other fields.
+SELECT slot_name, slot_type, temporary FROM pg_replication_slots;
+
+-- Cannot copy a physical slot to a logical slot
+SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
+
+-- Cannot copy a physical slot that doesn't reserve WAL
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'falied'); -- error
+
+-- temporary slots were dropped automatically
+SELECT pg_drop_replication_slot('orig_slot1');
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot1_no_change');
+
+-- Test based on the temporary physical slot
+SELECT 'init' FROM pg_create_physical_replication_slot('orig_slot2', true, true);
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_no_change');
+SELECT 'copy' FROM pg_copy_physical_replication_slot('orig_slot2', 'copied_slot2_notemp', false);
+
+-- Check all copied slots status
+SELECT
+    o.slot_name, o.temporary, c.slot_name, c.temporary
+FROM
+    (SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'orig%') as o
+    LEFT JOIN pg_replication_slots as c ON o.restart_lsn = c.restart_lsn
+WHERE
+    o.slot_name != c.slot_name
+ORDER BY o.slot_name, c.slot_name;
+
+SELECT pg_drop_replication_slot('orig_slot2');
+SELECT pg_drop_replication_slot('copied_slot2_no_change');
+SELECT pg_drop_replication_slot('copied_slot2_notemp');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 1a01473..b96c55e 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -20390,6 +20390,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
       <row>
        <entry>
         <indexterm>
+         <primary>pg_copy_physical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_physical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing physical replication slot name <parameter>src_slot_name</parameter>
+        to a physical replication slot named <parameter>dst_slot_name</parameter>.
+        The copied physical slot starts to reserve WAL from the same <acronym>LSN</acronym> as the
+        source slot.
+        <parameter>temporary</parameter> is optional. If <parameter>temporary</parameter>
+        is omitted, the same value as the source slot is used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
+         <primary>pg_copy_logical_replication_slot</primary>
+        </indexterm>
+        <literal><function>pg_copy_logical_replication_slot(<parameter>src_slot_name</parameter> <type>name</type>, <parameter>dst_slot_name</parameter> <type>name</type> <optional>, <parameter>temporary</parameter> <type>boolean</type> <optional>, <parameter>plugin</parameter> <type>name</type></optional></optional>)</function></literal>
+       </entry>
+       <entry>
+        (<parameter>slot_name</parameter> <type>name</type>, <parameter>lsn</parameter> <type>pg_lsn</type>)
+       </entry>
+       <entry>
+        Copies an existing logical replication slot name <parameter>src_slot_name</parameter>
+        to a logical replication slot named <parameter>dst_slot_name</parameter>
+        while changing the output plugin and persistence. The copied logical slot starts
+        from the same <acronym>LSN</acronym> as the source logical slot. Both
+        <parameter>temporary</parameter> and <parameter>plugin</parameter> are optional.
+        If <parameter>temporary</parameter> or <parameter>plugin</parameter> are omitted,
+        the same values as the source logical slot are used.
+       </entry>
+      </row>
+
+      <row>
+       <entry>
+        <indexterm>
          <primary>pg_logical_slot_get_changes</primary>
         </indexterm>
         <literal><function>pg_logical_slot_get_changes(<parameter>slot_name</parameter> <type>name</type>, <parameter>upto_lsn</parameter> <type>pg_lsn</type>, <parameter>upto_nchanges</parameter> <type>int</type>, VARIADIC <parameter>options</parameter> <type>text[]</type>)</function></literal>
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 6e5bc12..30f8482 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -221,6 +221,10 @@ StartupDecodingContext(List *output_plugin_options,
  * as the decoding context because further memory contexts will be created
  * inside it.
  *
+ * If restart_lsn is a valid value, we start decoding from the given lsn
+ * without WAL reservation routine. So the caller must guarantee that WAL
+ * is available.
+ *
  * Returns an initialized decoding context after calling the output plugin's
  * startup function.
  */
@@ -228,6 +232,7 @@ LogicalDecodingContext *
 CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
@@ -271,7 +276,14 @@ CreateInitDecodingContext(char *plugin,
 	StrNCpy(NameStr(slot->data.plugin), plugin, NAMEDATALEN);
 	SpinLockRelease(&slot->mutex);
 
-	ReplicationSlotReserveWal();
+	if (XLogRecPtrIsInvalid(restart_lsn))
+		ReplicationSlotReserveWal();
+	else
+	{
+		SpinLockAcquire(&slot->mutex);
+		slot->data.restart_lsn = restart_lsn;
+		SpinLockRelease(&slot->mutex);
+	}
 
 	/* ----
 	 * This is a bit tricky: We need to determine a safe xmin horizon to start
@@ -316,7 +328,7 @@ CreateInitDecodingContext(char *plugin,
 	ReplicationSlotMarkDirty();
 	ReplicationSlotSave();
 
-	ctx = StartupDecodingContext(NIL, InvalidXLogRecPtr, xmin_horizon,
+	ctx = StartupDecodingContext(NIL, restart_lsn, xmin_horizon,
 								 need_full_snapshot, false,
 								 read_page, prepare_write, do_write,
 								 update_progress);
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 224dd92..560051d 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -17,6 +17,7 @@
 #include "miscadmin.h"
 
 #include "access/htup_details.h"
+#include "access/xlog_internal.h"
 #include "replication/decode.h"
 #include "replication/slot.h"
 #include "replication/logical.h"
@@ -36,6 +37,38 @@ check_permissions(void)
 }
 
 /*
+ * Helper function for creating a new physical replication slot with
+ * given arguments. Note that this function doesn't release the created
+ * slot.
+ *
+ * If restart_lsn is a valid value, we use it without WAL reservation
+ * routine. So the caller must guarantee that WAL is available.
+ */
+static void
+create_physical_replication_slot(char *name, bool immediately_reserve,
+								 bool temporary, XLogRecPtr restart_lsn)
+{
+	Assert(!MyReplicationSlot);
+
+	/* acquire replication slot, this will check for conflicting names */
+	ReplicationSlotCreate(name, false,
+						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+
+	if (immediately_reserve)
+	{
+		/* Reserve WAL as the user asked for it */
+		if (XLogRecPtrIsInvalid(restart_lsn))
+			ReplicationSlotReserveWal();
+		else
+			MyReplicationSlot->data.restart_lsn = restart_lsn;
+
+		/* Write this slot to disk */
+		ReplicationSlotMarkDirty();
+		ReplicationSlotSave();
+	}
+}
+
+/*
  * SQL function for creating a new physical (streaming replication)
  * replication slot.
  */
@@ -51,8 +84,6 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 	HeapTuple	tuple;
 	Datum		result;
 
-	Assert(!MyReplicationSlot);
-
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
 
@@ -60,22 +91,16 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 
 	CheckSlotRequirements();
 
-	/* acquire replication slot, this will check for conflicting names */
-	ReplicationSlotCreate(NameStr(*name), false,
-						  temporary ? RS_TEMPORARY : RS_PERSISTENT);
+	create_physical_replication_slot(NameStr(*name),
+									 immediately_reserve,
+									 temporary,
+									 InvalidXLogRecPtr);
 
 	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
 	nulls[0] = false;
 
 	if (immediately_reserve)
 	{
-		/* Reserve WAL as the user asked for it */
-		ReplicationSlotReserveWal();
-
-		/* Write this slot to disk */
-		ReplicationSlotMarkDirty();
-		ReplicationSlotSave();
-
 		values[1] = LSNGetDatum(MyReplicationSlot->data.restart_lsn);
 		nulls[1] = false;
 	}
@@ -94,32 +119,18 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS)
 
 
 /*
- * SQL function for creating a new logical replication slot.
+ * Helper function for creating a new logical replication slot with
+ * given arguments. Note that this function doesn't release the created
+ * slot.
  */
-Datum
-pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+static void
+create_logical_replication_slot(char *name, char *plugin,
+								bool temporary, XLogRecPtr restart_lsn)
 {
-	Name		name = PG_GETARG_NAME(0);
-	Name		plugin = PG_GETARG_NAME(1);
-	bool		temporary = PG_GETARG_BOOL(2);
-
 	LogicalDecodingContext *ctx = NULL;
 
-	TupleDesc	tupdesc;
-	HeapTuple	tuple;
-	Datum		result;
-	Datum		values[2];
-	bool		nulls[2];
-
 	Assert(!MyReplicationSlot);
 
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
-	check_permissions();
-
-	CheckLogicalDecodingRequirements();
-
 	/*
 	 * Acquire a logical decoding slot, this will check for conflicting names.
 	 * Initially create persistent slot as ephemeral - that allows us to
@@ -128,25 +139,54 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
 	 * slots can be created as temporary from beginning as they get dropped on
 	 * error as well.
 	 */
-	ReplicationSlotCreate(NameStr(*name), true,
+	ReplicationSlotCreate(name, true,
 						  temporary ? RS_TEMPORARY : RS_EPHEMERAL);
 
 	/*
 	 * Create logical decoding context, to build the initial snapshot.
 	 */
-	ctx = CreateInitDecodingContext(NameStr(*plugin), NIL,
+	ctx = CreateInitDecodingContext(plugin, NIL,
 									false,	/* do not build snapshot */
+									restart_lsn,
 									logical_read_local_xlog_page, NULL, NULL,
 									NULL);
 
 	/* build initial snapshot, might take a while */
 	DecodingContextFindStartpoint(ctx);
 
-	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
-	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
-
 	/* don't need the decoding context anymore */
 	FreeDecodingContext(ctx);
+}
+
+/*
+ * SQL function for creating a new logical replication slot.
+ */
+Datum
+pg_create_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	Name		name = PG_GETARG_NAME(0);
+	Name		plugin = PG_GETARG_NAME(1);
+	bool		temporary = PG_GETARG_BOOL(2);
+	Datum		result;
+	TupleDesc	tupdesc;
+	HeapTuple	tuple;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	check_permissions();
+
+	CheckLogicalDecodingRequirements();
+
+	create_logical_replication_slot(NameStr(*name),
+									NameStr(*plugin),
+									temporary,
+									InvalidXLogRecPtr);
+
+	values[0] = NameGetDatum(&MyReplicationSlot->data.name);
+	values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
 
 	memset(nulls, 0, sizeof(nulls));
 
@@ -558,3 +598,252 @@ pg_replication_slot_advance(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Helper function of copying a replication slot.
+ */
+static Datum
+copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
+{
+	Name			src_name = PG_GETARG_NAME(0);
+	Name			dst_name = PG_GETARG_NAME(1);
+	ReplicationSlot *src = NULL;
+	XLogRecPtr		src_restart_lsn;
+	bool			src_islogical;
+	bool			temporary;
+	char			*plugin;
+	Datum			values[2];
+	bool			nulls[2];
+	Datum			result;
+	int				i;
+	TupleDesc		tupdesc;
+	HeapTuple		tuple;
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	check_permissions();
+
+	if (logical_slot)
+		CheckLogicalDecodingRequirements();
+	else
+		CheckSlotRequirements();
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+
+	/*
+	 * To prevent the WAL of the destination slot from removal during copy,
+	 * we copy current data of the source slot first. Then install those in
+	 * the new slot. The source slot could have progressed while installing,
+	 * but the installed values prevent global horizons from progressing
+	 * further. Therefore a second copy is sufficiently up-to-date.
+	 */
+	for (i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+
+		if (s->in_use && strcmp(NameStr(s->data.name), NameStr(*src_name)) == 0)
+		{
+			SpinLockAcquire(&s->mutex);
+			src_islogical = SlotIsLogical(s);
+			src_restart_lsn = s->data.restart_lsn;
+			temporary = s->data.persistency == RS_TEMPORARY;
+			plugin = logical_slot ? pstrdup(NameStr(s->data.plugin)) : NULL;
+			SpinLockRelease(&s->mutex);
+
+			src = s;
+			break;
+		}
+	}
+
+	LWLockRelease(ReplicationSlotControlLock);
+
+	if (src == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("replication slot \"%s\" does not exist", NameStr(*src_name))));
+
+	/* Check type of replication slot */
+	if (src_islogical != logical_slot)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a replication slot to the different type of replication slot"))));
+
+	/* Copying non-reserved slot doesn't make sense */
+	if (XLogRecPtrIsInvalid(src_restart_lsn))
+	{
+		Assert(!logical_slot);
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 (errmsg("cannot copy a replication slot that doesn't reserve WAL"))));
+	}
+
+	/* Overwrite by optional arguments */
+	if (PG_NARGS() >= 3)
+		temporary = PG_GETARG_BOOL(2);
+	if (PG_NARGS() >= 4)
+	{
+		Assert(logical_slot);
+		plugin = NameStr(*(PG_GETARG_NAME(3)));
+	}
+
+	/* Create new slot and acquire it */
+	if (logical_slot)
+		create_logical_replication_slot(NameStr(*dst_name),
+										plugin,
+										temporary,
+										src_restart_lsn);
+	else
+		create_physical_replication_slot(NameStr(*dst_name),
+										 true,
+										 temporary,
+										 src_restart_lsn);
+
+	/*
+	 * Update the destination slot with the second copy of the source slot
+	 * to reserve WAL.
+	 */
+	{
+		TransactionId	copy_effective_xmin;
+		TransactionId	copy_effective_catalog_xmin;
+		TransactionId	copy_xmin;
+		TransactionId	copy_catalog_xmin;
+		XLogRecPtr		copy_restart_lsn;
+		bool			copy_islogical;
+		char			*copy_name;
+
+		/* Copy data of source slot again */
+		SpinLockAcquire(&src->mutex);
+		copy_effective_xmin = src->effective_xmin;
+		copy_effective_catalog_xmin = src->effective_catalog_xmin;
+
+		copy_xmin = src->data.xmin;
+		copy_catalog_xmin = src->data.catalog_xmin;
+		copy_restart_lsn = src->data.restart_lsn;
+
+		/* for existence check */
+		copy_name = pstrdup(NameStr(src->data.name));
+		copy_islogical = SlotIsLogical(src);
+		SpinLockRelease(&src->mutex);
+
+		/*
+		 * Check if the source slot still exists and is valid. We regards it as
+		 * invalid if the type of replication slot or name has been changed, or
+		 * the restart_lsn either is invalid or have gone backward. The restart_lsn
+		 * of second copy can go backward when the source slot is dropped and
+		 * copied form another old slot during installation. Since erroring out will
+		 * release and drop the destination slot we don't need to release it here.
+		 */
+		if (copy_restart_lsn < src_restart_lsn ||
+			src_islogical != copy_islogical ||
+			strcmp(copy_name, NameStr(*src_name)) != 0)
+			ereport(ERROR,
+					(errmsg("could not copy logical replication slot \"%s\"",
+							NameStr(*src_name)),
+					 errdetail("The source replication slot has been dropped during copy")));
+
+		/* Install copied values again */
+		SpinLockAcquire(&MyReplicationSlot->mutex);
+		MyReplicationSlot->effective_xmin = copy_effective_xmin;
+		MyReplicationSlot->effective_catalog_xmin = copy_effective_catalog_xmin;
+
+		MyReplicationSlot->data.xmin = copy_xmin;
+		MyReplicationSlot->data.catalog_xmin = copy_catalog_xmin;
+		MyReplicationSlot->data.restart_lsn = copy_restart_lsn;
+		SpinLockRelease(&MyReplicationSlot->mutex);
+
+		ReplicationSlotMarkDirty();
+		ReplicationSlotsComputeRequiredXmin(false);
+		ReplicationSlotsComputeRequiredLSN();
+		ReplicationSlotSave();
+
+		/* Check if the restart_lsn is available */
+#ifdef USE_ASSERT_CHECKING
+		{
+			XLogSegNo	segno;
+
+			XLByteToSeg(copy_restart_lsn, segno, wal_segment_size);
+			Assert(XLogGetLastRemovedSegno() < segno);
+		}
+#endif
+	}
+
+	/* The destination slot is now fully created, mark it as persistent if needed */
+	if (logical_slot && !temporary)
+		ReplicationSlotPersist();
+
+	values[0] = NameGetDatum(dst_name);
+	nulls[0] = false;
+
+	if (!XLogRecPtrIsInvalid(MyReplicationSlot->data.confirmed_flush))
+	{
+		values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush);
+		nulls[1] = false;
+	}
+	else
+		nulls[1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	ReplicationSlotRelease();
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Copy logical replication slot (2 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin_temp(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * Copy logical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_logical_replication_slot_no_plugin(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * SQL function for copying a logical replication slot.
+ */
+Datum
+pg_copy_logical_replication_slot(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, true);
+}
+
+/*
+ * Copy physical replication slot (3 arguments)
+ *
+ * note: this wrapper is necessary to pass the sanity check in opr_sanity,
+ * which checks that all built-in functions that share the implementing C
+ * function take the same number of arguments
+ */
+Datum
+pg_copy_physical_replication_slot_no_temp(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, false);
+}
+
+/*
+ * SQL function for copying a physical replication slot.
+ */
+Datum
+pg_copy_physical_replication_slot(PG_FUNCTION_ARGS)
+{
+	return copy_replication_slot(fcinfo, false);
+}
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 4bb98ef..e3b651e 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -934,6 +934,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 
 		ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot,
+										InvalidXLogRecPtr,
 										logical_read_xlog_page,
 										WalSndPrepareWrite, WalSndWriteData,
 										WalSndUpdateProgress);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index acf1131..6f7f7dd 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9746,6 +9746,20 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,immediately_reserve,temporary,slot_name,lsn}',
   prosrc => 'pg_create_physical_replication_slot' },
+{ oid => '4220', descr => 'copy a physical replication slot while changing temporality',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot' },
+{ oid => '4221', descr => 'copy a physical replication slot',
+  proname => 'pg_copy_physical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_physical_replication_slot_no_temp' },
 { oid => '3780', descr => 'drop a replication slot',
   proname => 'pg_drop_replication_slot', provolatile => 'v', proparallel => 'u',
   prorettype => 'void', proargtypes => 'name',
@@ -9766,6 +9780,27 @@
   proargmodes => '{i,i,i,o,o}',
   proargnames => '{slot_name,plugin,temporary,slot_name,lsn}',
   prosrc => 'pg_create_logical_replication_slot' },
+{ oid => '4222', descr => 'copy a logical replication slot while changing temporality and plugin',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool name',
+  proallargtypes => '{name,name,bool,name,name,pg_lsn}',
+  proargmodes => '{i,i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,plugin,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot' },
+{ oid => '4223', descr => 'copy a logical replication slot while changing temporality',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name bool',
+  proallargtypes => '{name,name,bool,name,pg_lsn}',
+  proargmodes => '{i,i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,temporary,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin' },
+{ oid => '4224', descr => 'copy a logical replication slot',
+  proname => 'pg_copy_logical_replication_slot', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record', proargtypes => 'name name',
+  proallargtypes => '{name,name,name,pg_lsn}',
+  proargmodes => '{i,i,o,o}',
+  proargnames => '{src_slot_name,dst_slot_name,slot_name,lsn}',
+  prosrc => 'pg_copy_logical_replication_slot_no_plugin_temp' },
 { oid => '3782', descr => 'get changes from replication slot',
   proname => 'pg_logical_slot_get_changes', procost => '1000',
   prorows => '1000', provariadic => 'text', proisstrict => 'f',
diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h
index c8ffc4c..0a2a63a 100644
--- a/src/include/replication/logical.h
+++ b/src/include/replication/logical.h
@@ -97,6 +97,7 @@ extern void CheckLogicalDecodingRequirements(void);
 extern LogicalDecodingContext *CreateInitDecodingContext(char *plugin,
 						  List *output_plugin_options,
 						  bool need_full_snapshot,
+						  XLogRecPtr restart_lsn,
 						  XLogPageReadCB read_page,
 						  LogicalOutputPluginWriterPrepareWrite prepare_write,
 						  LogicalOutputPluginWriterWrite do_write,
-- 
2.10.5

#38Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Masahiko Sawada (#37)
Re: Re: Copy function for logical replication slots

Pushed this.

The mechanism of creating a new slot from the source, then later
advancing the LSN of the new slot using the updated values from the
source slot, seems quite clever. I reworded the comment that explained
how it is supposed to work; please double-check to ensure I got it
right.

I renamed the opr_sanity-induced wrappers. I see no reason to get
very precise about what's what ... these functions are all identical.

Thanks everyone!

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#39Petr Jelinek
petr.jelinek@2ndquadrant.com
In reply to: Alvaro Herrera (#38)
Re: Copy function for logical replication slots

On 05/04/2019 23:16, Alvaro Herrera wrote:

Pushed this.

Thanks!

The mechanism of creating a new slot from the source, then later
advancing the LSN of the new slot using the updated values from the
source slot, seems quite clever. I reworded the comment that explained
how it is supposed to work; please double-check to ensure I got it
right.

Besides the "We regards it" typo it looks fine to me.

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

#40Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Alvaro Herrera (#38)
Re: Re: Copy function for logical replication slots

On Sat, Apr 6, 2019 at 6:16 AM Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

Pushed this.

Thank you!

The mechanism of creating a new slot from the source, then later
advancing the LSN of the new slot using the updated values from the
source slot, seems quite clever. I reworded the comment that explained
how it is supposed to work; please double-check to ensure I got it
right.

It looks good to me other than a typo pointed out by Petr.

I renamed the opr_sanity-induced wrappers. I see no reason to get
very precise about what's what ... these functions are all identical.

Agreed.

Regards,

--
Masahiko Sawada
NIPPON TELEGRAPH AND TELEPHONE CORPORATION
NTT Open Source Software Center

#41Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Petr Jelinek (#39)
Re: Copy function for logical replication slots

On 2019-Apr-05, Petr Jelinek wrote:

On 05/04/2019 23:16, Alvaro Herrera wrote:

The mechanism of creating a new slot from the source, then later
advancing the LSN of the new slot using the updated values from the
source slot, seems quite clever. I reworded the comment that explained
how it is supposed to work; please double-check to ensure I got it
right.

Besides the "We regards it" typo it looks fine to me.

Doh! Thanks, fixed.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services