From 52f1f787563b858ca00665346b49cf8579b77534 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 19 Nov 2022 16:37:53 -0800
Subject: [PATCH v9 1/5] Refactor how VACUUM passes around its XID cutoffs.

Use a dedicated struct for the XID/MXID cutoffs used by VACUUM, such as
FreezeLimit and OldestXmin.  This state is initialized in vacuum.c, and
then passed around (via const pointers) by code from vacuumlazy.c to
external freezing related routines like heap_prepare_freeze_tuple.

Also simplify some of the logic for dealing with frozen xmin in
heap_prepare_freeze_tuple: add dedicated "xmin_already_frozen" state to
clearly distinguish xmin XIDs that we're going to freeze from those that
were already frozen from before.  This makes its xmin handling code
symmetrical with its xmax handling code.  This is preparation for an
upcoming commit that adds page level freezing.

Also refactor the control flow within FreezeMultiXactId(), while adding
stricter sanity checks.  We now test OldestXmin directly (instead of
using FreezeLimit as an inexact proxy for OldestXmin).  This is further
preparation for the page level freezing work, which will make the
function's caller give over control of page level freezing when needed
(whenever heap_prepare_freeze_tuple encounters a tuple/page that happens
to contain one or more MultiXactIds).

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-WznS9TxXmz2_=SY+SyJyDFbiOftKofM9=aDo68BbXNBUMA@mail.gmail.com
---
 src/include/access/heapam.h            |  10 +-
 src/include/access/tableam.h           |   2 +-
 src/include/commands/vacuum.h          |  49 ++-
 src/backend/access/heap/heapam.c       | 466 ++++++++++++-------------
 src/backend/access/heap/vacuumlazy.c   | 197 +++++------
 src/backend/access/transam/multixact.c |   9 +-
 src/backend/commands/cluster.c         |  25 +-
 src/backend/commands/vacuum.c          | 120 +++----
 8 files changed, 424 insertions(+), 454 deletions(-)

diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 810baaf9d..53eb01176 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -38,6 +38,7 @@
 
 typedef struct BulkInsertStateData *BulkInsertState;
 struct TupleTableSlot;
+struct VacuumCutoffs;
 
 #define MaxLockTupleMode	LockTupleExclusive
 
@@ -178,8 +179,7 @@ extern TM_Result heap_lock_tuple(Relation relation, HeapTuple tuple,
 
 extern void heap_inplace_update(Relation relation, HeapTuple tuple);
 extern bool heap_prepare_freeze_tuple(HeapTupleHeader tuple,
-									  TransactionId relfrozenxid, TransactionId relminmxid,
-									  TransactionId cutoff_xid, TransactionId cutoff_multi,
+									  const struct VacuumCutoffs *cutoffs,
 									  HeapTupleFreeze *frz, bool *totally_frozen,
 									  TransactionId *relfrozenxid_out,
 									  MultiXactId *relminmxid_out);
@@ -188,9 +188,9 @@ extern void heap_freeze_execute_prepared(Relation rel, Buffer buffer,
 										 HeapTupleFreeze *tuples, int ntuples);
 extern bool heap_freeze_tuple(HeapTupleHeader tuple,
 							  TransactionId relfrozenxid, TransactionId relminmxid,
-							  TransactionId cutoff_xid, TransactionId cutoff_multi);
-extern bool heap_tuple_would_freeze(HeapTupleHeader tuple, TransactionId cutoff_xid,
-									MultiXactId cutoff_multi,
+							  TransactionId FreezeLimit, TransactionId MultiXactCutoff);
+extern bool heap_tuple_would_freeze(HeapTupleHeader tuple,
+									const struct VacuumCutoffs *cutoffs,
 									TransactionId *relfrozenxid_out,
 									MultiXactId *relminmxid_out);
 extern bool heap_tuple_needs_eventual_freeze(HeapTupleHeader tuple);
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 4d1ef405c..1320ee6db 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -1634,7 +1634,7 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
  *   in that index's order; if false and OldIndex is InvalidOid, no sorting is
  *   performed
  * - OldIndex - see use_sort
- * - OldestXmin - computed by vacuum_set_xid_limits(), even when
+ * - OldestXmin - computed by vacuum_get_cutoffs(), even when
  *   not needed for the relation's AM
  * - *xid_cutoff - ditto
  * - *multi_cutoff - ditto
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 4e4bc26a8..896d1b1ac 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -235,6 +235,45 @@ typedef struct VacuumParams
 	int			nworkers;
 } VacuumParams;
 
+/*
+ * VacuumCutoffs is immutable state that describes the cutoffs used by VACUUM.
+ * Established at the beginning of each VACUUM operation.
+ */
+struct VacuumCutoffs
+{
+	/*
+	 * Existing pg_class fields at start of VACUUM (used for sanity checks)
+	 */
+	TransactionId relfrozenxid;
+	MultiXactId relminmxid;
+
+	/*
+	 * OldestXmin is the Xid below which tuples deleted by any xact (that
+	 * committed) should be considered DEAD, not just RECENTLY_DEAD.
+	 *
+	 * OldestMxact is the Mxid below which MultiXacts are definitely not seen
+	 * as visible by any running transaction.
+	 *
+	 * OldestXmin and OldestMxact are also the most recent values that can
+	 * ever be passed to vac_update_relstats() as frozenxid and minmulti
+	 * arguments at the end of VACUUM.  These same values should be passed
+	 * when it turns out that VACUUM will leave no unfrozen XIDs/MXIDs behind
+	 * in the table.
+	 */
+	TransactionId OldestXmin;
+	MultiXactId OldestMxact;
+
+	/*
+	 * FreezeLimit is the Xid below which all Xids are definitely replaced by
+	 * FrozenTransactionId in heap pages that VACUUM can cleanup lock.
+	 *
+	 * MultiXactCutoff is the value below which all MultiXactIds are
+	 * definitely removed from Xmax in heap pages VACUUM can cleanup lock.
+	 */
+	TransactionId FreezeLimit;
+	MultiXactId MultiXactCutoff;
+};
+
 /*
  * VacDeadItems stores TIDs whose index tuples are deleted by index vacuuming.
  */
@@ -286,13 +325,9 @@ extern void vac_update_relstats(Relation relation,
 								bool *frozenxid_updated,
 								bool *minmulti_updated,
 								bool in_outer_xact);
-extern bool vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
-								  TransactionId *OldestXmin,
-								  MultiXactId *OldestMxact,
-								  TransactionId *FreezeLimit,
-								  MultiXactId *MultiXactCutoff);
-extern bool vacuum_xid_failsafe_check(TransactionId relfrozenxid,
-									  MultiXactId relminmxid);
+extern bool vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
+							   struct VacuumCutoffs *cutoffs);
+extern bool vacuum_xid_failsafe_check(const struct VacuumCutoffs *cutoffs);
 extern void vac_update_datfrozenxid(void);
 extern void vacuum_delay_point(void);
 extern bool vacuum_is_permitted_for_relation(Oid relid, Form_pg_class reltuple,
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 747db5037..6c0634b38 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -52,6 +52,7 @@
 #include "access/xloginsert.h"
 #include "access/xlogutils.h"
 #include "catalog/catalog.h"
+#include "commands/vacuum.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "port/atomics.h"
@@ -6125,12 +6126,10 @@ heap_inplace_update(Relation relation, HeapTuple tuple)
  */
 static TransactionId
 FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
-				  TransactionId relfrozenxid, TransactionId relminmxid,
-				  TransactionId cutoff_xid, MultiXactId cutoff_multi,
-				  uint16 *flags, TransactionId *mxid_oldest_xid_out)
+				  const struct VacuumCutoffs *cutoffs, uint16 *flags,
+				  TransactionId *mxid_oldest_xid_out)
 {
-	TransactionId xid = InvalidTransactionId;
-	int			i;
+	TransactionId newxmax = InvalidTransactionId;
 	MultiXactMember *members;
 	int			nmembers;
 	bool		need_replace;
@@ -6153,12 +6152,12 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
 		*flags |= FRM_INVALIDATE_XMAX;
 		return InvalidTransactionId;
 	}
