v22-0002-Enable-and-disable-logical-decoding-also-when-sl.patch
application/octet-stream
Filename: v22-0002-Enable-and-disable-logical-decoding-also-when-sl.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v22-0002
Subject: Enable and disable logical decoding also when slot invalidation.
| File | + | − |
|---|---|---|
| doc/src/sgml/logicaldecoding.sgml | 7 | 7 |
| src/backend/replication/logical/decode.c | 1 | 1 |
| src/backend/replication/logical/logicalctl.c | 13 | 13 |
| src/backend/replication/slot.c | 34 | 15 |
| src/test/recovery/t/049_effective_wal_level.pl | 44 | 5 |
From cfb72d3e2e1f7a26008a52f407a3369479b87216 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Fri, 24 Oct 2025 13:34:17 -0700
Subject: [PATCH v22 2/2] Enable and disable logical decoding also when slot
invalidation.
---
doc/src/sgml/logicaldecoding.sgml | 14 +++---
src/backend/replication/logical/decode.c | 2 +-
src/backend/replication/logical/logicalctl.c | 26 +++++-----
src/backend/replication/slot.c | 49 +++++++++++++------
.../recovery/t/049_effective_wal_level.pl | 49 +++++++++++++++++--
5 files changed, 99 insertions(+), 41 deletions(-)
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index ff4d0e365e3..b0377523415 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -270,7 +270,7 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
<listitem>
<para>
When <xref linkend="guc-wal-level"/> is set to <literal>replica</literal>
- and at least one logical replication slot exists on the system.
+ and at least one valid logical replication slot exists on the system.
</para>
</listitem>
</itemizedlist>
@@ -285,17 +285,17 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
logical replication slot. This activation process involves several steps
and requires synchronization among processes, ensuring system-wide
consistency. Conversely, when the last logical replication slot is dropped
- from a system with <varname>wal_level</varname> set to <literal>replica</literal>,
- logical decoding is automatically disabled. Note that the deactivation of
- logical decoding might take some time as it is performed asynchronously
- by the checkpointer process.
+ from a system or invalidated with <varname>wal_level</varname> set to
+ <literal>replica</literal>, logical decoding is automatically disabled.
+ Note that the deactivation of logical decoding might take some time as it
+ is performed asynchronously by the checkpointer process.
</para>
<caution>
<para>
When <varname>wal_level</varname> is set to <literal>replica</literal>,
- dropping the last logical slot disables logical decoding on the primary,
- resulting in slots on standbys being invalidated.
+ dropping or invalidating the last logical slot disables logical decoding
+ on the primary, resulting in slots on standbys being invalidated.
</para>
</caution>
</sect2>
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 85c502555a5..ab46a27a2b5 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -153,7 +153,7 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
/*
* Even if wal_level on the primary got decreased to 'replica', as
- * long as there is at least one logical slot, the logical
+ * long as there is at least one valid logical slot, the logical
* decoding remains enabled. So we don't check the logical
* decoding availability here but do so in
* XLOG_LOGICAL_DECODING_STATUS_CHANGE case. It covers the case
diff --git a/src/backend/replication/logical/logicalctl.c b/src/backend/replication/logical/logicalctl.c
index 2d0c58a09e9..e7fe2ed46a0 100644
--- a/src/backend/replication/logical/logicalctl.c
+++ b/src/backend/replication/logical/logicalctl.c
@@ -4,7 +4,7 @@
*
* This module enables dynamic control of logical decoding availability.
* Logical decoding becomes active under two conditions: when the wal_level
- * parameter is set to 'logical', or when at least one logical replication
+ * parameter is set to 'logical', or when at least one valid logical replication
* slot exists with wal_level set to 'replica'. The system disables logical
* decoding when neither condition is met. Therefore, the dynamic control
* of logical decoding availability is required only when wal_level is set
@@ -30,13 +30,13 @@
* exit without releasing temporary slots explicitly. This lazy approach has
* a drawback: it may take longer to change the effective_wal_level and disable
* logical decoding, especially when the checkpointer is busy with other tasks.
- * However, since dropping the last slot should not happen frequently, we chose
- * this approach in all deactivation cases for simpler code implementation,
- * even though the lazy approach is required only in error cases or at process
- * exit time in principle. In the future, we could address this limitation
- * either by using a dedicated worker instead of the checkpointer, or by
- * implementing synchronous waiting during slot drops if workloads are significantly
- * affected by the lazy deactivation of logical decoding.
+ * However, since dropping or invalidating the last slot should not happen
+ * frequently, we chose this approach in all deactivation cases for simpler code
+ * implementation, even though the lazy approach is required only in error cases
+ * or at process exit time in principle. In the future, we could address this
+ * limitation either by using a dedicated worker instead of the checkpointer, or
+ * by implementing synchronous waiting during slot drops if workloads are
+ * significantly affected by the lazy deactivation of logical decoding.
*
* Standby servers inherit the logical decoding and logical WAL writing status
* from the primary server. Unlike normal activation and deactivation, these
@@ -358,9 +358,9 @@ retry:
/*
* When attempting to disable logical decoding, if there is at least one
- * logical slot, we cannot disable it. We need to check it here since
- * slots could be created or dropped while waiting for the status change
- * below.
+ * valid logical slot, we cannot disable it. We need to check it here
+ * since slots could be created or dropped while waiting for the status
+ * change below.
*/
if (!new_status && CheckLogicalSlotExists())
{
@@ -490,8 +490,8 @@ EnsureLogicalDecodingEnabled(void)
/*
* Initiate a request for disabling logical decoding.
*
- * This function expects to be called after dropping the possibly-last
- * logical replication slot as it doesn't check the logical slot presence.
+ * This function expects to be called after dropping or invalidating the
+ * possibly-last logical replication slot as it doesn't check the logical slot presence.
*/
void
RequestDisableLogicalDecoding(void)
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 961618f812d..1d593e69738 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -856,12 +856,12 @@ ReplicationSlotCleanup(bool synced_only)
{
int i;
bool dropped_logical = false;
- int nlogicalslots;
+ int n_valid_logicalslots;
Assert(MyReplicationSlot == NULL);
restart:
- nlogicalslots = 0;
+ n_valid_logicalslots = 0;
LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
for (i = 0; i < max_replication_slots; i++)
{
@@ -870,8 +870,8 @@ restart:
if (!s->in_use)
continue;
- if (SlotIsLogical(s))
- nlogicalslots++;
+ if (SlotIsLogical(s) && s->data.invalidated == RS_INVAL_NONE)
+ n_valid_logicalslots++;
SpinLockAcquire(&s->mutex);
if ((s->active_pid == MyProcPid &&
@@ -895,7 +895,7 @@ restart:
LWLockRelease(ReplicationSlotControlLock);
- if (dropped_logical && nlogicalslots == 0)
+ if (dropped_logical && n_valid_logicalslots == 0)
RequestDisableLogicalDecoding();
}
@@ -1462,14 +1462,14 @@ void
ReplicationSlotsDropDBSlots(Oid dboid)
{
int i;
- int nlogicalslots;
+ int n_valid_logicalslots;
bool dropped = false;
if (max_replication_slots <= 0)
return;
restart:
- nlogicalslots = 0;
+ n_valid_logicalslots = 0;
LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
for (i = 0; i < max_replication_slots; i++)
{
@@ -1487,13 +1487,14 @@ restart:
if (!SlotIsLogical(s))
continue;
- nlogicalslots++;
+ if (s->data.invalidated == RS_INVAL_NONE)
+ n_valid_logicalslots++;
/* not our database, skip */
if (s->data.database != dboid)
continue;
- /* NB: intentionally including invalidated slots */
+ /* NB: intentionally including invalidated slots to drop */
/* acquire slot, so ReplicationSlotDropAcquired can be reused */
SpinLockAcquire(&s->mutex);
@@ -1550,12 +1551,12 @@ restart:
}
LWLockRelease(ReplicationSlotControlLock);
- if (dropped && nlogicalslots == 0)
+ if (dropped && n_valid_logicalslots == 0)
RequestDisableLogicalDecoding();
}
/*
- * Returns true if there is at least in-use logical replication slot.
+ * Returns true if there is at least in-use valid logical replication slot.
*/
bool
CheckLogicalSlotExists(void)
@@ -1576,7 +1577,8 @@ CheckLogicalSlotExists(void)
if (!s->in_use)
continue;
- /* NB: counting invalidated slots */
+ if (s->data.invalidated != RS_INVAL_NONE)
+ continue;
if (SlotIsLogical(s))
{
@@ -2156,6 +2158,7 @@ InvalidateObsoleteReplicationSlots(uint32 possible_causes,
{
XLogRecPtr oldestLSN;
bool invalidated = false;
+ int n_valid_logicalslots;
Assert(!(possible_causes & RS_INVAL_HORIZON) || TransactionIdIsValid(snapshotConflictHorizon));
Assert(!(possible_causes & RS_INVAL_WAL_REMOVED) || oldestSegno > 0);
@@ -2167,6 +2170,7 @@ InvalidateObsoleteReplicationSlots(uint32 possible_causes,
XLogSegNoOffsetToRecPtr(oldestSegno, 0, wal_segment_size, oldestLSN);
restart:
+ n_valid_logicalslots = 0;
LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
for (int i = 0; i < max_replication_slots; i++)
{
@@ -2175,9 +2179,15 @@ restart:
if (!s->in_use)
continue;
- /* Prevent invalidation of logical slots during binary upgrade */
- if (SlotIsLogical(s) && IsBinaryUpgrade)
- continue;
+ if (SlotIsLogical(s))
+ {
+ if (s->data.invalidated == RS_INVAL_NONE)
+ n_valid_logicalslots++;
+
+ /* Prevent invalidation of logical slots during binary upgrade */
+ if (IsBinaryUpgrade)
+ continue;
+ }
if (InvalidatePossiblyObsoleteSlot(possible_causes, s, oldestLSN, dboid,
snapshotConflictHorizon,
@@ -2196,6 +2206,15 @@ restart:
{
ReplicationSlotsComputeRequiredXmin(false);
ReplicationSlotsComputeRequiredLSN();
+
+ /*
+ * Request the checkpointer process to disable logical decoding if no
+ * valid logical slots exist. While the checkpointer can call this
+ * function during a checkpoint, it doesn't perform the actual
+ * deactivation here to complete the checkpointing first.
+ */
+ if (n_valid_logicalslots == 0)
+ RequestDisableLogicalDecoding();
}
return invalidated;
diff --git a/src/test/recovery/t/049_effective_wal_level.pl b/src/test/recovery/t/049_effective_wal_level.pl
index 7d1db792faf..b5b1ea0165e 100644
--- a/src/test/recovery/t/049_effective_wal_level.pl
+++ b/src/test/recovery/t/049_effective_wal_level.pl
@@ -108,8 +108,47 @@ like(
# Revert the modified settings.
$primary->adjust_conf('postgresql.conf', 'wal_level', 'replica');
$primary->adjust_conf('postgresql.conf', 'max_wal_senders', '10');
+
+# Add other settings to test if we disable logical decoding when invalidating the last
+# logical slot.
+$primary->append_conf(
+ 'postgresql.conf',
+ qq[
+min_wal_size = 32MB
+max_wal_size = 32MB
+max_slot_wal_keep_size = 16MB
+]);
$primary->start;
+# Advance WAL and check if the slot gets invalidated.
+$primary->advance_wal(2);
+$primary->safe_psql('postgres', qq[CHECKPOINT]);
+is( $primary->safe_psql(
+ 'postgres',
+ qq[
+select invalidation_reason = 'wal_removed' from pg_replication_slots where slot_name = 'test_slot';
+ ]),
+ 't',
+ 'test_slot gets invalidated due to wal_removed');
+
+# Check if logical decoding is disabled after invalidating the last logical slot.
+wait_for_logical_decoding_disabled($primary);
+test_wal_level($primary, "replica|replica",
+ "effective_wal_level got decreased to 'replica' after invalidating the last logical slot"
+);
+
+# Revert the modified settings, and restart the server.
+$primary->adjust_conf('postgresql.conf', 'max_slot_wal_keep_size', undef);
+$primary->adjust_conf('postgresql.conf', 'min_wal_size', undef);
+$primary->adjust_conf('postgresql.conf', 'max_wal_size', undef);
+$primary->restart;
+
+# Recreate the logical slot to enable logical decoding again.
+$primary->safe_psql('postgres',
+ qq[select pg_drop_replication_slot('test_slot')]);
+$primary->safe_psql('postgres',
+ qq[select pg_create_logical_replication_slot('test_slot', 'pgoutput')]);
+
# Take backup during the effective_wal_level being 'logical'. But note that
# replication slots are not included in the backup.
$primary->backup('my_backup');
@@ -275,11 +314,11 @@ test_wal_level($standby4, "replica|logical",
"effective_wal_level remains 'logical' on standby even after setting wal_level to 'replica' on primary"
);
-# Promote the standby4 and check if effective_wal_level remains 'logical' even
-# after the promotion since it has an invalidated logical slot.
+# Promote the standby4 and check if effective_wal_level is now 'logical' after
+# the promotion since there is no valid logical slot.
$standby4->promote;
-test_wal_level($standby4, "replica|logical",
- "effective_wal_level remains 'logical' even after promotion as it has one invalidated slot"
+test_wal_level($standby4, "replica|replica",
+ "effective_wal_level got decreased to 'replica' as there is no valid logical slot"
);
# Drop the invalidated slot, decreasing effective_wal_level to 'replica'.
@@ -287,7 +326,7 @@ $standby4->safe_psql('postgres',
qq[select pg_drop_replication_slot('standby4_slot')]);
wait_for_logical_decoding_disabled($standby4);
test_wal_level($standby4, "replica|replica",
- "effective_wal_level got decreased to 'replica' after dropping the last invalidated slot"
+ "effective_wal_level doesn't change after dropping the last invalidated slot"
);
$standby4->stop;
--
2.47.3