From f6e0daeeae16af641e30e703969bfa6d67376f79 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 5 Sep 2022 17:46:34 -0700
Subject: [PATCH v17 3/3] Finish removing aggressive mode VACUUM.

The concept of aggressive/scan_all VACUUM dates back to the introduction
of the visibility map in Postgres 8.4.  Although pre-visibility-map
VACUUM was far less efficient (especially after 9.6's commit fd31cd26),
its naive approach had one notable advantage: users only had to think
about a single kind of lazy vacuum (the only kind that existed).

Break the final remaining dependency on aggressive mode: replace the
rules governing when VACUUM will wait for a cleanup lock with a new set
of rules more attuned to the needs of the table.  With that last
dependency gone, there is no need for aggressive mode, so get rid of it.
Users once again only have to think about one kind of lazy vacuum.

In general, all of the behaviors associated with aggressive mode prior
to Postgres 16 have been retained; they just get applied selectively, on
a more dynamic timeline.  For example, the aforementioned change to
VACUUM's cleanup lock behavior retains the general idea of sometimes
waiting for a cleanup lock to make sure that older XIDs get frozen, so
that relfrozenxid can be advanced by a sufficient amount.  All that
really changes is the information driving VACUUM's decision on waiting.

We use new, dedicated cutoffs, rather than applying the FreezeLimit and
MultiXactCutoff used when deciding whether we should trigger freezing on
the basis of XID/MXID age.  These minimum fallback cutoffs (which are
called MinXid and MinMulti) are typically far older than the standard
FreezeLimit/MultiXactCutoff cutoffs.  VACUUM doesn't need an aggressive
mode to decide on whether to wait for a cleanup lock anymore; it can
decide everything at the level of individual heap pages.

It is okay to aggressively punt on waiting for a cleanup lock like this
because VACUUM now directly understands the importance of never falling
too far behind on the work of freezing physical heap pages at the level
of the whole table, following recent work to add VM scanning strategies.
It is generally safer for VACUUM to press on with freezing other heap
pages from the table instead.  Even if relfrozenxid can only be advanced
by relatively few XIDs as a consequence, VACUUM should have more than
ample opportunity to catch up next time, since there is bound to be no
more than a small number of problematic unfrozen pages left behind.
VACUUM now tends to consistently advance relfrozenxid (at least by some
small amount) all the time in larger tables, so all that has to happen
for relfrozenxid to fully catch up is for a few remaining unfrozen pages
to get frozen.  Since relfrozenxid is now considered to be no more than
a lagging indicator of freezing, and since relfrozenxid isn't used to
trigger freezing in the way that it once was, time is on our side.

Also teach VACUUM to wait for a short while for cleanup locks when doing
so has a decent chance of preserving its ability to advance relfrozenxid
up to FreezeLimit (and/or to advance relminmxid up to MultiXactCutoff).
As a result, VACUUM typically manages to advance relfrozenxid by just as
much as it would have had it promised to advance it up to FreezeLimit
(i.e. had it made the traditional aggressive VACUUM guarantee), even
when vacuuming a table that happens to have relatively many cleanup lock
conflicts affecting pages with older XIDs/MXIDs.  VACUUM thereby avoids
missing out on advancing relfrozenxid up to the traditional target
amount when it really can be avoided fairly easily, without promising to
do so (VACUUM only promises to advance up to MinXid/MinMulti).

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Jeff Davis <pgsql@j-davis.com>
Reviewed-By: Andres Freund <andres@anarazel.de>
Reviewed-By: Matthias van de Meent <boekewurm+postgres@gmail.com>
Discussion: https://postgr.es/m/CAH2-WzkU42GzrsHhL2BiC1QMhaVGmVdb5HR0_qczz0Gu2aSn=A@mail.gmail.com
---
 src/include/commands/vacuum.h                 |   9 +-
 src/backend/access/heap/heapam.c              |   2 +
 src/backend/access/heap/vacuumlazy.c          | 230 +++++++++++-------
 src/backend/commands/vacuum.c                 |  42 +++-
 src/backend/utils/activity/pgstat_relation.c  |   4 +-
 doc/src/sgml/maintenance.sgml                 |  37 +--
 .../expected/vacuum-no-cleanup-lock.out       |  24 +-
 .../specs/vacuum-no-cleanup-lock.spec         |  33 +--
 8 files changed, 230 insertions(+), 151 deletions(-)

diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 18a56efbd..12f0704f9 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -281,6 +281,13 @@ struct VacuumCutoffs
 	TransactionId FreezeLimit;
 	MultiXactId MultiXactCutoff;
 
+	/*
+	 * Earliest permissible NewRelfrozenXid/NewRelminMxid values that can be
+	 * set in pg_class at the end of VACUUM.
+	 */
+	TransactionId MinXid;
+	MultiXactId MinMulti;
+
 	/*
 	 * Threshold that triggers VACUUM's eager freezing strategy
 	 */
@@ -357,7 +364,7 @@ extern void vac_update_relstats(Relation relation,
 								bool *frozenxid_updated,
 								bool *minmulti_updated,
 								bool in_outer_xact);
-extern bool vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
+extern void 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);
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 602befa1d..22a2e3028 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -7056,6 +7056,8 @@ heap_freeze_tuple(HeapTupleHeader tuple,
 	cutoffs.OldestMxact = MultiXactCutoff;
 	cutoffs.FreezeLimit = FreezeLimit;
 	cutoffs.MultiXactCutoff = MultiXactCutoff;
+	cutoffs.MinXid = FreezeLimit;
+	cutoffs.MinMulti = MultiXactCutoff;
 	cutoffs.freeze_strategy_threshold_pages = 0;
 	cutoffs.tableagefrac = 0;
 
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index b5a4094ba..9e5a7be23 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -157,8 +157,6 @@ typedef struct LVRelState
 	BufferAccessStrategy bstrategy;
 	ParallelVacuumState *pvs;
 
-	/* Aggressive VACUUM? (must set relfrozenxid >= FreezeLimit) */
-	bool		aggressive;
 	/* Eagerly freeze all tuples on pages about to be set all-visible? */
 	bool		eager_freeze_strategy;
 	/* Wraparound failsafe has been triggered? */
@@ -264,7 +262,8 @@ static void lazy_scan_prune(LVRelState *vacrel, Buffer buf,
 							LVPagePruneState *prunestate);
 static bool lazy_scan_noprune(LVRelState *vacrel, Buffer buf,
 							  BlockNumber blkno, Page page,
-							  bool *hastup, bool *recordfreespace);
+							  bool *hastup, bool *recordfreespace,
+							  Size *freespace);
 static void lazy_vacuum(LVRelState *vacrel);
 static bool lazy_vacuum_all_indexes(LVRelState *vacrel);
 static void lazy_vacuum_heap_rel(LVRelState *vacrel);
