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