-	else if (MultiXactIdPrecedes(multi, relminmxid))
+	else if (MultiXactIdPrecedes(multi, cutoffs->relminmxid))
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
 				 errmsg_internal("found multixact %u from before relminmxid %u",
-								 multi, relminmxid)));
-	else if (MultiXactIdPrecedes(multi, cutoff_multi))
+								 multi, cutoffs->relminmxid)));
+	else if (MultiXactIdPrecedes(multi, cutoffs->MultiXactCutoff))
 	{
 		/*
 		 * This old multi cannot possibly have members still running, but
@@ -6171,39 +6170,39 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
 					 errmsg_internal("multixact %u from before cutoff %u found to be still running",
-									 multi, cutoff_multi)));
+									 multi, cutoffs->MultiXactCutoff)));
 
 		if (HEAP_XMAX_IS_LOCKED_ONLY(t_infomask))
 		{
 			*flags |= FRM_INVALIDATE_XMAX;
-			xid = InvalidTransactionId;
+			newxmax = InvalidTransactionId;
 		}
 		else
 		{
-			/* replace multi by update xid */
-			xid = MultiXactIdGetUpdateXid(multi, t_infomask);
+			/* replace multi with single XID for its updater */
+			newxmax = MultiXactIdGetUpdateXid(multi, t_infomask);
 
 			/* wasn't only a lock, xid needs to be valid */
-			Assert(TransactionIdIsValid(xid));
+			Assert(TransactionIdIsValid(newxmax));
 
-			if (TransactionIdPrecedes(xid, relfrozenxid))
+			if (TransactionIdPrecedes(newxmax, cutoffs->relfrozenxid))
 				ereport(ERROR,
 						(errcode(ERRCODE_DATA_CORRUPTED),
 						 errmsg_internal("found update xid %u from before relfrozenxid %u",
-										 xid, relfrozenxid)));
+										 newxmax, cutoffs->relfrozenxid)));
 
 			/*
-			 * If the xid is older than the cutoff, it has to have aborted,
-			 * otherwise the tuple would have gotten pruned away.
+			 * If the new xmax xid is older than OldestXmin, it has to have
+			 * aborted, otherwise the tuple would have been pruned away
 			 */
-			if (TransactionIdPrecedes(xid, cutoff_xid))
+			if (TransactionIdPrecedes(newxmax, cutoffs->OldestXmin))
 			{
-				if (TransactionIdDidCommit(xid))
+				if (TransactionIdDidCommit(newxmax))
 					ereport(ERROR,
 							(errcode(ERRCODE_DATA_CORRUPTED),
-							 errmsg_internal("cannot freeze committed update xid %u", xid)));
+							 errmsg_internal("cannot freeze committed update xid %u", newxmax)));
 				*flags |= FRM_INVALIDATE_XMAX;
-				xid = InvalidTransactionId;
+				newxmax = InvalidTransactionId;
 			}
 			else
 			{
@@ -6215,17 +6214,14 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
 		 * Don't push back mxid_oldest_xid_out using FRM_RETURN_IS_XID Xid, or
 		 * when no Xids will remain
 		 */
-		return xid;
+		return newxmax;
 	}
 
 	/*
-	 * This multixact might have or might not have members still running, but
-	 * we know it's valid and is newer than the cutoff point for multis.
-	 * However, some member(s) of it may be below the cutoff for Xids, so we
+	 * Some member(s) of this Multi may be below FreezeLimit xid cutoff, so we
 	 * need to walk the whole members array to figure out what to do, if
 	 * anything.
 	 */
-
 	nmembers =
 		GetMultiXactIdMembers(multi, &members, false,
 							  HEAP_XMAX_IS_LOCKED_ONLY(t_infomask));
@@ -6236,12 +6232,15 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
 		return InvalidTransactionId;
 	}
 
-	/* is there anything older than the cutoff? */
 	need_replace = false;
 	temp_xid_out = *mxid_oldest_xid_out;	/* init for FRM_NOOP */
-	for (i = 0; i < nmembers; i++)
+	for (int i = 0; i < nmembers; i++)
 	{
-		if (TransactionIdPrecedes(members[i].xid, cutoff_xid))
+		TransactionId xid = members[i].xid;
+
+		Assert(!TransactionIdPrecedes(xid, cutoffs->relfrozenxid));
+
+		if (TransactionIdPrecedes(xid, cutoffs->FreezeLimit))
 		{
 			need_replace = true;
 			break;
@@ -6251,7 +6250,7 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
 	}
 
 	/*
-	 * In the simplest case, there is no member older than the cutoff; we can
+	 * In the simplest case, there is no member older than FreezeLimit; we can
 	 * keep the existing MultiXactId as-is, avoiding a more expensive second
 	 * pass over the multi
 	 */
@@ -6279,110 +6278,97 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
 	update_committed = false;
 	temp_xid_out = *mxid_oldest_xid_out;	/* init for FRM_RETURN_IS_MULTI */
 
-	for (i = 0; i < nmembers; i++)
+	/*
+	 * Determine whether to keep each member xid, or to ignore it instead
+	 */
+	for (int i = 0; i < nmembers; i++)
 	{
-		/*
-		 * Determine whether to keep this member or ignore it.
-		 */
-		if (ISUPDATE_from_mxstatus(members[i].status))
+		TransactionId xid = members[i].xid;
+		MultiXactStatus mstatus = members[i].status;
+
+		Assert(!TransactionIdPrecedes(xid, cutoffs->relfrozenxid));
+
+		if (!ISUPDATE_from_mxstatus(mstatus))
 		{
-			TransactionId txid = members[i].xid;
-
-			Assert(TransactionIdIsValid(txid));
-			if (TransactionIdPrecedes(txid, relfrozenxid))
-				ereport(ERROR,
-						(errcode(ERRCODE_DATA_CORRUPTED),
-						 errmsg_internal("found update xid %u from before relfrozenxid %u",
-										 txid, relfrozenxid)));
-
 			/*
-			 * It's an update; should we keep it?  If the transaction is known
-			 * aborted or crashed then it's okay to ignore it, otherwise not.
-			 * Note that an updater older than cutoff_xid cannot possibly be
-			 * committed, because HeapTupleSatisfiesVacuum would have returned
-			 * HEAPTUPLE_DEAD and we would not be trying to freeze the tuple.
-			 *
-			 * As with all tuple visibility routines, it's critical to test
-			 * TransactionIdIsInProgress before TransactionIdDidCommit,
-			 * because of race conditions explained in detail in
-			 * heapam_visibility.c.
+			 * Locker XID (not updater XID).  We only keep lockers that are
+			 * still running.
 			 */
-			if (TransactionIdIsCurrentTransactionId(txid) ||
-				TransactionIdIsInProgress(txid))
-			{
-				Assert(!TransactionIdIsValid(update_xid));
-				update_xid = txid;
-			}
-			else if (TransactionIdDidCommit(txid))
-			{
-				/*
-				 * The transaction committed, so we can tell caller to set
-				 * HEAP_XMAX_COMMITTED.  (We can only do this because we know
-				 * the transaction is not running.)
-				 */
-				Assert(!TransactionIdIsValid(update_xid));
-				update_committed = true;
-				update_xid = txid;
-			}
-			else
-			{
-				/*
-				 * Not in progress, not committed -- must be aborted or
-				 * crashed; we can ignore it.
-				 */
-			}
-
-			/*
-			 * Since the tuple wasn't totally removed when vacuum pruned, the
-			 * update Xid cannot possibly be older than the xid cutoff. The
-			 * presence of such a tuple would cause corruption, so be paranoid
-			 * and check.
-			 */
-			if (TransactionIdIsValid(update_xid) &&
-				TransactionIdPrecedes(update_xid, cutoff_xid))
-				ereport(ERROR,
-						(errcode(ERRCODE_DATA_CORRUPTED),
-						 errmsg_internal("found update xid %u from before xid cutoff %u",
-										 update_xid, cutoff_xid)));
-
-			/*
-			 * We determined that this is an Xid corresponding to an update
-			 * that must be retained -- add it to new members list for later.
-			 *
-			 * Also consider pushing back temp_xid_out, which is needed when
-			 * we later conclude that a new multi is required (i.e. when we go
-			 * on to set FRM_RETURN_IS_MULTI for our caller because we also
-			 * need to retain a locker that's still running).
-			 */
-			if (TransactionIdIsValid(update_xid))
+			if (TransactionIdIsCurrentTransactionId(xid) ||
+				TransactionIdIsInProgress(xid))
 			{
 				newmembers[nnewmembers++] = members[i];
-				if (TransactionIdPrecedes(members[i].xid, temp_xid_out))
-					temp_xid_out = members[i].xid;
+				has_lockers = true;
+
+				/*
+				 * Cannot possibly be older than VACUUM's OldestXmin, so we
+				 * don't need a NewRelfrozenXid step here
+				 */
+				Assert(TransactionIdPrecedesOrEquals(cutoffs->OldestXmin, xid));
 			}
+
+			continue;
+		}
+
+		/*
+		 * Updater XID (not locker XID).  Should we keep it?
+		 *
+		 * Since the tuple wasn't totally removed when vacuum pruned, the
+		 * update Xid cannot possibly be older than OldestXmin cutoff. The
+		 * presence of such a tuple would cause corruption, so be paranoid and
+		 * check.
+		 */
+		if (TransactionIdPrecedes(xid, cutoffs->OldestXmin))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg_internal("found update xid %u from before removable cutoff %u",
+									 xid, cutoffs->OldestXmin)));
+		if (TransactionIdIsValid(update_xid))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg_internal("multixact %u has two or more updating members",
+									 multi),
+					 errdetail_internal("First updater XID=%u second updater XID=%u.",
+										update_xid, xid)));
+
+		/*
+		 * If the transaction is known aborted or crashed then it's okay to
+		 * ignore it, otherwise not.
+		 *
+		 * As with all tuple visibility routines, it's critical to test
+		 * TransactionIdIsInProgress before TransactionIdDidCommit, because of
+		 * race conditions explained in detail in heapam_visibility.c.
+		 */
+		if (TransactionIdIsCurrentTransactionId(xid) ||
+			TransactionIdIsInProgress(xid))
+			update_xid = xid;
+		else if (TransactionIdDidCommit(xid))
+		{
+			/*
+			 * The transaction committed, so we can tell caller to set
+			 * HEAP_XMAX_COMMITTED.  (We can only do this because we know the
+			 * transaction is not running.)
+			 */
+			update_committed = true;
+			update_xid = xid;
 		}
 		else
 		{
-			/* We only keep lockers if they are still running */
-			if (TransactionIdIsCurrentTransactionId(members[i].xid) ||
-				TransactionIdIsInProgress(members[i].xid))
-			{
-				/*
-				 * Running locker cannot possibly be older than the cutoff.
-				 *
-				 * The cutoff is <= VACUUM's OldestXmin, which is also the
-				 * initial value used for top-level relfrozenxid_out tracking
-				 * state.  A running locker cannot be older than VACUUM's
-				 * OldestXmin, either, so we don't need a temp_xid_out step.
-				 */
-				Assert(TransactionIdIsNormal(members[i].xid));
-				Assert(!TransactionIdPrecedes(members[i].xid, cutoff_xid));
-				Assert(!TransactionIdPrecedes(members[i].xid,
-											  *mxid_oldest_xid_out));
-				newmembers[nnewmembers++] = members[i];
-				has_lockers = true;
-			}
+			/*
+			 * Not in progress, not committed -- must be aborted or crashed;
+			 * we can ignore it.
+			 */
+			continue;
 		}
