From 326619b374a8e5c7bc4a34f972d75ee8e27e2c5c Mon Sep 17 00:00:00 2001 From: Zhijie Hou Date: Wed, 2 Sep 2026 00:44:31 +0800 Subject: [PATCH vM1] Report WAL insert position in primary status update messages The retain_dead_tuples feature waits for concurrent transactions to be applied before advancing conflict_detection_slot.xmin. It fetches the WAL write position from the publisher and waits until the apply remote position passes it. This is necessary to retain dead tuples, commit timestamps, and origins for conflict detection (update_deleted, update_origin_differs, delete_origin_differs) when applying those concurrent transactions. However, when asynchronous commit is enabled on the publisher, concurrent transactions do not update the WAL write position immediately. As a result, the received WAL position may be earlier than intended (e.g., it may not reflect the latest committed transaction). This can cause conflict_detection_slot.xmin to advance prematurely, losing dead tuples needed for conflict detection when applying subsequent asynchronously committed transactions. Fix this by reporting the end of the last inserted WAL record instead, ensuring that the insert position covers every transaction that has already committed and has a commit timestamp. --- src/backend/replication/logical/worker.c | 6 +++--- src/backend/replication/walsender.c | 21 ++++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index 0efed3a6f77..455099f68e6 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -158,8 +158,8 @@ * * - RDT_REQUEST_PUBLISHER_STATUS: * Send a message to the walsender requesting the publisher status, which - * includes the latest WAL write position and information about transactions - * that are in the commit phase. + * includes the latest WAL insert position and information about + * transactions that are in the commit phase. * * - RDT_WAIT_FOR_PUBLISHER_STATUS: * Wait for the status from the walsender. After receiving the first status, @@ -417,7 +417,7 @@ typedef enum typedef struct RetainDeadTuplesData { RetainDeadTuplesPhase phase; /* current phase */ - XLogRecPtr remote_lsn; /* WAL write position on the publisher */ + XLogRecPtr remote_lsn; /* WAL insert position on the publisher */ /* * Oldest transaction ID that was in the commit phase on the publisher. diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index c65dd324325..2cdc149621c 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -2895,7 +2895,26 @@ ProcessStandbyPSRequestMessage(void) nextFullXid = ReadNextFullTransactionId(); fullOldestXidInCommit = FullTransactionIdFromAllowableAt(nextFullXid, oldestXidInCommit); - lsn = GetXLogWriteRecPtr(); + + /* + * Report the end of the last inserted WAL record rather than the WAL + * write position. A transaction that commits asynchronously (with + * synchronous_commit = off) clears DELAY_CHKPT_IN_COMMIT without flushing + * its commit record, so it is visible to neither the in-commit scan above + * nor a write position that has not yet reached its commit record. The + * subscriber waits until it has applied and flushed up to the reported + * position before advancing its non-removable transaction ID, so the + * reported position must cover every transaction that can already own a + * commit timestamp. The insert position guarantees that, because a + * commit timestamp is assigned only after the commit record has been + * inserted. + * + * GetXLogInsertEndRecPtr() is used rather than GetXLogInsertRecPtr() + * because the latter can return a position past the page header when the + * last record ends at a page boundary, which can never match a record end + * and would needlessly stall the subscriber's wait. + */ + lsn = GetXLogInsertEndRecPtr(); elog(DEBUG2, "sending primary status"); -- 2.43.0