diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index ebd42096795..f441ec43314 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29095,7 +29095,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.
@@ -29120,7 +29121,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
The failover option of the source logical slot
is not copied and is set to false by default. This
is to avoid the risk of being unable to continue logical replication
- after failover to standby where the slot is being synchronized.
+ after failover to standby where the slot is being synchronized. Copy of
+ an invalidated slot is not allowed.
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 43c3cc7336b..01e98bc1cef 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -681,6 +681,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);
@@ -782,6 +789,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 create 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 aeb79f51e71..4eca17885d6 100644
--- a/src/test/recovery/t/035_standby_logical_decoding.pl
+++ b/src/test/recovery/t/035_standby_logical_decoding.pl
@@ -583,6 +583,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);