postgres.git / summary / log / commit / refs

commit    5f3a49abf7ff7bb4a9eba7101eb8e8021960074f
Author:   Amit Kapila <akapila@postgresql.org>
Date:     Thu Sep 03 08:42:53 2026 +0000

    Use WAL insert position for conflict detection.
    
    The retain_dead_tuples feature waits for concurrent transactions to be
    applied before advancing conflict_detection_slot.xmin. It gets the WAL
    write position from the publisher and waits until the apply remote
    position passes it. This ensures that dead tuples, commit timestamps, and
    origins needed for conflict detection (update_deleted,
    update_origin_differs, and delete_origin_differs) are retained while
    those transactions are applied.
    
    However, with asynchronous commit enabled on the publisher, a committed
    transaction may not have updated the WAL write position yet. As a result,
    the reported WAL position can lag behind the latest committed transaction.
    This can cause conflict_detection_slot.xmin to advance prematurely,
    allowing dead tuples needed for conflict detection to be removed before
    subsequent asynchronously committed transactions are applied.
    
    Report the end of the last inserted WAL record instead. A transaction
    inserts its commit record before it is marked committed, so the insert
    position cannot be behind a transaction that has already committed.
    Transactions that have entered the commit phase but have not yet inserted
    their commit record are still accounted for by the in-commit transaction
    ID reported in the same message.
    
    In addition to above, add missing trailing period to update_deleted
    conflict detail messages which are added for the same feature.
    
    Reported-by: Nisha Moond <nisha.moond412@gmail.com>
    Author: Zhijie Hou <houzj.fnst@fujitsu.com>
    Reviewed-by: Amit Kapila <amit.kapila16@gmail.com>
    Discussion: https://postgr.es/m/TY4PR01MB177182F547A62FC2666EC04EC94B72@TY4PR01MB17718.jpnprd01.prod.outlook.com
    Backpatch-through: 19, where it was introduced


src/backend/replication/logical/conflict.c | 8 ++++---- src/backend/replication/logical/worker.c | 6 +++--- src/backend/replication/walsender.c | 25 ++++++++++++++++++++++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/backend/replication/logical/conflict.c b/src/backend/replication/logical/conflict.c index f0a6ed20c60..b71a0c9e206 100644 --- a/src/backend/replication/logical/conflict.c +++ b/src/backend/replication/logical/conflict.c @@ -567,19 +567,19 @@ errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo, if (localts) { if (localorigin == InvalidReplOriginId) - appendStringInfo(&err_detail, _("The row to be updated was deleted locally in transaction %u at %s"), + appendStringInfo(&err_detail, _("The row to be updated was deleted locally in transaction %u at %s."), localxmin, timestamptz_to_str(localts)); else if (replorigin_by_oid(localorigin, true, &origin_name)) - appendStringInfo(&err_detail, _("The row to be updated was deleted by a different origin \"%s\" in transaction %u at %s"), + appendStringInfo(&err_detail, _("The row to be updated was deleted by a different origin \"%s\" in transaction %u at %s."), origin_name, localxmin, timestamptz_to_str(localts)); /* The origin that modified this row has been removed. */ else - appendStringInfo(&err_detail, _("The row to be updated was deleted by a non-existent origin in transaction %u at %s"), + appendStringInfo(&err_detail, _("The row to be updated was deleted by a non-existent origin in transaction %u at %s."), localxmin, timestamptz_to_str(localts)); } else - appendStringInfoString(&err_detail, _("The row to be updated was deleted")); + appendStringInfoString(&err_detail, _("The row to be updated was deleted.")); break; 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..e9331de3df5 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -2895,7 +2895,30 @@ 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 has already + * committed, as each of those already carries a commit timestamp earlier + * than this reply. A transaction's commit timestamp is written as part + * of its commit record, so it cannot have committed without that record + * already being at or before the insert position. A transaction that has + * determined its commit timestamp but not yet inserted the record is + * still in the commit phase, and so is reported by the in-commit scan + * above. + * + * 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"); [parent: 88aa6ca138a1]