@@ -462,7 +461,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 * 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.)
 	 */
-	vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs);
+	vacuum_get_cutoffs(rel, params, &vacrel->cutoffs);
 	vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel);
 	vacrel->vistest = GlobalVisTestFor(rel);
 	/* Initialize state used to track oldest extant XID/MXID */
@@ -542,17 +541,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	/*
 	 * Prepare to update rel's pg_class entry.
 	 *
-	 * Aggressive VACUUMs must always be able to advance relfrozenxid to a
-	 * value >= FreezeLimit, and relminmxid to a value >= MultiXactCutoff.
-	 * Non-aggressive VACUUMs may advance them by any amount, or not at all.
+	 * VACUUM can only advance relfrozenxid to a value >= MinXid, and
+	 * relminmxid to a value >= MinMulti.
 	 */
 	Assert(vacrel->NewRelfrozenXid == vacrel->cutoffs.OldestXmin ||
-		   TransactionIdPrecedesOrEquals(vacrel->aggressive ? vacrel->cutoffs.FreezeLimit :
-										 vacrel->cutoffs.relfrozenxid,
+		   TransactionIdPrecedesOrEquals(vacrel->cutoffs.MinXid,
 										 vacrel->NewRelfrozenXid));
 	Assert(vacrel->NewRelminMxid == vacrel->cutoffs.OldestMxact ||
-		   MultiXactIdPrecedesOrEquals(vacrel->aggressive ? vacrel->cutoffs.MultiXactCutoff :
-									   vacrel->cutoffs.relminmxid,
+		   MultiXactIdPrecedesOrEquals(vacrel->cutoffs.MinMulti,
 									   vacrel->NewRelminMxid));
 	if (vacrel->vmstrat == VMSNAP_SCAN_LAZY)
 	{
@@ -560,7 +556,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 		 * Must keep original relfrozenxid/relminmxid when lazy_scan_strategy
 		 * decided to skip all-visible pages containing unfrozen XIDs/MXIDs
 		 */
-		Assert(!vacrel->aggressive);
 		vacrel->NewRelfrozenXid = InvalidTransactionId;
 		vacrel->NewRelminMxid = InvalidMultiXactId;
 	}
@@ -629,33 +624,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 			TimestampDifference(starttime, endtime, &secs_dur, &usecs_dur);
 			memset(&walusage, 0, sizeof(WalUsage));
 			WalUsageAccumDiff(&walusage, &pgWalUsage, &startwalusage);
-
 			initStringInfo(&buf);
+
 			if (verbose)
-			{
-				Assert(!params->is_wraparound);
 				msgfmt = _("finished vacuuming \"%s.%s.%s\": index scans: %d\n");
-			}
 			else if (params->is_wraparound)
-			{
-				/*
-				 * While it's possible for a VACUUM to be both is_wraparound
-				 * and !aggressive, that's just a corner-case -- is_wraparound
-				 * implies aggressive.  Produce distinct output for the corner
-				 * case all the same, just in case.
-				 */
-				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");
-			}
+				msgfmt = _("automatic vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n");
 			else
-			{
-				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");
-			}
+				msgfmt = _("automatic vacuum of table \"%s.%s.%s\": index scans: %d\n");
 			appendStringInfo(&buf, msgfmt,
 							 vacrel->dbname,
 							 vacrel->relnamespace,
@@ -941,6 +917,7 @@ lazy_scan_heap(LVRelState *vacrel)
 		{
 			bool		hastup,
 						recordfreespace;
+			Size		freespace;
 
 			LockBuffer(buf, BUFFER_LOCK_SHARE);
 
@@ -954,10 +931,8 @@ lazy_scan_heap(LVRelState *vacrel)
 
 			/* Collect LP_DEAD items in dead_items array, count tuples */
 			if (lazy_scan_noprune(vacrel, buf, blkno, page, &hastup,
-								  &recordfreespace))
+								  &recordfreespace, &freespace))
 			{
-				Size		freespace = 0;
-
 				/*
 				 * Processed page successfully (without cleanup lock) -- just
 				 * need to perform rel truncation and FSM steps, much like the
@@ -966,21 +941,14 @@ lazy_scan_heap(LVRelState *vacrel)
 				 */
 				if (hastup)
 					vacrel->nonempty_pages = blkno + 1;
-				if (recordfreespace)
-					freespace = PageGetHeapFreeSpace(page);
-				UnlockReleaseBuffer(buf);
 				if (recordfreespace)
 					RecordPageWithFreeSpace(vacrel->rel, blkno, freespace);
+
+				/* lock and pin released by lazy_scan_noprune */
 				continue;
 			}
 
-			/*
-			 * lazy_scan_noprune could not do all required processing.  Wait
-			 * for a cleanup lock, and call lazy_scan_prune in the usual way.
-			 */
-			Assert(vacrel->aggressive);
-			LockBuffer(buf, BUFFER_LOCK_UNLOCK);
-			LockBufferForCleanup(buf);
+			/* cleanup lock acquired by lazy_scan_noprune */
 		}
 
 		/* Check for new or empty pages before lazy_scan_prune call */
@@ -1403,8 +1371,6 @@ lazy_scan_strategy(LVRelState *vacrel, bool force_scan_all)
 	if (force_scan_all)
 		vacrel->vmstrat = VMSNAP_SCAN_ALL;
 
-	Assert(!vacrel->aggressive || vacrel->vmstrat != VMSNAP_SCAN_LAZY);
-
 	/* Inform vmsnap infrastructure of our chosen strategy */
 	visibilitymap_snap_strategy(vacrel->vmsnap, vacrel->vmstrat);
 
@@ -1998,17 +1964,32 @@ retry:
  * lazy_scan_prune, which requires a full cleanup lock.  While pruning isn't
  * performed here, it's quite possible that an earlier opportunistic pruning
  * operation left LP_DEAD items behind.  We'll at least collect any such items
- * in the dead_items array for removal from indexes.
+ * in the dead_items array for removal from indexes (assuming caller's page
+ * can be processed successfully here).
  *
- * For aggressive VACUUM callers, we may return false to indicate that a full
- * cleanup lock is required for processing by lazy_scan_prune.  This is only
- * necessary when the aggressive VACUUM needs to freeze some tuple XIDs from
- * one or more tuples on the page.  We always return true for non-aggressive
- * callers.
+ * We return true to indicate that processing succeeded, in which case we'll
+ * have dropped the lock and pin on buf/page.  Else returns false, indicating
+ * that page must be processed by lazy_scan_prune in the usual way after all.
+ * Acquires a cleanup lock on buf/page for caller before returning false.
+ *
+ * We go to considerable trouble to get a cleanup lock on any page that has
+ * XIDs/MXIDs that need to be frozen in order for VACUUM to be able to set
+ * relfrozenxid/relminmxid to values >= FreezeLimit/MultiXactCutoff cutoffs.
+ * But we don't strictly guarantee it; we only guarantee that final values
+ * will be >= MinXid/MinMulti cutoffs in the worst case.
+ *
+ * We prefer to "under promise and over deliver" like this because a strong
+ * guarantee has the potential to make a bad situation even worse.  VACUUM
+ * should avoid waiting for a cleanup lock for an indefinitely long time until
+ * it has already exhausted every available alternative.  It's quite possible
+ * (and perhaps even likely) that the problem will go away on its own.  But
+ * even when it doesn't, our approach at least makes it likely that the first
+ * VACUUM that encounters the issue will catch up on whatever freezing may
+ * still be required for every other page in the target rel.
  *
  * See lazy_scan_prune for an explanation of hastup return flag.
  * recordfreespace flag instructs caller on whether or not it should do
- * generic FSM processing for page.
+ * generic FSM processing for page, using *freespace value set here.
  */
 static bool
 lazy_scan_noprune(LVRelState *vacrel,
@@ -2016,7 +1997,8 @@ lazy_scan_noprune(LVRelState *vacrel,
 				  BlockNumber blkno,
 				  Page page,
 				  bool *hastup,
-				  bool *recordfreespace)
+				  bool *recordfreespace,
+				  Size *freespace)
 {
 	OffsetNumber offnum,
 				maxoff;
@@ -2024,6 +2006,7 @@ lazy_scan_noprune(LVRelState *vacrel,
 				live_tuples,
 				recently_dead_tuples,
 				missed_dead_tuples;
+	bool		should_freeze = false;
 	HeapTupleHeader tupleheader;
 	TransactionId NoFreezePageRelfrozenXid = vacrel->NewRelfrozenXid;
 	MultiXactId NoFreezePageRelminMxid = vacrel->NewRelminMxid;
@@ -2033,6 +2016,7 @@ lazy_scan_noprune(LVRelState *vacrel,
 
 	*hastup = false;			/* for now */
 	*recordfreespace = false;	/* for now */
+	*freespace = PageGetHeapFreeSpace(page);
 
 	lpdead_items = 0;
 	live_tuples = 0;
@@ -2074,34 +2058,7 @@ lazy_scan_noprune(LVRelState *vacrel,
 		if (heap_tuple_should_freeze(tupleheader, &vacrel->cutoffs,
 									 &NoFreezePageRelfrozenXid,
 									 &NoFreezePageRelminMxid))
-		{
-			/* Tuple with XID < FreezeLimit (or MXID < MultiXactCutoff) */
-			if (vacrel->aggressive)
-			{
-				/*
-				 * Aggressive VACUUMs must always be able to advance rel's
-				 * relfrozenxid to a value >= FreezeLimit (and be able to
-				 * advance rel's relminmxid to a value >= MultiXactCutoff).
-				 * The ongoing aggressive VACUUM won't be able to do that
-				 * unless it can freeze an XID (or MXID) from this tuple now.
-				 *
-				 * The only safe option is to have caller perform processing
-				 * of this page using lazy_scan_prune.  Caller might have to
-				 * wait a while for a cleanup lock, but it can't be helped.
-				 */
-				vacrel->offnum = InvalidOffsetNumber;
-				return false;
-			}
-
-			/*
-			 * Non-aggressive VACUUMs are under no obligation to advance
-			 * relfrozenxid (even by one XID).  We can be much laxer here.
-			 *
-			 * Currently we always just accept an older final relfrozenxid
-			 * and/or relminmxid value.  We never make caller wait or work a
-			 * little harder, even when it likely makes sense to do so.
-			 */
-		}
+			should_freeze = true;
 
 		ItemPointerSet(&(tuple.t_self), blkno, offnum);
 		tuple.t_data = (HeapTupleHeader) PageGetItem(page, itemid);
@@ -2150,10 +2107,107 @@ lazy_scan_noprune(LVRelState *vacrel,
 	vacrel->offnum = InvalidOffsetNumber;
 
 	/*
-	 * By here we know for sure that caller can put off freezing and pruning
-	 * this particular page until the next VACUUM.  Remember its details now.
-	 * (lazy_scan_prune expects a clean slate, so we have to do this last.)
+	 * Release lock (but not pin) on page now.  Then consider if we should
+	 * back out of accepting reduced processing for this page.
+	 *
+	 * Our caller's initial inability to get a cleanup lock will often turn
+	 * out to have been nothing more than a momentary blip, and it would be a
+	 * shame if relfrozenxid/relminmxid values < FreezeLimit/MultiXactCutoff
+	 * were used without good reason.  For example, the checkpointer might
+	 * have been writing out this page a moment ago, in which case its buffer
+	 * pin might have already been released by now.
+	 *
+	 * It's also possible that the conflicting buffer pin will continue to
+	 * block cleanup lock acquisition on the buffer for an extended period.
+	 * For example, it isn't uncommon for heap_lock_tuple to sleep while
+	 * holding a buffer pin, in which case a conflicting pin could easily be
+	 * held for much longer than VACUUM can reasonably be expected to wait.
+	 * There are also truly pathological cases to worry about.  For example,
+	 * the case where buggy application code holds open a cursor forever.
 	 */
+	LockBuffer(buf, BUFFER_LOCK_UNLOCK);
+	if (should_freeze)
+	{
+		TransactionId RelfrozenXid;
+		MultiXactId RelminMxid;
+
+		/*
+		 * If page has tuple with a dangerously old XID/MXID (an XID < MinXid,
+		 * or an MXID < MinMulti), then we wait for however long it takes to
+		 * get a cleanup lock.
+		 *
+		 * Check for that first (get it out of the way).
+		 */
+		if (TransactionIdPrecedes(NoFreezePageRelfrozenXid,
+								  vacrel->cutoffs.MinXid) ||
+			MultiXactIdPrecedes(NoFreezePageRelminMxid,
+								vacrel->cutoffs.MinMulti))
+		{
+			/*
+			 * MinXid/MinMulti are considered to be only barely adequate final
+			 * values, so we only expect to end up here when previous VACUUMs
+			 * put off processing by lazy_scan_prune in the hope that it would
+			 * never come to this.  That hasn't worked out, so we must wait.
+			 */
+			LockBufferForCleanup(buf);
+			return false;
+		}
+
+		/*
+		 * Page has tuple with XID < FreezeLimit, or MXID < MultiXactCutoff,
+		 * but they're not so old that we're _strictly_ obligated to freeze.
+		 *
+		 * We are willing to go to the trouble of waiting for a cleanup lock
+		 * for a short while for such a page -- just not indefinitely long.
+		 * This avoids squandering opportunities to advance relfrozenxid or
+		 * relminmxid by the target amount during any one VACUUM, which is
+		 * particularly important with larger tables that only get vacuumed
+		 * when autovacuum.c is concerned about table age.  It would not be
+		 * okay if the number of autovacuums such a table ended up requiring
+		 * noticeably exceeded the expected autovacuum_freeze_max_age cadence.
+		 */
+		RelfrozenXid = NoFreezePageRelfrozenXid;
+		if (TransactionIdPrecedes(vacrel->cutoffs.FreezeLimit, RelfrozenXid))
+			RelfrozenXid = vacrel->cutoffs.FreezeLimit;
+		RelminMxid = NoFreezePageRelminMxid;
+		if (MultiXactIdPrecedes(vacrel->cutoffs.MultiXactCutoff, RelminMxid))
+			RelminMxid = vacrel->cutoffs.MultiXactCutoff;
+
+		/*
+		 * We are willing to wait and try again a total of 3 times.  If that
+		 * doesn't work then we just give up.  We only wait here when it is
+		 * actually expected to preserve current NewRelfrozenXid/NewRelminMxid
+		 * tracker values, and when trackers will actually be used to update
+		 * pg_class later on.  This also tends to limit the impact of waiting
+		 * for VACUUMs that experience relatively many cleanup lock conflicts.
+		 */
+		if (vacrel->vmstrat != VMSNAP_SCAN_LAZY &&
+			(TransactionIdPrecedes(RelfrozenXid, vacrel->NewRelfrozenXid) ||
+			 MultiXactIdPrecedes(RelminMxid, vacrel->NewRelminMxid)))
+		{
+			/* wait 10ms, then 20ms, then 30ms, then give up */
+			for (int i = 1; i <= 3; i++)
+			{
+				CHECK_FOR_INTERRUPTS();
+
+				pg_usleep(1000L * 10L * i);
+				if (ConditionalLockBufferForCleanup(buf))
+				{
+					/* Go process page in lazy_scan_prune after all */
+					return false;
+				}
+			}
+
+			/* Give up, accepting reduced processing for this page */
+		}
+	}
+
+	/*
+	 * By here we know for sure that caller will put off freezing and pruning
+	 * this particular page until the next VACUUM.  Remember its details now.
+	 * Also drop the buffer pin that we held onto during cleanup lock steps.
+	 */
+	ReleaseBuffer(buf);
 	vacrel->NewRelfrozenXid = NoFreezePageRelfrozenXid;
 	vacrel->NewRelminMxid = NoFreezePageRelminMxid;
 
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 4d1e13d51..6f3bfa1e1 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -951,13 +951,8 @@ get_all_vacuum_rels(int options)
  * The target relation and VACUUM parameters are our inputs.
  *
  * 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).
  */
-bool
+void
 vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
 				   struct VacuumCutoffs *cutoffs)
 {
@@ -1138,6 +1133,39 @@ vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
 		multixact_freeze_table_age = effective_multixact_freeze_max_age;
 	Assert(multixact_freeze_table_age >= 0);
 
+	/*
+	 * Determine the cutoffs used by VACUUM to decide on whether to wait for a
+	 * cleanup lock on a page (that it can't cleanup lock right away).  These
+	 * are the MinXid and MinMulti cutoffs.  They are related to the cutoffs
+	 * for freezing (FreezeLimit and MultiXactCutoff), and are only applied on
+	 * pages that we cannot freeze right away.  See vacuumlazy.c for details.
+	 *
+	 * VACUUM can ratchet back NewRelfrozenXid and/or NewRelminMxid instead of
+	 * waiting indefinitely for a cleanup lock in almost all cases.  The high
+	 * level goal is to create as many opportunities as possible to freeze
+	 * (across many successive VACUUM operations), while avoiding waiting for
+	 * a cleanup lock whenever possible.  Any time spent waiting is time spent
+	 * not freezing other eligible pages, which is typically a bad trade-off.
+	 *
+	 * As a consequence of all this, MinXid and MinMulti also act as limits on
+	 * the oldest acceptable values that can ever be set in pg_class by VACUUM
+	 * (though this is only relevant when they have already attained XID/XMID
+	 * ages that approach freeze_table_age and/or multixact_freeze_table_age).
+	 */
+	cutoffs->MinXid = nextXID - (freeze_table_age * 0.95);
+	if (!TransactionIdIsNormal(cutoffs->MinXid))
+		cutoffs->MinXid = FirstNormalTransactionId;
+	/* MinXid must always be <= FreezeLimit */
+	if (TransactionIdPrecedes(cutoffs->FreezeLimit, cutoffs->MinXid))
+		cutoffs->MinXid = cutoffs->FreezeLimit;
+
+	cutoffs->MinMulti = nextMXID - (multixact_freeze_table_age * 0.95);
+	if (cutoffs->MinMulti < FirstMultiXactId)
+		cutoffs->MinMulti = FirstMultiXactId;
+	/* MinMulti must always be <= MultiXactCutoff */
+	if (MultiXactIdPrecedes(cutoffs->MultiXactCutoff, cutoffs->MinMulti))
+		cutoffs->MinMulti = cutoffs->MultiXactCutoff;
+
 	/*
 	 * Finally, set tableagefrac for VACUUM.  This can come from either XID or
 	 * XMID table age (whichever is greater currently).
@@ -1155,8 +1183,6 @@ vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
 	 */
 	if (params->is_wraparound)
 		cutoffs->tableagefrac = 1.0;
-
-	return (cutoffs->tableagefrac >= 1.0);
 }
 
 /*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 2e20b93c2..056ef0178 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -235,8 +235,8 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	tabentry->dead_tuples = deadtuples;
 
 	/*
-	 * It is quite possible that a non-aggressive VACUUM ended up skipping
-	 * various pages, however, we'll zero the insert counter here regardless.
+	 * It is quite possible that VACUUM will skip all-visible pages for a
+	 * smaller table, however, we'll zero the insert counter here regardless.
 	 * It's currently used only to track when we need to perform an "insert"
 	 * autovacuum, which are mainly intended to freeze newly inserted tuples.
 	 * Zeroing this may just mean we'll not try to vacuum the table again
diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index 63bd10f2b..7899c27ea 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -525,19 +525,13 @@
     will skip pages that don't have any dead row versions even if those pages
     might still have row versions with old XID values.  Therefore, normal
     <command>VACUUM</command>s won't always freeze every old row version in the table.
-    When that happens, <command>VACUUM</command> will eventually need to perform an
-    <firstterm>aggressive vacuum</firstterm>, which will freeze all eligible unfrozen
-    XID and MXID values, including those from all-visible but not all-frozen pages.
-    In practice most tables require periodic aggressive vacuuming.
    </para>
 
    <para>
     The maximum time that a table can go unvacuumed is two billion
     transactions minus the <varname>vacuum_freeze_min_age</varname> value at
-    the time of the last aggressive vacuum. If it were to go
-    unvacuumed for longer than
-    that, data loss could result.  To ensure that this does not happen,
-    autovacuum is invoked on any table that might contain unfrozen rows with
+    the time of the last vacuum that advanced <structfield>relfrozenxid</structfield>.
+    Autovacuum is invoked on any table that might contain unfrozen rows with
     XIDs older than the age specified by the configuration parameter <xref
     linkend="guc-autovacuum-freeze-max-age"/>.  (This will happen even if
     autovacuum is disabled.)
@@ -595,8 +589,7 @@
     the <structfield>relfrozenxid</structfield> column of a table's
     <structname>pg_class</structname> row contains the oldest remaining unfrozen
     XID at the end of the most recent <command>VACUUM</command> that successfully
-    advanced <structfield>relfrozenxid</structfield> (typically the most recent
-    aggressive VACUUM).  Similarly, the
+    advanced <structfield>relfrozenxid</structfield>.  Similarly, the
     <structfield>datfrozenxid</structfield> column of a database's
     <structname>pg_database</structname> row is a lower bound on the unfrozen XIDs
     appearing in that database &mdash; it is just the minimum of the
@@ -753,22 +746,14 @@ HINT:  Stop the postmaster and vacuum that database in single-user mode.
     </para>
 
     <para>
-     Aggressive <command>VACUUM</command>s, regardless of what causes
-     them, are <emphasis>guaranteed</emphasis> to be able to advance
-     the table's <structfield>relminmxid</structfield>.
-     Eventually, as all tables in all databases are scanned and their
-     oldest multixact values are advanced, on-disk storage for older
-     multixacts can be removed.
-    </para>
-
-    <para>
-     As a safety device, an aggressive vacuum scan will
-     occur for any table whose multixact-age is greater than <xref
-     linkend="guc-autovacuum-multixact-freeze-max-age"/>.  Also, if the
-     storage occupied by multixacts members exceeds 2GB, aggressive vacuum
-     scans will occur more often for all tables, starting with those that
-     have the oldest multixact-age.  Both of these kinds of aggressive
-     scans will occur even if autovacuum is nominally disabled.
+     As a safety device, a vacuum to advance
+     <structfield>relminmxid</structfield> will occur for any table
+     whose multixact-age is greater than <xref
+      linkend="guc-autovacuum-multixact-freeze-max-age"/>.
+     Also, if the storage occupied by multixacts members exceeds 2GB,
+     vacuum scans will occur more often for all tables, starting with those that
+     have the oldest multixact-age.  This will occur even if
+     autovacuum is nominally disabled.
     </para>
    </sect3>
   </sect2>
diff --git a/src/test/isolation/expected/vacuum-no-cleanup-lock.out b/src/test/isolation/expected/vacuum-no-cleanup-lock.out
index f7bc93e8f..076fe07ab 100644
--- a/src/test/isolation/expected/vacuum-no-cleanup-lock.out
+++ b/src/test/isolation/expected/vacuum-no-cleanup-lock.out
@@ -1,6 +1,6 @@
 Parsed test spec with 4 sessions
 
-starting permutation: vacuumer_pg_class_stats dml_insert vacuumer_nonaggressive_vacuum vacuumer_pg_class_stats
+starting permutation: vacuumer_pg_class_stats dml_insert vacuumer_vacuum_noprune vacuumer_pg_class_stats
 step vacuumer_pg_class_stats: 
   SELECT relpages, reltuples FROM pg_class WHERE oid = 'smalltbl'::regclass;
 
@@ -12,7 +12,7 @@ relpages|reltuples
 step dml_insert: 
   INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step vacuumer_pg_class_stats: 
@@ -24,7 +24,7 @@ relpages|reltuples
 (1 row)
 
 
-starting permutation: vacuumer_pg_class_stats dml_insert pinholder_cursor vacuumer_nonaggressive_vacuum vacuumer_pg_class_stats pinholder_commit
+starting permutation: vacuumer_pg_class_stats dml_insert pinholder_cursor vacuumer_vacuum_noprune vacuumer_pg_class_stats pinholder_commit
 step vacuumer_pg_class_stats: 
   SELECT relpages, reltuples FROM pg_class WHERE oid = 'smalltbl'::regclass;
 
@@ -46,7 +46,7 @@ dummy
     1
 (1 row)
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step vacuumer_pg_class_stats: 
@@ -61,7 +61,7 @@ step pinholder_commit:
   COMMIT;
 
 
-starting permutation: vacuumer_pg_class_stats pinholder_cursor dml_insert dml_delete dml_insert vacuumer_nonaggressive_vacuum vacuumer_pg_class_stats pinholder_commit
+starting permutation: vacuumer_pg_class_stats pinholder_cursor dml_insert dml_delete dml_insert vacuumer_vacuum_noprune vacuumer_pg_class_stats pinholder_commit
 step vacuumer_pg_class_stats: 
   SELECT relpages, reltuples FROM pg_class WHERE oid = 'smalltbl'::regclass;
 
@@ -89,7 +89,7 @@ step dml_delete:
 step dml_insert: 
   INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step vacuumer_pg_class_stats: 
@@ -104,7 +104,7 @@ step pinholder_commit:
   COMMIT;
 
 
-starting permutation: vacuumer_pg_class_stats dml_insert dml_delete pinholder_cursor dml_insert vacuumer_nonaggressive_vacuum vacuumer_pg_class_stats pinholder_commit
+starting permutation: vacuumer_pg_class_stats dml_insert dml_delete pinholder_cursor dml_insert vacuumer_vacuum_noprune vacuumer_pg_class_stats pinholder_commit
 step vacuumer_pg_class_stats: 
   SELECT relpages, reltuples FROM pg_class WHERE oid = 'smalltbl'::regclass;
 
@@ -132,7 +132,7 @@ dummy
 step dml_insert: 
   INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step vacuumer_pg_class_stats: 
@@ -147,7 +147,7 @@ step pinholder_commit:
   COMMIT;
 
 
-starting permutation: dml_begin dml_other_begin dml_key_share dml_other_key_share vacuumer_nonaggressive_vacuum pinholder_cursor dml_other_update dml_commit dml_other_commit vacuumer_nonaggressive_vacuum pinholder_commit vacuumer_nonaggressive_vacuum
+starting permutation: dml_begin dml_other_begin dml_key_share dml_other_key_share vacuumer_vacuum_noprune pinholder_cursor dml_other_update dml_commit dml_other_commit vacuumer_vacuum_noprune pinholder_commit vacuumer_vacuum_noprune
 step dml_begin: BEGIN;
 step dml_other_begin: BEGIN;
 step dml_key_share: SELECT id FROM smalltbl WHERE id = 3 FOR KEY SHARE;
@@ -162,7 +162,7 @@ id
  3
 (1 row)
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step pinholder_cursor: 
@@ -178,12 +178,12 @@ dummy
 step dml_other_update: UPDATE smalltbl SET t = 'u' WHERE id = 3;
 step dml_commit: COMMIT;
 step dml_other_commit: COMMIT;
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step pinholder_commit: 
   COMMIT;
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
diff --git a/src/test/isolation/specs/vacuum-no-cleanup-lock.spec b/src/test/isolation/specs/vacuum-no-cleanup-lock.spec
index 05fd280f6..f9e4194cd 100644
--- a/src/test/isolation/specs/vacuum-no-cleanup-lock.spec
+++ b/src/test/isolation/specs/vacuum-no-cleanup-lock.spec
@@ -55,15 +55,21 @@ step dml_other_key_share  { SELECT id FROM smalltbl WHERE id = 3 FOR KEY SHARE;
 step dml_other_update     { UPDATE smalltbl SET t = 'u' WHERE id = 3; }
 step dml_other_commit     { COMMIT; }
 
-# This session runs non-aggressive VACUUM, but with maximally aggressive
-# cutoffs for tuple freezing (e.g., FreezeLimit == OldestXmin):
+# This session runs VACUUM with maximally aggressive cutoffs for tuple
+# freezing (e.g., FreezeLimit == OldestXmin), while still using default
+# settings for vacuum_freeze_table_age/autovacuum_freeze_max_age.
+#
+# This makes VACUUM freeze tuples just as aggressively as it would if the
+# VACUUM command's FREEZE option was specified with almost all heap pages.
+# However, VACUUM is still unwilling to wait indefinitely for a cleanup lock,
+# just to freeze a few XIDs/MXIDs that still aren't very old.
 session vacuumer
 setup
 {
   SET vacuum_freeze_min_age = 0;
   SET vacuum_multixact_freeze_min_age = 0;
 }
-step vacuumer_nonaggressive_vacuum
+step vacuumer_vacuum_noprune
 {
   VACUUM smalltbl;
 }
@@ -75,15 +81,14 @@ step vacuumer_pg_class_stats
 # Test VACUUM's reltuples counting mechanism.
 #
 # Final pg_class.reltuples should never be affected by VACUUM's inability to
-# get a cleanup lock on any page, except to the extent that any cleanup lock
-# contention changes the number of tuples that remain ("missed dead" tuples
-# are counted in reltuples, much like "recently dead" tuples).
+# get a cleanup lock on any page.  Note that "missed dead" tuples are counted
+# in reltuples, much like "recently dead" tuples.
 
 # Easy case:
 permutation
     vacuumer_pg_class_stats  # Start with 20 tuples
     dml_insert
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     vacuumer_pg_class_stats  # End with 21 tuples
 
 # Harder case -- count 21 tuples at the end (like last time), but with cleanup
@@ -92,7 +97,7 @@ permutation
     vacuumer_pg_class_stats  # Start with 20 tuples
     dml_insert
     pinholder_cursor
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     vacuumer_pg_class_stats  # End with 21 tuples
     pinholder_commit  # order doesn't matter
 
@@ -103,7 +108,7 @@ permutation
     dml_insert
     dml_delete
     dml_insert
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     # reltuples is 21 here again -- "recently dead" tuple won't be included in
     # count here:
     vacuumer_pg_class_stats
@@ -116,7 +121,7 @@ permutation
     dml_delete
     pinholder_cursor
     dml_insert
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     # reltuples is 21 here again -- "missed dead" tuple ("recently dead" when
     # concurrent activity held back VACUUM's OldestXmin) won't be included in
     # count here:
@@ -128,7 +133,7 @@ permutation
 # This provides test coverage for code paths that are only hit when we need to
 # freeze, but inability to acquire a cleanup lock on a heap page makes
 # freezing some XIDs/MXIDs < FreezeLimit/MultiXactCutoff impossible (without
-# waiting for a cleanup lock, which non-aggressive VACUUM is unwilling to do).
+# waiting for a cleanup lock, which won't ever happen here).
 permutation
     dml_begin
     dml_other_begin
@@ -136,15 +141,15 @@ permutation
     dml_other_key_share
     # Will get cleanup lock, can't advance relminmxid yet:
     # (though will usually advance relfrozenxid by ~2 XIDs)
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     pinholder_cursor
     dml_other_update
     dml_commit
     dml_other_commit
     # Can't cleanup lock, so still can't advance relminmxid here:
     # (relfrozenxid held back by XIDs in MultiXact too)
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     pinholder_commit
     # Pin was dropped, so will advance relminmxid, at long last:
     # (ditto for relfrozenxid advancement)
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
-- 
2.39.0

