Hi,

On Wed, Sep 30, 2026 at 10:19 AM Sergei Patiakin
<[email protected]> wrote:
>
> For an aborted transaction, there is today a gap between the
> AtEOXact_LogicalCtl call and the XID being cleared. If an
> effective_wal_level change occurs during that gap, the change will be
> missed. That means the next transaction on that session will not be
> logically decoded.
>
> Attaching a reproducer bash script that demonstrates the missing decoded 
> change.
> Attaching a fix that moves the AtEOXact_LogicalCtl call later, closing the 
> gap.

Thank you for the report. I agree it's an issue that we should fix for v19.

The fix looks good to me. We need to check and update XLogLogicalInfo
in every place where we reset the top-level transaction id.

I've updated the patch with the regression tests. Please review it.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From c24b0c58c91f4a7cb6dd1d44641d554569bea3c6 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Wed, 30 Sep 2026 20:14:51 -0700
Subject: [PATCH v1] Fix deferred XLogLogicalInfo update after a failed
 transaction block.

Previously, a backend with an assigned XID deferred the
XLogLogicalInfo update requested by a procsignal barrier, and we
applied it in AbortTransaction() on abort. However, a failed
transaction block keeps its XID until ROLLBACK, so a barrier absorbed
while idle in that state was deferred, and CleanupTransaction() did
not apply it. The next transaction then ran entirely with the stale
value. If logical decoding had just been enabled, that transaction's
changes were written without logical information and silently skipped
by decoding.

Fix by applying the pending update in CleanupTransaction() instead of
AbortTransaction().

Patch by Sergei Patiakin, with tests added by me.

Oversight in commit 67c20979ce7.

Reported-by: Sergei Patiakin <[email protected]>
Author: Sergei Patiakin <[email protected]>
Discussion: https://postgr.es/m/cane55rapenafaqxlwrvm-nc0y5gkrbfzfvahvyp89ags2sy...@mail.gmail.com
Backpatch-through: 19
---
 src/backend/access/transam/xact.c             |  8 ++++-
 src/backend/replication/logical/logicalctl.c  |  6 ++++
 .../recovery/t/051_effective_wal_level.pl     | 31 +++++++++++++++++++
 3 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 7b67db514ec..7748643d3fa 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -3054,7 +3054,6 @@ AbortTransaction(void)
 		AtEOXact_PgStat(false, is_parallel_worker);
 		AtEOXact_ApplyLauncher(false);
 		AtEOXact_LogicalRepWorkers(false);
-		AtEOXact_LogicalCtl();
 		pgstat_report_xact_timestamp(0);
 	}
 
@@ -3107,6 +3106,13 @@ CleanupTransaction(void)
 	XactTopFullTransactionId = InvalidFullTransactionId;
 	nParallelCurrentXids = 0;
 
+	/*
+	 * Apply any pending XLogLogicalInfo update.  This is done here rather
+	 * than in AbortTransaction(), as a failed transaction block keeps its XID
+	 * until we get here.  See AtEOXact_LogicalCtl().
+	 */
+	AtEOXact_LogicalCtl();
+
 	/*
 	 * done with abort processing, set current transaction state back to
 	 * default
diff --git a/src/backend/replication/logical/logicalctl.c b/src/backend/replication/logical/logicalctl.c
index 642d965bd1c..a1238d7bc5d 100644
--- a/src/backend/replication/logical/logicalctl.c
+++ b/src/backend/replication/logical/logicalctl.c
@@ -228,6 +228,12 @@ IsXLogLogicalInfoEnabled(void)
 
 /*
  * Reset the local cache at end of the transaction.
+ *
+ * Apply a pending XLogLogicalInfo update at end of the top-level transaction.
+ *
+ * This is called from CommitTransaction(), PrepareTransaction(), and
+ * CleanupTransaction(), which are the only places where the top-level XID is
+ * reset, so the next transaction always starts with the latest value.
  */
 void
 AtEOXact_LogicalCtl(void)
diff --git a/src/test/recovery/t/051_effective_wal_level.pl b/src/test/recovery/t/051_effective_wal_level.pl
index b11690863d9..138c8dab1ea 100644
--- a/src/test/recovery/t/051_effective_wal_level.pl
+++ b/src/test/recovery/t/051_effective_wal_level.pl
@@ -78,6 +78,37 @@ wait_for_logical_decoding_disabled($primary);
 test_wal_level($primary, "replica|replica",
 	"logical decoding disabled after repack");
 
+# Test that a backend applies an XLogLogicalInfo update that it received
+# while its transaction block was in the failed state, once the block ends.
+# The failed transaction keeps its XID until ROLLBACK, so the update is
+# deferred; it must not be carried over into the next transaction.
+my $psql_aborted = $primary->background_psql('postgres', on_error_stop => 0);
+$psql_aborted->query_safe(q[begin; select pg_current_xact_id();]);
+my ($aborted_out, $aborted_ret) = $psql_aborted->query(q[select 1/0;]);
+is($aborted_ret, 1, "transaction block failed");
+$psql_aborted->{stderr} = '';
+
+# Enable logical decoding while the backend is idle in the failed transaction
+# block.  This waits for all backends to absorb the barrier.
+$primary->safe_psql('postgres',
+   qq[select pg_create_logical_replication_slot('test_aborted_slot', 'test_decoding')]
+);
+
+# Check the value in the transaction right after ROLLBACK.  Note that both
+# commands need to be sent together, as query_safe() appends an empty query
+# that would run in its own transaction and apply the deferred update.
+is( $psql_aborted->query_safe(
+       q[rollback; select current_setting('effective_wal_level');]),
+   'logical',
+   "effective_wal_level is updated after rolling back a failed transaction block"
+);
+$psql_aborted->quit;
+
+$primary->safe_psql('postgres',
+   qq[select pg_drop_replication_slot('test_aborted_slot')]);
+wait_for_logical_decoding_disabled($primary);
+
+
 # Create a new logical slot and check that effective_wal_level must be increased
 # to 'logical'.
 $primary->safe_psql('postgres',
-- 
2.55.0

Reply via email to