Hi all,

While reviewing the online wal_level change feature (commit
67c20979ce7), I found an assertion failure during crash recovery.
Also, I noticed that pg_controldata doesn't show the logical decoding
status. Patches attached.

0001 fixes the assertion failure that happens in the following scenario:

1. Run the server with wal_level = 'replica' and create a logical
slot, which writes an XLOG_LOGICAL_DECODING_STATUS_CHANGE record that
activates logical decoding.
2. Drop the slot, and crash the server before the checkpointer
deactivates logical decoding.
3. Restart the server with wal_level = 'minimal'.

Crash recovery replays the status change record and activates logical
decoding, then UpdateLogicalDecodingStatusEndOfRecovery() fails the
assertion (Assert(!IsXLogLogicalInfoEnabled() &&
!IsLogicalDecodingEnabled())) that logical decoding is never active
with wal_level='minimal'.

I think that replaying the status change record itself is correct, as
it reflects the status at the time the record was written. The problem
is that the end-of-recovery code assumed that this cannot happen,
instead of adjusting the status. Therefore, the fix removes the
wal_level='minimal' special case from
UpdateLogicalDecodingStatusEndOfRecovery() so that it recomputes the
status as usual.

0002 adds the logical decoding status stored in the checkpoint record
to the pg_controldata output, which I found useful while investigating
the above issue.

Both patches are intended to be backpatched to v19.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From 99d4e515d3a059457268fdf253b20ec1c4cf1c93 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Wed, 15 Jul 2026 12:12:16 -0700
Subject: [PATCH v1 1/2] Correct logical decoding status at end of recovery
 with minimal WAL level.

Crash recovery running with wal_level='minimal' can replay an
XLOG_LOGICAL_DECODING_STATUS_CHANGE record that activates logical
decoding, if the server previously ran with a higher wal_level and
crashed after the last logical slot was dropped but before the
checkpointer deactivated logical decoding. Replaying such a record is
correct since it reflects the status at the time it was written.
However, UpdateLogicalDecodingStatusEndOfRecovery() asserted that
logical decoding is never active with wal_level='minimal', causing an
assertion failure at the end of recovery. In production builds, logical
decoding would remain active while running with wal_level='minimal'.

Instead of special-casing wal_level='minimal', recompute the status at
the end of recovery as usual: no logical slot can exist with
wal_level='minimal' as RestoreSlotFromDisk() would have rejected it, so
the recomputation always deactivates logical decoding in this case,
also writing the corresponding status change record.

Oversight in 67c20979ce7.

Discussion: https://postgr.es/m/
Backpatch-through: 19
---
 src/backend/replication/logical/logicalctl.c | 20 +++++++++-----------
 1 file changed, 9 insertions(+), 11 deletions(-)

diff --git a/src/backend/replication/logical/logicalctl.c b/src/backend/replication/logical/logicalctl.c
index c11d1316450..f6bc617426b 100644
--- a/src/backend/replication/logical/logicalctl.c
+++ b/src/backend/replication/logical/logicalctl.c
@@ -561,19 +561,17 @@ UpdateLogicalDecodingStatusEndOfRecovery(void)
 
 	Assert(RecoveryInProgress());
 
-	/*
-	 * With 'minimal' WAL level, there are no logical replication slots during
-	 * recovery. Logical decoding is always disabled, so there is no need to
-	 * synchronize XLogLogicalInfo.
-	 */
-	if (wal_level == WAL_LEVEL_MINIMAL)
-	{
-		Assert(!IsXLogLogicalInfoEnabled() && !IsLogicalDecodingEnabled());
-		return;
-	}
-
 	LWLockAcquire(LogicalDecodingControlLock, LW_EXCLUSIVE);
 
+	/*
+	 * With 'minimal' WAL level, no logical replication slot can exist (see
+	 * RestoreSlotFromDisk()), so the new status is always false. However,
+	 * logical decoding could have been enabled during recovery by replaying
+	 * an XLOG_LOGICAL_DECODING_STATUS_CHANGE record from WAL generated with
+	 * a higher wal_level, e.g. if the server crashed right after the last
+	 * logical slot was dropped and then restarted with wal_level='minimal'.
+	 * The code below disables logical decoding in that case.
+	 */
 	if (wal_level == WAL_LEVEL_LOGICAL || CheckLogicalSlotExists())
 		new_status = true;
 
-- 
2.54.0

From 7565506149e0ac7a93d9b253975f741bbb2fdc0f Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Wed, 15 Jul 2026 15:29:02 -0700
Subject: [PATCH v1 2/2] pg_controldata: Show logical decoding status.

The logical decoding status is stored in checkpoint records and used to
restore the status at server startup, but pg_controldata did not show
it. This information is useful for diagnosing issues around the
dynamicactivation and deactivation of logical decoding.

Oversight in 67c20979ce7.

Discussion: https://postgr.es/m/
Backpatch-through: 19
---
 src/bin/pg_controldata/pg_controldata.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index fe5fc5ec133..6fc87ed114d 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -264,6 +264,8 @@ main(int argc, char *argv[])
 		   ControlFile->checkPointCopy.PrevTimeLineID);
 	printf(_("Latest checkpoint's full_page_writes: %s\n"),
 		   ControlFile->checkPointCopy.fullPageWrites ? _("on") : _("off"));
+	printf(_("Latest checkpoint's logical decoding: %s\n"),
+		   ControlFile->checkPointCopy.logicalDecodingEnabled ? _("on") : _("off"));
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile->checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile->checkPointCopy.nextXid));
-- 
2.54.0

Reply via email to