+
+		/*
+		 * We determined that this is an Xid corresponding to an update that
+		 * must be retained -- add it to new members list for later.  Also
+		 * consider pushing back mxid_oldest_xid_out.
+		 */
+		newmembers[nnewmembers++] = members[i];
+		if (TransactionIdPrecedes(xid, temp_xid_out))
+			temp_xid_out = xid;
 	}
 
 	pfree(members);
@@ -6395,7 +6381,7 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
 	{
 		/* nothing worth keeping!? Tell caller to remove the whole thing */
 		*flags |= FRM_INVALIDATE_XMAX;
-		xid = InvalidTransactionId;
+		newxmax = InvalidTransactionId;
 		/* Don't push back mxid_oldest_xid_out -- no Xids will remain */
 	}
 	else if (TransactionIdIsValid(update_xid) && !has_lockers)
@@ -6411,7 +6397,7 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
 		*flags |= FRM_RETURN_IS_XID;
 		if (update_committed)
 			*flags |= FRM_MARK_COMMITTED;
-		xid = update_xid;
+		newxmax = update_xid;
 		/* Don't push back mxid_oldest_xid_out using FRM_RETURN_IS_XID Xid */
 	}
 	else
@@ -6421,26 +6407,29 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
 		 * one, to set as new Xmax in the tuple.  The oldest surviving member
 		 * might push back mxid_oldest_xid_out.
 		 */
-		xid = MultiXactIdCreateFromMembers(nnewmembers, newmembers);
+		newxmax = MultiXactIdCreateFromMembers(nnewmembers, newmembers);
 		*flags |= FRM_RETURN_IS_MULTI;
 		*mxid_oldest_xid_out = temp_xid_out;
 	}
 
 	pfree(newmembers);
 
-	return xid;
+	return newxmax;
 }
 
 /*
  * heap_prepare_freeze_tuple
  *
  * Check to see whether any of the XID fields of a tuple (xmin, xmax, xvac)
- * are older than the specified cutoff XID and cutoff MultiXactId.  If so,
+ * are older than the FreezeLimit and/or MultiXactCutoff freeze cutoffs.  If so,
  * setup enough state (in the *frz output argument) to later execute and
- * WAL-log what we would need to do, and return true.  Return false if nothing
- * is to be changed.  In addition, set *totally_frozen to true if the tuple
- * will be totally frozen after these operations are performed and false if
- * more freezing will eventually be required.
+ * WAL-log what caller needs to do for the tuple, and return true.  Return
+ * false if nothing can be changed about the tuple right now.
+ *
+ * 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
+ * XIDs or MultiXactIds that will need to be processed by a future VACUUM.
  *
  * VACUUM caller must assemble HeapTupleFreeze entries for every tuple that we
  * returned true for when called.  A later heap_freeze_execute_prepared call
@@ -6458,12 +6447,6 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
  * Each call here pushes back *relfrozenxid_out and/or *relminmxid_out as
  * needed to avoid unsafe final values in rel's authoritative pg_class tuple.
  *
- * NB: cutoff_xid *must* be <= VACUUM's OldestXmin, to ensure that any
- * XID older than it could neither be running nor seen as running by any
- * open transaction.  This ensures that the replacement will not change
- * anyone's idea of the tuple state.
- * Similarly, cutoff_multi must be <= VACUUM's OldestMxact.
- *
  * NB: This function has side effects: it might allocate a new MultiXactId.
  * It will be set as tuple's new xmax when our *frz output is processed within
  * heap_execute_freeze_tuple later on.  If the tuple is in a shared buffer
@@ -6471,16 +6454,17 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
  */
 bool
 heap_prepare_freeze_tuple(HeapTupleHeader tuple,
-						  TransactionId relfrozenxid, TransactionId relminmxid,
-						  TransactionId cutoff_xid, TransactionId cutoff_multi,
+						  const struct VacuumCutoffs *cutoffs,
 						  HeapTupleFreeze *frz, bool *totally_frozen,
 						  TransactionId *relfrozenxid_out,
 						  MultiXactId *relminmxid_out)
 {
-	bool		changed = false;
-	bool		xmax_already_frozen = false;
-	bool		xmin_frozen;
-	bool		freeze_xmax;
+	bool		xmin_already_frozen = false,
+				xmax_already_frozen = false;
+	bool		freeze_xmin = false,
+				replace_xvac = false,
+				replace_xmax = false,
+				freeze_xmax = false;
 	TransactionId xid;
 
 	frz->frzflags = 0;
@@ -6489,37 +6473,29 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 	frz->xmax = HeapTupleHeaderGetRawXmax(tuple);
 
 	/*
-	 * Process xmin.  xmin_frozen has two slightly different meanings: in the
-	 * !XidIsNormal case, it means "the xmin doesn't need any freezing" (it's
-	 * already a permanent value), while in the block below it is set true to
-	 * mean "xmin won't need freezing after what we do to it here" (false
-	 * otherwise).  In both cases we're allowed to set totally_frozen, as far
-	 * as xmin is concerned.  Both cases also don't require relfrozenxid_out
-	 * handling, since either way the tuple's xmin will be a permanent value
-	 * once we're done with it.
+	 * Process xmin, while keeping track of whether it's already frozen, or
+	 * will become frozen when our freeze plan is executed by caller (could be
+	 * neither).
 	 */
 	xid = HeapTupleHeaderGetXmin(tuple);
 	if (!TransactionIdIsNormal(xid))
-		xmin_frozen = true;
+		xmin_already_frozen = true;
 	else
 	{
-		if (TransactionIdPrecedes(xid, relfrozenxid))
+		if (TransactionIdPrecedes(xid, cutoffs->relfrozenxid))
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
 					 errmsg_internal("found xmin %u from before relfrozenxid %u",
-									 xid, relfrozenxid)));
+									 xid, cutoffs->relfrozenxid)));
 
-		xmin_frozen = TransactionIdPrecedes(xid, cutoff_xid);
-		if (xmin_frozen)
+		freeze_xmin = TransactionIdPrecedes(xid, cutoffs->FreezeLimit);
+		if (freeze_xmin)
 		{
 			if (!TransactionIdDidCommit(xid))
 				ereport(ERROR,
 						(errcode(ERRCODE_DATA_CORRUPTED),
 						 errmsg_internal("uncommitted xmin %u from before xid cutoff %u needs to be frozen",
-										 xid, cutoff_xid)));
-
-			frz->t_infomask |= HEAP_XMIN_FROZEN;
-			changed = true;
+										 xid, cutoffs->FreezeLimit)));
 		}
 		else
 		{
@@ -6529,17 +6505,31 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 		}
 	}
 
+	/*
+	 * Old-style VACUUM FULL is gone, but we have to process xvac for as long
+	 * as we support having MOVED_OFF/MOVED_IN tuples in the database
+	 */
+	xid = HeapTupleHeaderGetXvac(tuple);
+	if (TransactionIdIsNormal(xid))
+	{
+		Assert(TransactionIdPrecedesOrEquals(cutoffs->relfrozenxid, xid));
+		Assert(TransactionIdPrecedes(xid, cutoffs->OldestXmin));
+
+		/*
+		 * For Xvac, we always freeze proactively.  This allows totally_frozen
+		 * tracking to ignore xvac.
+		 */
+		replace_xvac = true;
+	}
+
 	/*
 	 * Process xmax.  To thoroughly examine the current Xmax value we need to
 	 * resolve a MultiXactId to its member Xids, in case some of them are
-	 * below the given cutoff for Xids.  In that case, those values might need
+	 * below the given FreezeLimit.  In that case, those values might need
 	 * freezing, too.  Also, if a multi needs freezing, we cannot simply take
 	 * it out --- if there's a live updater Xid, it needs to be kept.
-	 *
-	 * Make sure to keep heap_tuple_would_freeze in sync with this.
 	 */
 	xid = HeapTupleHeaderGetRawXmax(tuple);
