From c2a23dcf9e3af1c80a99b4ee43f0885eb6894e3a Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Tue, 10 Mar 2026 15:24:39 -0400 Subject: [PATCH] Use the newest to-be-frozen xid as the conflict horizon for freezing Previously WAL records that froze tuples used OldestXmin as the snapshot conflict horizon, or the visibility cutoff if the page would become all-frozen. Both are newer than (or equal to) the newst XID actually frozen on the page. Track the newest XID that will be frozen and use that as the snapshot conflict horizon instead. This yields an older horizon resulting in fewer query cancellations on standbys. Author: Melanie Plageman Reviewed-by: Peter Geoghegan Discussion: https://postgr.es/m/CAAKRu_bbaUV8OUjAfVa_iALgKnTSfB4gO3jnkfpcFgrxEpSGJQ%40mail.gmail.com --- src/backend/access/heap/heapam.c | 14 +++++++++++ src/backend/access/heap/pruneheap.c | 36 +++++++++-------------------- src/include/access/heapam.h | 12 ++++++++++ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index 1ecc8330851..8f1c11a9350 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -7089,6 +7089,12 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask, * process this tuple as part of freezing its page, and return true. Return * false if nothing can be changed about the tuple right now. * + * FreezePageConflictXid is advanced only for xmin/xvac freezing, not for xmax + * changes. We only remove xmax state here when it is lock-only, or when the + * updater XID (including an updater member of a MultiXact) must be aborted; + * otherwise, the tuple would already be removable. Neither case affects + * visibility on a standby. + * * Also sets *totally_frozen to true if the tuple will be totally frozen once * caller executes returned freeze plan (or if the tuple was already totally * frozen by an earlier VACUUM). This indicates that there are no remaining @@ -7164,7 +7170,11 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple, /* Verify that xmin committed if and when freeze plan is executed */ if (freeze_xmin) + { frz->checkflags |= HEAP_FREEZE_CHECK_XMIN_COMMITTED; + if (TransactionIdFollows(xid, pagefrz->FreezePageConflictXid)) + pagefrz->FreezePageConflictXid = xid; + } } /* @@ -7183,6 +7193,9 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple, */ replace_xvac = pagefrz->freeze_required = true; + if (TransactionIdFollows(xid, pagefrz->FreezePageConflictXid)) + pagefrz->FreezePageConflictXid = xid; + /* Will set replace_xvac flags in freeze plan below */ } @@ -7492,6 +7505,7 @@ heap_freeze_tuple(HeapTupleHeader tuple, pagefrz.freeze_required = true; pagefrz.FreezePageRelfrozenXid = FreezeLimit; pagefrz.FreezePageRelminMxid = MultiXactCutoff; + pagefrz.FreezePageConflictXid = InvalidTransactionId; pagefrz.NoFreezePageRelfrozenXid = FreezeLimit; pagefrz.NoFreezePageRelminMxid = MultiXactCutoff; diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 65c9f393f41..8748fa882e9 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -377,6 +377,7 @@ prune_freeze_setup(PruneFreezeParams *params, /* initialize page freezing working state */ prstate->pagefrz.freeze_required = false; + prstate->pagefrz.FreezePageConflictXid = InvalidTransactionId; if (prstate->attempt_freeze) { Assert(new_relfrozen_xid && new_relmin_mxid); @@ -407,7 +408,6 @@ prune_freeze_setup(PruneFreezeParams *params, * PruneState. */ prstate->deadoffsets = presult->deadoffsets; - prstate->frz_conflict_horizon = InvalidTransactionId; /* * Vacuum may update the VM after we're done. We can keep track of @@ -746,22 +746,8 @@ heap_page_will_freeze(bool did_tuple_hint_fpi, * critical section. */ heap_pre_freeze_checks(prstate->buffer, prstate->frozen, prstate->nfrozen); - - /* - * Calculate what the snapshot conflict horizon should be for a record - * freezing tuples. We can use the visibility_cutoff_xid as our cutoff - * for conflicts when the whole page is eligible to become all-frozen - * in the VM once we're done with it. Otherwise, we generate a - * conservative cutoff by stepping back from OldestXmin. - */ - if (prstate->set_all_frozen) - prstate->frz_conflict_horizon = prstate->visibility_cutoff_xid; - else - { - /* Avoids false conflicts when hot_standby_feedback in use */ - prstate->frz_conflict_horizon = prstate->cutoffs->OldestXmin; - TransactionIdRetreat(prstate->frz_conflict_horizon); - } + Assert(TransactionIdPrecedes(prstate->pagefrz.FreezePageConflictXid, + prstate->cutoffs->OldestXmin)); } else if (prstate->nfrozen > 0) { @@ -952,18 +938,18 @@ heap_page_prune_and_freeze(PruneFreezeParams *params, /* * The snapshotConflictHorizon for the whole record should be the * most conservative of all the horizons calculated for any of the - * possible modifications. If this record will prune tuples, any - * transactions on the standby older than the youngest xmax of the - * most recently removed tuple this record will prune will - * conflict. If this record will freeze tuples, any transactions - * on the standby with xids older than the youngest tuple this - * record will freeze will conflict. + * possible modifications. If this record will prune tuples, any + * queries on the standby older than the newest xid of the most + * recently removed tuple this record will prune will conflict. If + * this record will freeze tuples, any queries on the standby with + * xids older than the newest tuple this record will freeze will + * conflict. */ TransactionId conflict_xid; - if (TransactionIdFollows(prstate.frz_conflict_horizon, + if (TransactionIdFollows(prstate.pagefrz.FreezePageConflictXid, prstate.latest_xid_removed)) - conflict_xid = prstate.frz_conflict_horizon; + conflict_xid = prstate.pagefrz.FreezePageConflictXid; else conflict_xid = prstate.latest_xid_removed; diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 24a27cc043a..ad993c07311 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -208,6 +208,18 @@ typedef struct HeapPageFreeze TransactionId FreezePageRelfrozenXid; MultiXactId FreezePageRelminMxid; + /* + * Newest XID that this page's freeze actions will remove from tuple + * visibility metadata (currently xmin and/or xvac). It is used to derive + * the snapshot conflict horizon for a WAL record that freezes tuples. On + * a standby, we must not replay that change while any snapshot could + * still treat that XID as running. + * + * It's only used if we execute freeze plans for this page, so there is no + * corresponding "no freeze" tracker. + */ + TransactionId FreezePageConflictXid; + /* * "No freeze" NewRelfrozenXid/NewRelminMxid trackers. *