diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 54f8a058471..5efac2f7e06 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -27258,7 +27258,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
The copied physical slot starts to reserve WAL from the same LSN as the
source slot.
temporary is optional. If temporary
- is omitted, the same value as the source slot is used.
+ is omitted, the same value as the source slot is used. Copy of an
+ invalidated slot is not allowed.
@@ -27280,6 +27281,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
from the same LSN as the source logical slot. Both
temporary and plugin are
optional; if they are omitted, the values of the source slot are used.
+ Copy of an invalidated slot is not allowed.
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 6035cf48160..612bdd99b52 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -756,6 +756,13 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot copy a replication slot that doesn't reserve WAL")));
+ /* Cannot copy an invalidated replication slot */
+ if (first_slot_contents.data.invalidated != RS_INVAL_NONE)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot copy invalidated replication slot \"%s\"",
+ NameStr(*src_name)));
+
/* Overwrite params from optional arguments */
if (PG_NARGS() >= 3)
temporary = PG_GETARG_BOOL(2);
@@ -843,6 +850,20 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
NameStr(*src_name)),
errhint("Retry when the source replication slot's confirmed_flush_lsn is valid.")));
+ /*
+ * Copying an invalid slot doesn't make sense. Note that the source
+ * slot can become invalid after we creat the new slot and copy the
+ * data of source slot. This is possible because the operations in
+ * InvalidateObsoleteReplicationSlots() are not serialized with this
+ * function. Even though we can't detect such a case here, the copied
+ * slot will become invalid in the next checkpoint cycle.
+ */
+ if (second_slot_contents.data.invalidated != RS_INVAL_NONE)
+ ereport(ERROR,
+ errmsg("cannot copy replication slot \"%s\"",
+ NameStr(*src_name)),
+ errdetail("The source replication slot was invalidated during the copy operation."));
+
/* Install copied values again */
SpinLockAcquire(&MyReplicationSlot->mutex);
MyReplicationSlot->effective_xmin = copy_effective_xmin;
diff --git a/src/test/recovery/t/035_standby_logical_decoding.pl b/src/test/recovery/t/035_standby_logical_decoding.pl
index 8120dfc2132..82ad7ce0c2b 100644
--- a/src/test/recovery/t/035_standby_logical_decoding.pl
+++ b/src/test/recovery/t/035_standby_logical_decoding.pl
@@ -563,6 +563,15 @@ check_pg_recvlogical_stderr($handle,
"can no longer get changes from replication slot \"vacuum_full_activeslot\""
);
+# Attempt to copy an invalidated logical replication slot
+($result, $stdout, $stderr) = $node_standby->psql(
+ 'postgres',
+ qq[select pg_copy_logical_replication_slot('vacuum_full_inactiveslot', 'vacuum_full_inactiveslot_copy');],
+ replication => 'database');
+ok( $stderr =~
+ /ERROR: cannot copy invalidated replication slot "vacuum_full_inactiveslot"/,
+ "invalidated slot cannot be copied");
+
# Turn hot_standby_feedback back on
change_hot_standby_feedback_and_wait_for_xmins(1, 1);