-
 	if (tuple->t_infomask & HEAP_XMAX_IS_MULTI)
 	{
 		/* Raw xmax is a MultiXactId */
@@ -6547,13 +6537,9 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 		uint16		flags;
 		TransactionId mxid_oldest_xid_out = *relfrozenxid_out;
 
-		newxmax = FreezeMultiXactId(xid, tuple->t_infomask,
-									relfrozenxid, relminmxid,
-									cutoff_xid, cutoff_multi,
+		newxmax = FreezeMultiXactId(xid, tuple->t_infomask, cutoffs,
 									&flags, &mxid_oldest_xid_out);
 
-		freeze_xmax = (flags & FRM_INVALIDATE_XMAX);
-
 		if (flags & FRM_RETURN_IS_XID)
 		{
 			/*
@@ -6562,8 +6548,7 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 			 * Might have to ratchet back relfrozenxid_out here, though never
 			 * relminmxid_out.
 			 */
-			Assert(!freeze_xmax);
-			Assert(TransactionIdIsValid(newxmax));
+			Assert(!TransactionIdPrecedes(newxmax, cutoffs->OldestXmin));
 			if (TransactionIdPrecedes(newxmax, *relfrozenxid_out))
 				*relfrozenxid_out = newxmax;
 
@@ -6578,7 +6563,7 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 			frz->xmax = newxmax;
 			if (flags & FRM_MARK_COMMITTED)
 				frz->t_infomask |= HEAP_XMAX_COMMITTED;
-			changed = true;
+			replace_xmax = true;
 		}
 		else if (flags & FRM_RETURN_IS_MULTI)
 		{
@@ -6591,9 +6576,7 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 			 * Might have to ratchet back relfrozenxid_out here, though never
 			 * relminmxid_out.
 			 */
-			Assert(!freeze_xmax);
-			Assert(MultiXactIdIsValid(newxmax));
-			Assert(!MultiXactIdPrecedes(newxmax, *relminmxid_out));
+			Assert(!MultiXactIdPrecedes(newxmax, cutoffs->OldestMxact));
 			Assert(TransactionIdPrecedesOrEquals(mxid_oldest_xid_out,
 												 *relfrozenxid_out));
 			*relfrozenxid_out = mxid_oldest_xid_out;
@@ -6609,10 +6592,8 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 			GetMultiXactIdHintBits(newxmax, &newbits, &newbits2);
 			frz->t_infomask |= newbits;
 			frz->t_infomask2 |= newbits2;
-
 			frz->xmax = newxmax;
-
-			changed = true;
+			replace_xmax = true;
 		}
 		else if (flags & FRM_NOOP)
 		{
@@ -6621,7 +6602,6 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 			 * Might have to ratchet back relminmxid_out, relfrozenxid_out, or
 			 * both together.
 			 */
-			Assert(!freeze_xmax);
 			Assert(MultiXactIdIsValid(newxmax) && xid == newxmax);
 			Assert(TransactionIdPrecedesOrEquals(mxid_oldest_xid_out,
 												 *relfrozenxid_out));
@@ -6632,23 +6612,25 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 		else
 		{
 			/*
-			 * Keeping nothing (neither an Xid nor a MultiXactId) in xmax.
-			 * Won't have to ratchet back relminmxid_out or relfrozenxid_out.
+			 * Freeze plan for tuple "freezes xmax" in the strictest sense:
+			 * it'll leave nothing in xmax (neither an Xid nor a MultiXactId).
 			 */
-			Assert(freeze_xmax);
+			Assert(flags & FRM_INVALIDATE_XMAX);
+			Assert(MultiXactIdPrecedes(xid, cutoffs->OldestMxact));
 			Assert(!TransactionIdIsValid(newxmax));
+			freeze_xmax = true;
 		}
 	}
 	else if (TransactionIdIsNormal(xid))
 	{
 		/* Raw xmax is normal XID */
-		if (TransactionIdPrecedes(xid, relfrozenxid))
+		if (TransactionIdPrecedes(xid, cutoffs->relfrozenxid))
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
 					 errmsg_internal("found xmax %u from before relfrozenxid %u",
-									 xid, relfrozenxid)));
+									 xid, cutoffs->relfrozenxid)));
 
-		if (TransactionIdPrecedes(xid, cutoff_xid))
+		if (TransactionIdPrecedes(xid, cutoffs->FreezeLimit))
 		{
 			/*
 			 * If we freeze xmax, make absolutely sure that it's not an XID
@@ -6667,7 +6649,7 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 		}
 		else
 		{
-			freeze_xmax = false;
+			/* Might have to ratchet back relfrozenxid_out */
 			if (TransactionIdPrecedes(xid, *relfrozenxid_out))
 				*relfrozenxid_out = xid;
 		}
@@ -6676,19 +6658,41 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 	{
 		/* Raw xmax is InvalidTransactionId XID */
 		Assert((tuple->t_infomask & HEAP_XMAX_IS_MULTI) == 0);
-		freeze_xmax = false;
 		xmax_already_frozen = true;
-		/* No need for relfrozenxid_out handling for already-frozen xmax */
 	}
 	else
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("found xmax %u (infomask 0x%04x) not frozen, not multi, not normal",
+				 errmsg_internal("found raw xmax %u (infomask 0x%04x) not invalid and not multi",
 								 xid, tuple->t_infomask)));
 
+	if (freeze_xmin)
+	{
+		Assert(!xmin_already_frozen);
+
+		frz->t_infomask |= HEAP_XMIN_FROZEN;
+	}
+	if (replace_xvac)
+	{
+		/*
+		 * If a MOVED_OFF tuple is not dead, the xvac transaction must have
+		 * failed; whereas a non-dead MOVED_IN tuple must mean the xvac
+		 * transaction succeeded.
+		 */
+		if (tuple->t_infomask & HEAP_MOVED_OFF)
+			frz->frzflags |= XLH_INVALID_XVAC;
+		else
+			frz->frzflags |= XLH_FREEZE_XVAC;
+	}
+	if (replace_xmax)
+	{
+		Assert(!xmax_already_frozen && !freeze_xmax);
+
+		/* Already set t_infomask/t_infomask2 flags in freeze plan */
+	}
 	if (freeze_xmax)
 	{
-		Assert(!xmax_already_frozen);
+		Assert(!xmax_already_frozen && !replace_xmax);
 
 		frz->xmax = InvalidTransactionId;
 
@@ -6701,52 +6705,20 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 		frz->t_infomask |= HEAP_XMAX_INVALID;
 		frz->t_infomask2 &= ~HEAP_HOT_UPDATED;
 		frz->t_infomask2 &= ~HEAP_KEYS_UPDATED;
-		changed = true;
 	}
 
 	/*
-	 * Old-style VACUUM FULL is gone, but we have to keep this code as long as
-	 * we support having MOVED_OFF/MOVED_IN tuples in the database.
+	 * Determine if this tuple is already totally frozen, or will become
+	 * totally frozen
 	 */
-	if (tuple->t_infomask & HEAP_MOVED)
-	{
-		xid = HeapTupleHeaderGetXvac(tuple);
-
-		/*
-		 * For Xvac, we ignore the cutoff_xid and just always perform the
-		 * freeze operation.  The oldest release in which such a value can
-		 * actually be set is PostgreSQL 8.4, because old-style VACUUM FULL
-		 * was removed in PostgreSQL 9.0.  Note that if we were to respect
-		 * cutoff_xid here, we'd need to make surely to clear totally_frozen
-		 * when we skipped freezing on that basis.
-		 *
-		 * No need for relfrozenxid_out handling, since we always freeze xvac.
-		 */
-		if (TransactionIdIsNormal(xid))
-		{
-			/*
-			 * If a MOVED_OFF tuple is not dead, the xvac transaction must
-			 * have failed; whereas a non-dead MOVED_IN tuple must mean the
-			 * xvac transaction succeeded.
-			 */
-			if (tuple->t_infomask & HEAP_MOVED_OFF)
-				frz->frzflags |= XLH_INVALID_XVAC;
-			else
-				frz->frzflags |= XLH_FREEZE_XVAC;
-
-			/*
-			 * Might as well fix the hint bits too; usually XMIN_COMMITTED
-			 * will already be set here, but there's a small chance not.
-			 */
-			Assert(!(tuple->t_infomask & HEAP_XMIN_INVALID));
-			frz->t_infomask |= HEAP_XMIN_COMMITTED;
-			changed = true;
-		}
-	}
-
-	*totally_frozen = (xmin_frozen &&
+	*totally_frozen = ((freeze_xmin || xmin_already_frozen) &&
 					   (freeze_xmax || xmax_already_frozen));
-	return changed;
+
+	/* A "totally_frozen" tuple must not leave anything behind in xmax */
+	Assert(!*totally_frozen || !replace_xmax);
+
+	/* Tell caller if this tuple has a usable freeze plan set in *frz */
+	return freeze_xmin || replace_xvac || replace_xmax || freeze_xmax;
 }
 
 /*
@@ -6865,19 +6837,25 @@ heap_freeze_execute_prepared(Relation rel, Buffer buffer,
 bool
 heap_freeze_tuple(HeapTupleHeader tuple,
 				  TransactionId relfrozenxid, TransactionId relminmxid,
-				  TransactionId cutoff_xid, TransactionId cutoff_multi)
+				  TransactionId FreezeLimit, TransactionId MultiXactCutoff)
 {
 	HeapTupleFreeze frz;
 	bool		do_freeze;
-	bool		tuple_totally_frozen;
-	TransactionId relfrozenxid_out = cutoff_xid;
-	MultiXactId relminmxid_out = cutoff_multi;
+	bool		totally_frozen;
+	struct VacuumCutoffs cutoffs;
+	TransactionId NewRelfrozenXid = FreezeLimit;
+	MultiXactId NewRelminMxid = MultiXactCutoff;
 
-	do_freeze = heap_prepare_freeze_tuple(tuple,
-										  relfrozenxid, relminmxid,
-										  cutoff_xid, cutoff_multi,
-										  &frz, &tuple_totally_frozen,
-										  &relfrozenxid_out, &relminmxid_out);
+	cutoffs.relfrozenxid = relfrozenxid;
+	cutoffs.relminmxid = relminmxid;
+	cutoffs.OldestXmin = FreezeLimit;
+	cutoffs.OldestMxact = MultiXactCutoff;
+	cutoffs.FreezeLimit = FreezeLimit;
+	cutoffs.MultiXactCutoff = MultiXactCutoff;
+
+	do_freeze = heap_prepare_freeze_tuple(tuple, &cutoffs,
+										  &frz, &totally_frozen,
+										  &NewRelfrozenXid, &NewRelminMxid);
 
 	/*
 	 * Note that because this is not a WAL-logged operation, we don't need to
@@ -7312,11 +7290,13 @@ heap_tuple_needs_eventual_freeze(HeapTupleHeader tuple)
  * never freeze here, which makes tracking the oldest extant XID/MXID simple.
  */
 bool
