]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Use the newest to-be-frozen xid as the conflict horizon for freezing
authorMelanie Plageman <melanieplageman@gmail.com>
Tue, 10 Mar 2026 19:24:39 +0000 (15:24 -0400)
committerMelanie Plageman <melanieplageman@gmail.com>
Tue, 10 Mar 2026 19:24:39 +0000 (15:24 -0400)
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 <melanieplageman@gmail.com>
Reviewed-by: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAAKRu_bbaUV8OUjAfVa_iALgKnTSfB4gO3jnkfpcFgrxEpSGJQ%40mail.gmail.com

src/backend/access/heap/heapam.c
src/backend/access/heap/pruneheap.c
src/include/access/heapam.h

index 1ecc83308510c9e389dec64b1171197f0818bb95..8f1c11a93500de4bb545cb286d111d2280495857 100644 (file)
@@ -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;
 
index 65c9f393f41a9da59c2822ab667d13458e90a2a8..8748fa882e9829863929cf491de7a1ed92062c80 100644 (file)
@@ -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;
 
index 24a27cc043afa60d93cef213f86dba0ddd02afe0..ad993c07311c843029602c7f23a8649eb64c86b9 100644 (file)
@@ -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.
         *