-heap_tuple_would_freeze(HeapTupleHeader tuple, TransactionId cutoff_xid,
-						MultiXactId cutoff_multi,
+heap_tuple_would_freeze(HeapTupleHeader tuple,
+						const struct VacuumCutoffs *cutoffs,
 						TransactionId *relfrozenxid_out,
 						MultiXactId *relminmxid_out)
 {
+	TransactionId cutoff_xid = cutoffs->FreezeLimit;
+	MultiXactId cutoff_multi = cutoffs->MultiXactCutoff;
 	TransactionId xid;
 	MultiXactId multi;
 	bool		would_freeze = false;
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index d59711b7e..b234072e8 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -144,6 +144,10 @@ typedef struct LVRelState
 	Relation   *indrels;
 	int			nindexes;
 
+	/* Buffer access strategy and parallel vacuum state */
+	BufferAccessStrategy bstrategy;
+	ParallelVacuumState *pvs;
+
 	/* Aggressive VACUUM? (must set relfrozenxid >= FreezeLimit) */
 	bool		aggressive;
 	/* Use visibility map to skip? (disabled by DISABLE_PAGE_SKIPPING) */
@@ -158,21 +162,9 @@ typedef struct LVRelState
 	bool		do_index_cleanup;
 	bool		do_rel_truncate;
 
-	/* Buffer access strategy and parallel vacuum state */
-	BufferAccessStrategy bstrategy;
-	ParallelVacuumState *pvs;
-
-	/* rel's initial relfrozenxid and relminmxid */
-	TransactionId relfrozenxid;
-	MultiXactId relminmxid;
-	double		old_live_tuples;	/* previous value of pg_class.reltuples */
-
 	/* VACUUM operation's cutoffs for freezing and pruning */
-	TransactionId OldestXmin;
+	struct VacuumCutoffs cutoffs;
 	GlobalVisState *vistest;
-	/* VACUUM operation's target cutoffs for freezing XIDs and MultiXactIds */
-	TransactionId FreezeLimit;
-	MultiXactId MultiXactCutoff;
 	/* Tracks oldest extant XID/MXID for setting relfrozenxid/relminmxid */
 	TransactionId NewRelfrozenXid;
 	MultiXactId NewRelminMxid;
@@ -314,14 +306,9 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	LVRelState *vacrel;
 	bool		verbose,
 				instrument,
-				aggressive,
 				skipwithvm,
 				frozenxid_updated,
 				minmulti_updated;
-	TransactionId OldestXmin,
-				FreezeLimit;
-	MultiXactId OldestMxact,
-				MultiXactCutoff;
 	BlockNumber orig_rel_pages,
 				new_rel_pages,
 				new_rel_allvisible;
@@ -353,27 +340,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
 								  RelationGetRelid(rel));
 
-	/*
-	 * Get OldestXmin cutoff, which is used to determine which deleted tuples
-	 * are considered DEAD, not just RECENTLY_DEAD.  Also get related cutoffs
-	 * used to determine which XIDs/MultiXactIds will be frozen.  If this is
-	 * an aggressive VACUUM then lazy_scan_heap cannot leave behind unfrozen
-	 * XIDs < FreezeLimit (all MXIDs < MultiXactCutoff also need to go away).
-	 */
-	aggressive = vacuum_set_xid_limits(rel, params, &OldestXmin, &OldestMxact,
-									   &FreezeLimit, &MultiXactCutoff);
-
-	skipwithvm = true;
-	if (params->options & VACOPT_DISABLE_PAGE_SKIPPING)
-	{
-		/*
-		 * Force aggressive mode, and disable skipping blocks using the
-		 * visibility map (even those set all-frozen)
-		 */
-		aggressive = true;
-		skipwithvm = false;
-	}
-
 	/*
 	 * Setup error traceback support for ereport() first.  The idea is to set
 	 * up an error context callback to display additional information on any
@@ -396,25 +362,12 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	errcallback.arg = vacrel;
 	errcallback.previous = error_context_stack;
 	error_context_stack = &errcallback;
-	if (verbose)
-	{
-		Assert(!IsAutoVacuumWorkerProcess());
-		if (aggressive)
-			ereport(INFO,
-					(errmsg("aggressively vacuuming \"%s.%s.%s\"",
-							get_database_name(MyDatabaseId),
-							vacrel->relnamespace, vacrel->relname)));
-		else
-			ereport(INFO,
-					(errmsg("vacuuming \"%s.%s.%s\"",
-							get_database_name(MyDatabaseId),
-							vacrel->relnamespace, vacrel->relname)));
-	}
 
 	/* Set up high level stuff about rel and its indexes */
 	vacrel->rel = rel;
 	vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
 					 &vacrel->indrels);
+	vacrel->bstrategy = bstrategy;
 	if (instrument && vacrel->nindexes > 0)
 	{
 		/* Copy index names used by instrumentation (not error reporting) */
@@ -435,8 +388,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	Assert(params->index_cleanup != VACOPTVALUE_UNSPECIFIED);
 	Assert(params->truncate != VACOPTVALUE_UNSPECIFIED &&
 		   params->truncate != VACOPTVALUE_AUTO);
-	vacrel->aggressive = aggressive;
-	vacrel->skipwithvm = skipwithvm;
 	vacrel->failsafe_active = false;
 	vacrel->consider_bypass_optimization = true;
 	vacrel->do_index_vacuuming = true;
@@ -459,11 +410,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 		Assert(params->index_cleanup == VACOPTVALUE_AUTO);
 	}
 
-	vacrel->bstrategy = bstrategy;
-	vacrel->relfrozenxid = rel->rd_rel->relfrozenxid;
-	vacrel->relminmxid = rel->rd_rel->relminmxid;
-	vacrel->old_live_tuples = rel->rd_rel->reltuples;
-
 	/* Initialize page counters explicitly (be tidy) */
 	vacrel->scanned_pages = 0;
 	vacrel->removed_pages = 0;
@@ -489,32 +435,53 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	vacrel->missed_dead_tuples = 0;
 
 	/*
-	 * Determine the extent of the blocks that we'll scan in lazy_scan_heap,
-	 * and finalize cutoffs used for freezing and pruning in lazy_scan_prune.
+	 * Get cutoffs that determine which deleted tuples are considered DEAD,
+	 * not just RECENTLY_DEAD, and which XIDs/MXIDs to freeze.  Then determine
+	 * the extent of the blocks that we'll scan in lazy_scan_heap.  It has to
+	 * happen in this order to ensure that the OldestXmin cutoff field works
+	 * as an upper bound on the XIDs stored in the pages we'll actually scan
+	 * (NewRelfrozenXid tracking must never be allowed to miss unfrozen XIDs).
 	 *
+	 * Next acquire vistest, a related cutoff that's used in heap_page_prune.
 	 * We expect vistest will always make heap_page_prune remove any deleted
 	 * tuple whose xmax is < OldestXmin.  lazy_scan_prune must never become
 	 * confused about whether a tuple should be frozen or removed.  (In the
 	 * future we might want to teach lazy_scan_prune to recompute vistest from
 	 * time to time, to increase the number of dead tuples it can prune away.)
-	 *
-	 * We must determine rel_pages _after_ OldestXmin has been established.
-	 * lazy_scan_heap's physical heap scan (scan of pages < rel_pages) is
-	 * thereby guaranteed to not miss any tuples with XIDs < OldestXmin. These
-	 * XIDs must at least be considered for freezing (though not necessarily
-	 * frozen) during its scan.
 	 */
+	vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs);
 	vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel);
-	vacrel->OldestXmin = OldestXmin;
 	vacrel->vistest = GlobalVisTestFor(rel);
-	/* FreezeLimit controls XID freezing (always <= OldestXmin) */
-	vacrel->FreezeLimit = FreezeLimit;
-	/* MultiXactCutoff controls MXID freezing (always <= OldestMxact) */
-	vacrel->MultiXactCutoff = MultiXactCutoff;
 	/* Initialize state used to track oldest extant XID/MXID */
-	vacrel->NewRelfrozenXid = OldestXmin;
-	vacrel->NewRelminMxid = OldestMxact;
+	vacrel->NewRelfrozenXid = vacrel->cutoffs.OldestXmin;
+	vacrel->NewRelminMxid = vacrel->cutoffs.OldestMxact;
 	vacrel->skippedallvis = false;
+	skipwithvm = true;
+	if (params->options & VACOPT_DISABLE_PAGE_SKIPPING)
+	{
+		/*
+		 * Force aggressive mode, and disable skipping blocks using the
+		 * visibility map (even those set all-frozen)
+		 */
+		vacrel->aggressive = true;
+		skipwithvm = false;
+	}
+
+	vacrel->skipwithvm = skipwithvm;
+
+	if (verbose)
+	{
+		if (vacrel->aggressive)
+			ereport(INFO,
+					(errmsg("aggressively vacuuming \"%s.%s.%s\"",
+							get_database_name(MyDatabaseId),
+							vacrel->relnamespace, vacrel->relname)));
+		else
+			ereport(INFO,
+					(errmsg("vacuuming \"%s.%s.%s\"",
+							get_database_name(MyDatabaseId),
+							vacrel->relnamespace, vacrel->relname)));
+	}
 
 	/*
 	 * Allocate dead_items array memory using dead_items_alloc.  This handles
@@ -569,13 +536,13 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 * value >= FreezeLimit, and relminmxid to a value >= MultiXactCutoff.
 	 * Non-aggressive VACUUMs may advance them by any amount, or not at all.
 	 */
-	Assert(vacrel->NewRelfrozenXid == OldestXmin ||
-		   TransactionIdPrecedesOrEquals(aggressive ? FreezeLimit :
-										 vacrel->relfrozenxid,
+	Assert(vacrel->NewRelfrozenXid == vacrel->cutoffs.OldestXmin ||
+		   TransactionIdPrecedesOrEquals(vacrel->aggressive ? vacrel->cutoffs.FreezeLimit :
+										 vacrel->cutoffs.relfrozenxid,
 										 vacrel->NewRelfrozenXid));
-	Assert(vacrel->NewRelminMxid == OldestMxact ||
-		   MultiXactIdPrecedesOrEquals(aggressive ? MultiXactCutoff :
-									   vacrel->relminmxid,
+	Assert(vacrel->NewRelminMxid == vacrel->cutoffs.OldestMxact ||
+		   MultiXactIdPrecedesOrEquals(vacrel->aggressive ? vacrel->cutoffs.MultiXactCutoff :
+									   vacrel->cutoffs.relminmxid,
 									   vacrel->NewRelminMxid));
 	if (vacrel->skippedallvis)
 	{
@@ -584,7 +551,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 		 * chose to skip an all-visible page range.  The state that tracks new
 		 * values will have missed unfrozen XIDs from the pages we skipped.
 		 */
-		Assert(!aggressive);
+		Assert(!vacrel->aggressive);
 		vacrel->NewRelfrozenXid = InvalidTransactionId;
 		vacrel->NewRelminMxid = InvalidMultiXactId;
 	}
@@ -669,14 +636,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 				 * implies aggressive.  Produce distinct output for the corner
 				 * case all the same, just in case.
 				 */
-				if (aggressive)
+				if (vacrel->aggressive)
 					msgfmt = _("automatic aggressive vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n");
 				else
 					msgfmt = _("automatic vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n");
 			}
 			else
 			{
-				if (aggressive)
+				if (vacrel->aggressive)
 					msgfmt = _("automatic aggressive vacuum of table \"%s.%s.%s\": index scans: %d\n");
 				else
 					msgfmt = _("automatic vacuum of table \"%s.%s.%s\": index scans: %d\n");
@@ -702,20 +669,23 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 								 _("tuples missed: %lld dead from %u pages not removed due to cleanup lock contention\n"),
 								 (long long) vacrel->missed_dead_tuples,
 								 vacrel->missed_dead_pages);
-			diff = (int32) (ReadNextTransactionId() - OldestXmin);
+			diff = (int32) (ReadNextTransactionId() -
+							vacrel->cutoffs.OldestXmin);
 			appendStringInfo(&buf,
 							 _("removable cutoff: %u, which was %d XIDs old when operation ended\n"),
-							 OldestXmin, diff);
+							 vacrel->cutoffs.OldestXmin, diff);
 			if (frozenxid_updated)
 			{
-				diff = (int32) (vacrel->NewRelfrozenXid - vacrel->relfrozenxid);
+				diff = (int32) (vacrel->NewRelfrozenXid -
+								vacrel->cutoffs.relfrozenxid);
 				appendStringInfo(&buf,
 								 _("new relfrozenxid: %u, which is %d XIDs ahead of previous value\n"),
 								 vacrel->NewRelfrozenXid, diff);
 			}
 			if (minmulti_updated)
 			{
-				diff = (int32) (vacrel->NewRelminMxid - vacrel->relminmxid);
+				diff = (int32) (vacrel->NewRelminMxid -
+								vacrel->cutoffs.relminmxid);
 				appendStringInfo(&buf,
 								 _("new relminmxid: %u, which is %d MXIDs ahead of previous value\n"),
 								 vacrel->NewRelminMxid, diff);
@@ -1610,7 +1580,7 @@ retry:
 		 offnum <= maxoff;
 		 offnum = OffsetNumberNext(offnum))
 	{
-		bool		tuple_totally_frozen;
+		bool		totally_frozen;
 
 		/*
 		 * Set the offset number so that we can display it along with any
@@ -1666,7 +1636,8 @@ retry:
 		 * since heap_page_prune() looked.  Handle that here by restarting.
 		 * (See comments at the top of function for a full explanation.)
 		 */
-		res = HeapTupleSatisfiesVacuum(&tuple, vacrel->OldestXmin, buf);
+		res = HeapTupleSatisfiesVacuum(&tuple, vacrel->cutoffs.OldestXmin,
+									   buf);
 
 		if (unlikely(res == HEAPTUPLE_DEAD))
 			goto retry;
@@ -1723,7 +1694,8 @@ retry:
 					 * that everyone sees it as committed?
 					 */
 					xmin = HeapTupleHeaderGetXmin(tuple.t_data);
-					if (!TransactionIdPrecedes(xmin, vacrel->OldestXmin))
+					if (!TransactionIdPrecedes(xmin,
+											   vacrel->cutoffs.OldestXmin))
 					{
 						prunestate->all_visible = false;
 						break;
@@ -1774,13 +1746,8 @@ retry:
 		prunestate->hastup = true;	/* page makes rel truncation unsafe */
 
 		/* Tuple with storage -- consider need to freeze */
-		if (heap_prepare_freeze_tuple(tuple.t_data,
-									  vacrel->relfrozenxid,
-									  vacrel->relminmxid,
-									  vacrel->FreezeLimit,
-									  vacrel->MultiXactCutoff,
-									  &frozen[tuples_frozen],
-									  &tuple_totally_frozen,
+		if (heap_prepare_freeze_tuple(tuple.t_data, &vacrel->cutoffs,
+									  &frozen[tuples_frozen], &totally_frozen,
 									  &NewRelfrozenXid, &NewRelminMxid))
 		{
 			/* Save prepared freeze plan for later */
@@ -1791,7 +1758,7 @@ retry:
 		 * If tuple is not frozen (and not about to become frozen) then caller
 		 * had better not go on to set this page's VM bit
 		 */
-		if (!tuple_totally_frozen)
+		if (!totally_frozen)
 			prunestate->all_frozen = false;
 	}
 
@@ -1817,7 +1784,8 @@ retry:
 		vacrel->frozen_pages++;
 
 		/* Execute all freeze plans for page as a single atomic action */
-		heap_freeze_execute_prepared(vacrel->rel, buf, vacrel->FreezeLimit,
+		heap_freeze_execute_prepared(vacrel->rel, buf,
+									 vacrel->cutoffs.FreezeLimit,
 									 frozen, tuples_frozen);
 	}
 
@@ -1972,9 +1940,7 @@ lazy_scan_noprune(LVRelState *vacrel,
 
 		*hastup = true;			/* page prevents rel truncation */
 		tupleheader = (HeapTupleHeader) PageGetItem(page, itemid);
-		if (heap_tuple_would_freeze(tupleheader,
-									vacrel->FreezeLimit,
-									vacrel->MultiXactCutoff,
+		if (heap_tuple_would_freeze(tupleheader, &vacrel->cutoffs,
 									&NewRelfrozenXid, &NewRelminMxid))
 		{
 			/* Tuple with XID < FreezeLimit (or MXID < MultiXactCutoff) */
@@ -2010,7 +1976,8 @@ lazy_scan_noprune(LVRelState *vacrel,
 		tuple.t_len = ItemIdGetLength(itemid);
 		tuple.t_tableOid = RelationGetRelid(vacrel->rel);
 
-		switch (HeapTupleSatisfiesVacuum(&tuple, vacrel->OldestXmin, buf))
+		switch (HeapTupleSatisfiesVacuum(&tuple, vacrel->cutoffs.OldestXmin,
+										 buf))
 		{
 			case HEAPTUPLE_DELETE_IN_PROGRESS:
 			case HEAPTUPLE_LIVE:
@@ -2274,6 +2241,7 @@ static bool
 lazy_vacuum_all_indexes(LVRelState *vacrel)
 {
 	bool		allindexes = true;
+	double		old_live_tuples = vacrel->rel->rd_rel->reltuples;
 
 	Assert(vacrel->nindexes > 0);
 	Assert(vacrel->do_index_vacuuming);
@@ -2297,9 +2265,9 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 			Relation	indrel = vacrel->indrels[idx];
 			IndexBulkDeleteResult *istat = vacrel->indstats[idx];
 
-			vacrel->indstats[idx] =
-				lazy_vacuum_one_index(indrel, istat, vacrel->old_live_tuples,
-									  vacrel);
+			vacrel->indstats[idx] = lazy_vacuum_one_index(indrel, istat,
+														  old_live_tuples,
+														  vacrel);
 
 			if (lazy_check_wraparound_failsafe(vacrel))
 			{
@@ -2312,7 +2280,7 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 	else
 	{
 		/* Outsource everything to parallel variant */
-		parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, vacrel->old_live_tuples,
+		parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples,
 											vacrel->num_index_scans);
 
 		/*
@@ -2581,15 +2549,14 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer,
 static bool
 lazy_check_wraparound_failsafe(LVRelState *vacrel)
 {
-	Assert(TransactionIdIsNormal(vacrel->relfrozenxid));
-	Assert(MultiXactIdIsValid(vacrel->relminmxid));
+	Assert(TransactionIdIsNormal(vacrel->cutoffs.relfrozenxid));
+	Assert(MultiXactIdIsValid(vacrel->cutoffs.relminmxid));
 
 	/* Don't warn more than once per VACUUM */
 	if (vacrel->failsafe_active)
 		return true;
 
-	if (unlikely(vacuum_xid_failsafe_check(vacrel->relfrozenxid,
-										   vacrel->relminmxid)))
+	if (unlikely(vacuum_xid_failsafe_check(&vacrel->cutoffs)))
 	{
 		vacrel->failsafe_active = true;
 
@@ -3246,7 +3213,8 @@ heap_page_is_all_visible(LVRelState *vacrel, Buffer buf,
 		tuple.t_len = ItemIdGetLength(itemid);
 		tuple.t_tableOid = RelationGetRelid(vacrel->rel);
 
-		switch (HeapTupleSatisfiesVacuum(&tuple, vacrel->OldestXmin, buf))
+		switch (HeapTupleSatisfiesVacuum(&tuple, vacrel->cutoffs.OldestXmin,
+										 buf))
 		{
 			case HEAPTUPLE_LIVE:
 				{
@@ -3265,7 +3233,8 @@ heap_page_is_all_visible(LVRelState *vacrel, Buffer buf,
 					 * that everyone sees it as committed?
 					 */
 					xmin = HeapTupleHeaderGetXmin(tuple.t_data);
-					if (!TransactionIdPrecedes(xmin, vacrel->OldestXmin))
+					if (!TransactionIdPrecedes(xmin,
+											   vacrel->cutoffs.OldestXmin))
 					{
 						all_visible = false;
 						*all_frozen = false;
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index e1191a756..28514a1c5 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -2813,14 +2813,11 @@ ReadMultiXactCounts(uint32 *multixacts, MultiXactOffset *members)
  * As the fraction of the member space currently in use grows, we become
  * more aggressive in clamping this value.  That not only causes autovacuum
  * to ramp up, but also makes any manual vacuums the user issues more
- * aggressive.  This happens because vacuum_set_xid_limits() clamps the
- * freeze table and the minimum freeze age based on the effective
+ * aggressive.  This happens because vacuum_get_cutoffs() will clamp the
+ * freeze table and the minimum freeze age cutoffs based on the effective
  * autovacuum_multixact_freeze_max_age this function returns.  In the worst
  * case, we'll claim the freeze_max_age to zero, and every vacuum of any
- * table will try to freeze every multixact.
- *
- * It's possible that these thresholds should be user-tunable, but for now
- * we keep it simple.
+ * table will freeze every multixact.
  */
 int
 MultiXactMemberFreezeThreshold(void)
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 07e091bb8..b0e310604 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -824,10 +824,7 @@ copy_table_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex, bool verbose,
 	TupleDesc	oldTupDesc PG_USED_FOR_ASSERTS_ONLY;
 	TupleDesc	newTupDesc PG_USED_FOR_ASSERTS_ONLY;
 	VacuumParams params;
-	TransactionId OldestXmin,
-				FreezeXid;
-	MultiXactId OldestMxact,
-				MultiXactCutoff;
+	struct VacuumCutoffs cutoffs;
 	bool		use_sort;
 	double		num_tuples = 0,
 				tups_vacuumed = 0,
@@ -916,23 +913,24 @@ copy_table_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex, bool verbose,
 	 * not to be aggressive about this.
 	 */
 	memset(&params, 0, sizeof(VacuumParams));
-	vacuum_set_xid_limits(OldHeap, &params, &OldestXmin, &OldestMxact,
-						  &FreezeXid, &MultiXactCutoff);
+	vacuum_get_cutoffs(OldHeap, &params, &cutoffs);
 
 	/*
 	 * FreezeXid will become the table's new relfrozenxid, and that mustn't go
 	 * backwards, so take the max.
 	 */
 	if (TransactionIdIsValid(OldHeap->rd_rel->relfrozenxid) &&
-		TransactionIdPrecedes(FreezeXid, OldHeap->rd_rel->relfrozenxid))
-		FreezeXid = OldHeap->rd_rel->relfrozenxid;
+		TransactionIdPrecedes(cutoffs.FreezeLimit,
+							  OldHeap->rd_rel->relfrozenxid))
+		cutoffs.FreezeLimit = OldHeap->rd_rel->relfrozenxid;
 
 	/*
 	 * MultiXactCutoff, similarly, shouldn't go backwards either.
 	 */
 	if (MultiXactIdIsValid(OldHeap->rd_rel->relminmxid) &&
-		MultiXactIdPrecedes(MultiXactCutoff, OldHeap->rd_rel->relminmxid))
-		MultiXactCutoff = OldHeap->rd_rel->relminmxid;
+		MultiXactIdPrecedes(cutoffs.MultiXactCutoff,
+							OldHeap->rd_rel->relminmxid))
+		cutoffs.MultiXactCutoff = OldHeap->rd_rel->relminmxid;
 
 	/*
 	 * Decide whether to use an indexscan or seqscan-and-optional-sort to scan
@@ -971,13 +969,14 @@ copy_table_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex, bool verbose,
 	 * values (e.g. because the AM doesn't use freezing).
 	 */
 	table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
-									OldestXmin, &FreezeXid, &MultiXactCutoff,
+									cutoffs.OldestXmin, &cutoffs.FreezeLimit,
+									&cutoffs.MultiXactCutoff,
 									&num_tuples, &tups_vacuumed,
 									&tups_recently_dead);
 
 	/* return selected values to caller, get set as relfrozenxid/minmxid */
-	*pFreezeXid = FreezeXid;
-	*pCutoffMulti = MultiXactCutoff;
+	*pFreezeXid = cutoffs.FreezeLimit;
+	*pCutoffMulti = cutoffs.MultiXactCutoff;
 
 	/* Reset rd_toastoid just to be tidy --- it shouldn't be looked at again */
 	NewHeap->rd_toastoid = InvalidOid;
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a6d5ed1f6..cdc39d17d 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -912,34 +912,20 @@ get_all_vacuum_rels(int options)
 }
 
 /*
- * vacuum_set_xid_limits() -- compute OldestXmin and freeze cutoff points
+ * vacuum_get_cutoffs() -- compute OldestXmin and freeze cutoff points
  *
  * The target relation and VACUUM parameters are our inputs.
  *
- * Our output parameters are:
- * - OldestXmin is the Xid below which tuples deleted by any xact (that
- *   committed) should be considered DEAD, not just RECENTLY_DEAD.
- * - OldestMxact is the Mxid below which MultiXacts are definitely not
- *   seen as visible by any running transaction.
- * - FreezeLimit is the Xid below which all Xids are definitely frozen or
- *   removed during aggressive vacuums.
- * - MultiXactCutoff is the value below which all MultiXactIds are definitely
- *   removed from Xmax during aggressive vacuums.
+ * Output parameters are the cutoffs that VACUUM caller should use.
  *
  * Return value indicates if vacuumlazy.c caller should make its VACUUM
  * operation aggressive.  An aggressive VACUUM must advance relfrozenxid up to
  * FreezeLimit (at a minimum), and relminmxid up to MultiXactCutoff (at a
  * minimum).
- *
- * OldestXmin and OldestMxact are the most recent values that can ever be
- * passed to vac_update_relstats() as frozenxid and minmulti arguments by our
- * vacuumlazy.c caller later on.  These values should be passed when it turns
- * out that VACUUM will leave no unfrozen XIDs/MXIDs behind in the table.
  */
 bool
-vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
-					  TransactionId *OldestXmin, MultiXactId *OldestMxact,
-					  TransactionId *FreezeLimit, MultiXactId *MultiXactCutoff)
+vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
+				   struct VacuumCutoffs *cutoffs)
 {
 	int			freeze_min_age,
 				multixact_freeze_min_age,
@@ -959,6 +945,10 @@ vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
 	freeze_table_age = params->freeze_table_age;
 	multixact_freeze_table_age = params->multixact_freeze_table_age;
 
+	/* Set pg_class fields in cutoffs */
+	cutoffs->relfrozenxid = rel->rd_rel->relfrozenxid;
+	cutoffs->relminmxid = rel->rd_rel->relminmxid;
+
 	/*
 	 * Acquire OldestXmin.
 	 *
@@ -970,14 +960,14 @@ vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
 	 * that only one vacuum process can be working on a particular table at
 	 * any time, and that each vacuum is always an independent transaction.
 	 */
-	*OldestXmin = GetOldestNonRemovableTransactionId(rel);
+	cutoffs->OldestXmin = GetOldestNonRemovableTransactionId(rel);
 
 	if (OldSnapshotThresholdActive())
 	{
 		TransactionId limit_xmin;
 		TimestampTz limit_ts;
 
-		if (TransactionIdLimitedForOldSnapshots(*OldestXmin, rel,
+		if (TransactionIdLimitedForOldSnapshots(cutoffs->OldestXmin, rel,
 												&limit_xmin, &limit_ts))
 		{
 			/*
@@ -987,20 +977,48 @@ vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
 			 * frequency), but would still be a significant improvement.
 			 */
 			SetOldSnapshotThresholdTimestamp(limit_ts, limit_xmin);
-			*OldestXmin = limit_xmin;
+			cutoffs->OldestXmin = limit_xmin;
 		}
 	}
 
-	Assert(TransactionIdIsNormal(*OldestXmin));
+	Assert(TransactionIdIsNormal(cutoffs->OldestXmin));
 
 	/* Acquire OldestMxact */
-	*OldestMxact = GetOldestMultiXactId();
-	Assert(MultiXactIdIsValid(*OldestMxact));
+	cutoffs->OldestMxact = GetOldestMultiXactId();
+	Assert(MultiXactIdIsValid(cutoffs->OldestMxact));
 
 	/* Acquire next XID/next MXID values used to apply age-based settings */
 	nextXID = ReadNextTransactionId();
 	nextMXID = ReadNextMultiXactId();
 
+	/*
+	 * Also compute the multixact age for which freezing is urgent.  This is
+	 * normally autovacuum_multixact_freeze_max_age, but may be less if we are
+	 * short of multixact member space.
+	 */
+	effective_multixact_freeze_max_age = MultiXactMemberFreezeThreshold();
+
+	/*
+	 * Almost ready to set freeze output parameters; check if OldestXmin or
+	 * OldestMxact are held back to an unsafe degree before we start on that
+	 */
+	safeOldestXmin = nextXID - autovacuum_freeze_max_age;
+	if (!TransactionIdIsNormal(safeOldestXmin))
+		safeOldestXmin = FirstNormalTransactionId;
+	safeOldestMxact = nextMXID - effective_multixact_freeze_max_age;
+	if (safeOldestMxact < FirstMultiXactId)
+		safeOldestMxact = FirstMultiXactId;
+	if (TransactionIdPrecedes(cutoffs->OldestXmin, safeOldestXmin))
+		ereport(WARNING,
+				(errmsg("cutoff for removing and freezing tuples is far in the past"),
+				 errhint("Close open transactions soon to avoid wraparound problems.\n"
+						 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
+	if (MultiXactIdPrecedes(cutoffs->OldestMxact, safeOldestMxact))
+		ereport(WARNING,
+				(errmsg("cutoff for freezing multixacts is far in the past"),
+				 errhint("Close open transactions soon to avoid wraparound problems.\n"
+						 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
+
 	/*
 	 * Determine the minimum freeze age to use: as specified by the caller, or
 	 * vacuum_freeze_min_age, but in any case not more than half
@@ -1013,19 +1031,12 @@ vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
 	Assert(freeze_min_age >= 0);
 
 	/* Compute FreezeLimit, being careful to generate a normal XID */
-	*FreezeLimit = nextXID - freeze_min_age;
-	if (!TransactionIdIsNormal(*FreezeLimit))
-		*FreezeLimit = FirstNormalTransactionId;
+	cutoffs->FreezeLimit = nextXID - freeze_min_age;
+	if (!TransactionIdIsNormal(cutoffs->FreezeLimit))
+		cutoffs->FreezeLimit = FirstNormalTransactionId;
 	/* FreezeLimit must always be <= OldestXmin */
-	if (TransactionIdPrecedes(*OldestXmin, *FreezeLimit))
-		*FreezeLimit = *OldestXmin;
-
-	/*
-	 * Compute the multixact age for which freezing is urgent.  This is
-	 * normally autovacuum_multixact_freeze_max_age, but may be less if we are
-	 * short of multixact member space.
-	 */
-	effective_multixact_freeze_max_age = MultiXactMemberFreezeThreshold();
+	if (TransactionIdPrecedes(cutoffs->OldestXmin, cutoffs->FreezeLimit))
+		cutoffs->FreezeLimit = cutoffs->OldestXmin;
 
 	/*
 	 * Determine the minimum multixact freeze age to use: as specified by
@@ -1040,33 +1051,12 @@ vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
 	Assert(multixact_freeze_min_age >= 0);
 
 	/* Compute MultiXactCutoff, being careful to generate a valid value */
-	*MultiXactCutoff = nextMXID - multixact_freeze_min_age;
-	if (*MultiXactCutoff < FirstMultiXactId)
-		*MultiXactCutoff = FirstMultiXactId;
+	cutoffs->MultiXactCutoff = nextMXID - multixact_freeze_min_age;
+	if (cutoffs->MultiXactCutoff < FirstMultiXactId)
+		cutoffs->MultiXactCutoff = FirstMultiXactId;
 	/* MultiXactCutoff must always be <= OldestMxact */
-	if (MultiXactIdPrecedes(*OldestMxact, *MultiXactCutoff))
-		*MultiXactCutoff = *OldestMxact;
-
-	/*
-	 * Done setting output parameters; check if OldestXmin or OldestMxact are
-	 * held back to an unsafe degree in passing
-	 */
-	safeOldestXmin = nextXID - autovacuum_freeze_max_age;
-	if (!TransactionIdIsNormal(safeOldestXmin))
-		safeOldestXmin = FirstNormalTransactionId;
-	safeOldestMxact = nextMXID - effective_multixact_freeze_max_age;
-	if (safeOldestMxact < FirstMultiXactId)
-		safeOldestMxact = FirstMultiXactId;
-	if (TransactionIdPrecedes(*OldestXmin, safeOldestXmin))
-		ereport(WARNING,
-				(errmsg("cutoff for removing and freezing tuples is far in the past"),
-				 errhint("Close open transactions soon to avoid wraparound problems.\n"
-						 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
-	if (MultiXactIdPrecedes(*OldestMxact, safeOldestMxact))
-		ereport(WARNING,
-				(errmsg("cutoff for freezing multixacts is far in the past"),
-				 errhint("Close open transactions soon to avoid wraparound problems.\n"
-						 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
+	if (MultiXactIdPrecedes(cutoffs->OldestMxact, cutoffs->MultiXactCutoff))
+		cutoffs->MultiXactCutoff = cutoffs->OldestMxact;
 
 	/*
 	 * Finally, figure out if caller needs to do an aggressive VACUUM or not.
@@ -1118,13 +1108,13 @@ vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
  * mechanism to determine if its table's relfrozenxid and relminmxid are now
  * dangerously far in the past.
  *
- * Input parameters are the target relation's relfrozenxid and relminmxid.
- *
  * When we return true, VACUUM caller triggers the failsafe.
  */
 bool
-vacuum_xid_failsafe_check(TransactionId relfrozenxid, MultiXactId relminmxid)
+vacuum_xid_failsafe_check(const struct VacuumCutoffs *cutoffs)
 {
+	TransactionId relfrozenxid = cutoffs->relfrozenxid;
+	MultiXactId relminmxid = cutoffs->relminmxid;
 	TransactionId xid_skip_limit;
 	MultiXactId multi_skip_limit;
 	int			skip_index_vacuum;
-- 
2.38.1

