From 2dfdad045c07d274e97c9223b0240c91098d006b Mon Sep 17 00:00:00 2001
From: Jingtang Zhang <mrdrivingduck@gmail.com>
Date: Wed, 26 Aug 2026 12:24:40 +0800
Subject: [PATCH v2 2/2] vacuum: Allow freezing without a cleanup lock

Allow aggressive vacuum to freeze eligible tuple headers after cleanup
lock contention.

Keep pages containing old dead tuples on the existing cleanup path so
pruning and freezing remain combined when needed.
---
 src/backend/access/heap/pruneheap.c           | 185 ++++++++++++++++++
 src/backend/access/heap/vacuumlazy.c          |  89 ++++++++-
 src/include/access/heapam.h                   |  32 +++
 .../expected/vacuum-no-cleanup-lock.out       |  57 ++++++
 .../specs/vacuum-no-cleanup-lock.spec         |  34 ++++
 src/tools/pgindent/typedefs.list              |   2 +
 6 files changed, 395 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index 90d59ca306f..9f84437d2e9 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -1465,6 +1465,191 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
 										 new_relfrozen_xid, new_relmin_mxid);
 }
 
+/*
+ * Freeze tuples on a page without pruning it.
+ *
+ * Caller must hold a pin and an exclusive lock on the page.  This function
+ * neither changes line pointers nor updates pruning hints or the visibility
+ * map.  It is intended for VACUUM to make freeze progress on a page that is
+ * pinned by another backend and therefore cannot be cleanup locked.
+ *
+ * result->needs_cleanup is set when a DEAD tuple has XIDs or MultiXactIds
+ * that must be removed before the relation's freeze cutoffs can advance.
+ */
+void
+heap_page_freeze_only(HeapPageFreezeParams *params,
+					  HeapPageFreezeResult *result, OffsetNumber *off_loc,
+					  TransactionId *new_relfrozen_xid,
+					  MultiXactId *new_relmin_mxid)
+{
+	Page		page = BufferGetPage(params->buffer);
+	HeapTupleData tuple;
+	FreezeState state;
+	TransactionId dead_relfrozen_xid = *new_relfrozen_xid;
+	MultiXactId dead_relmin_mxid = *new_relmin_mxid;
+	TransactionId conflict_xid;
+	OffsetNumber maxoff;
+	bool		totally_frozen;
+	int8		htsv[MaxHeapTuplesPerPage + 1];
+	enum
+	{
+		HEAPTUPLE_NONE = -1
+	};
+
+	Assert(BufferIsLockedByMeInMode(params->buffer, BUFFER_LOCK_EXCLUSIVE));
+	Assert(params->cutoffs != NULL);
+	Assert(TransactionIdIsValid(*new_relfrozen_xid));
+	Assert(MultiXactIdIsValid(*new_relmin_mxid));
+
+	MemSet(result, 0, sizeof(*result));
+	tuple.t_tableOid = RelationGetRelid(params->relation);
+	maxoff = PageGetMaxOffsetNumber(page);
+
+	/*
+	 * Classify line pointers and determine tuple visibility before preparing
+	 * any freeze plans.  Save the results, since checking visibility twice
+	 * can give different answers.  This avoids creating a MultiXactId when an
+	 * old DEAD tuple requires a later cleanup-lock pass.
+	 */
+	for (OffsetNumber offnum = maxoff;
+		 offnum >= FirstOffsetNumber;
+		 offnum = OffsetNumberPrev(offnum))
+	{
+		ItemId		itemid = PageGetItemId(page, offnum);
+		HeapTupleHeader tupleheader;
+
+		*off_loc = offnum;
+		htsv[offnum] = HEAPTUPLE_NONE;
+		if (!ItemIdIsUsed(itemid))
+			continue;
+		if (ItemIdIsDead(itemid))
+		{
+			result->deadoffsets[result->lpdead_items++] = offnum;
+			continue;
+		}
+		if (ItemIdIsRedirected(itemid))
+		{
+			result->hastup = true;
+			continue;
+		}
+
+		Assert(ItemIdIsNormal(itemid));
+		result->hastup = true;
+		tupleheader = (HeapTupleHeader) PageGetItem(page, itemid);
+		tuple.t_data = tupleheader;
+		tuple.t_len = ItemIdGetLength(itemid);
+		ItemPointerSet(&tuple.t_self,
+					   BufferGetBlockNumber(params->buffer), offnum);
+
+		htsv[offnum] = heap_tuple_satisfies_vacuum(&tuple, params->buffer,
+												   params->vistest, params->cutoffs);
+		if (htsv[offnum] == HEAPTUPLE_DEAD &&
+			heap_tuple_should_freeze(tupleheader, params->cutoffs,
+									 &dead_relfrozen_xid,
+									 &dead_relmin_mxid))
+		{
+			result->needs_cleanup = true;
+			*off_loc = InvalidOffsetNumber;
+			return;
+		}
+	}
+
+	/*
+	 * No DEAD tuple requires a cleanup-lock pass.  Prepare freeze plans and
+	 * collect page statistics.
+	 */
+	heap_page_freeze_init(&state, true,
+						  *new_relfrozen_xid, *new_relmin_mxid);
+
+	for (OffsetNumber offnum = maxoff;
+		 offnum >= FirstOffsetNumber;
+		 offnum = OffsetNumberPrev(offnum))
+	{
+		ItemId		itemid;
+		HeapTupleHeader tupleheader;
+
+		/* Non-normal line pointers do not need freezing. */
+		if (htsv[offnum] == HEAPTUPLE_NONE)
+			continue;
+
+		/*
+		 * Leave DEAD tuples for a later cleanup-lock pass, avoiding a
+		 * separate freeze WAL record before they are removed.
+		 */
+		if (htsv[offnum] == HEAPTUPLE_DEAD)
+		{
+			result->dead_tuples++;
+			continue;
+		}
+
+		*off_loc = offnum;
+		switch (htsv[offnum])
+		{
+			case HEAPTUPLE_DELETE_IN_PROGRESS:
+			case HEAPTUPLE_LIVE:
+				result->live_tuples++;
+				break;
+
+			case HEAPTUPLE_RECENTLY_DEAD:
+				result->recently_dead_tuples++;
+				break;
+
+			case HEAPTUPLE_INSERT_IN_PROGRESS:
+				break;
+
+			default:
+				elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
+				break;
+		}
+
+		itemid = PageGetItemId(page, offnum);
+		tupleheader = (HeapTupleHeader) PageGetItem(page, itemid);
+
+		if (heap_prepare_freeze_tuple(tupleheader, params->cutoffs,
+									  &state.pagefrz,
+									  &state.frozen[state.nfrozen],
+									  &totally_frozen))
+			state.frozen[state.nfrozen++].offset = offnum;
+	}
+
+	*off_loc = InvalidOffsetNumber;
+
+	if (state.nfrozen > 0)
+	{
+		heap_pre_freeze_checks(params->buffer, state.frozen, state.nfrozen);
+		Assert(TransactionIdPrecedes(state.pagefrz.FreezePageConflictXid,
+									 params->cutoffs->OldestXmin));
+		conflict_xid = state.pagefrz.FreezePageConflictXid;
+
+		START_CRIT_SECTION();
+
+		heap_freeze_prepared_tuples(params->buffer, state.frozen,
+									state.nfrozen);
+		MarkBufferDirty(params->buffer);
+
+		if (RelationNeedsWAL(params->relation))
+			log_heap_prune_and_freeze(params->relation, params->buffer,
+									  InvalidBuffer, 0, conflict_xid,
+									  false, PRUNE_VACUUM_SCAN,
+									  state.frozen, state.nfrozen,
+									  NULL, 0, NULL, 0, NULL, 0);
+
+		END_CRIT_SECTION();
+	}
+
+	result->nfrozen = state.nfrozen;
+	heap_page_freeze_update_relstats(&state,
+									 new_relfrozen_xid, new_relmin_mxid);
+
+	if (result->dead_tuples > 0)
+	{
+		if (TransactionIdPrecedes(dead_relfrozen_xid, *new_relfrozen_xid))
+			*new_relfrozen_xid = dead_relfrozen_xid;
+		if (MultiXactIdPrecedes(dead_relmin_mxid, *new_relmin_mxid))
+			*new_relmin_mxid = dead_relmin_mxid;
+	}
+}
+
 /* Perform visibility checks shared by pruning and freezing. */
 static HTSV_Result
 heap_tuple_satisfies_vacuum(HeapTuple tup,
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 063ef2208de..76844dd9576 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -437,6 +437,9 @@ static int	lazy_scan_prune(LVRelState *vacrel, Buffer buf,
 							BlockNumber blkno, Page page,
 							Buffer vmbuffer,
 							bool *has_lpdead_items, bool *vm_page_frozen);
+static bool lazy_scan_freeze(LVRelState *vacrel, Buffer buf,
+							 BlockNumber blkno, Page page,
+							 bool *has_lpdead_items);
 static bool lazy_scan_noprune(LVRelState *vacrel, Buffer buf,
 							  BlockNumber blkno, Page page,
 							  bool *has_lpdead_items);
@@ -1456,13 +1459,22 @@ lazy_scan_heap(LVRelState *vacrel)
 			!lazy_scan_noprune(vacrel, buf, blkno, page, &has_lpdead_items))
 		{
 			/*
-			 * lazy_scan_noprune could not do all required processing.  Wait
-			 * for a cleanup lock, and call lazy_scan_prune in the usual way.
+			 * An aggressive VACUUM needs to freeze this page. First try to
+			 * freeze tuple headers with an ordinary exclusive lock. This can
+			 * make progress on pages pinned by row-lock waiters. Fall back to
+			 * the usual cleanup-lock path only when a dead tuple prevents the
+			 * relation freeze cutoffs from advancing.
 			 */
 			Assert(vacrel->aggressive);
 			LockBuffer(buf, BUFFER_LOCK_UNLOCK);
-			LockBufferForCleanup(buf);
-			got_cleanup_lock = true;
+			LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+			if (!lazy_scan_freeze(vacrel, buf, blkno, page,
+								  &has_lpdead_items))
+			{
+				LockBuffer(buf, BUFFER_LOCK_UNLOCK);
+				LockBufferForCleanup(buf);
+				got_cleanup_lock = true;
+			}
 		}
 
 		/*
@@ -2147,6 +2159,75 @@ lazy_scan_prune(LVRelState *vacrel,
 	return presult.ndeleted;
 }
 
+/*
+ * lazy_scan_freeze() -- freeze tuple headers without physical pruning.
+ *
+ * Caller must hold a pin and an ordinary exclusive lock on the buffer.
+ * Returns false when a DEAD tuple prevents advancing the relation's freeze
+ * cutoffs without a cleanup-lock pass.
+ */
+static bool
+lazy_scan_freeze(LVRelState *vacrel,
+				 Buffer buf,
+				 BlockNumber blkno,
+				 Page page,
+				 bool *has_lpdead_items)
+{
+	HeapPageFreezeParams params;
+	HeapPageFreezeResult result;
+
+	Assert(BufferGetBlockNumber(buf) == blkno);
+	Assert(page == BufferGetPage(buf));
+
+	params.relation = vacrel->rel;
+	params.buffer = buf;
+	params.vistest = vacrel->vistest;
+	params.cutoffs = &vacrel->cutoffs;
+
+	heap_page_freeze_only(&params, &result,
+						  &vacrel->offnum,
+						  &vacrel->NewRelfrozenXid,
+						  &vacrel->NewRelminMxid);
+
+	if (result.needs_cleanup)
+		return false;
+
+	if (result.nfrozen > 0)
+		vacrel->new_frozen_tuple_pages++;
+
+	if (result.lpdead_items > 0)
+	{
+		if (vacrel->nindexes == 0)
+		{
+			result.hastup = true;
+			result.dead_tuples += result.lpdead_items;
+		}
+		else
+		{
+			vacrel->lpdead_item_pages++;
+			qsort(result.deadoffsets, result.lpdead_items,
+				  sizeof(OffsetNumber), cmpOffsetNumbers);
+			dead_items_add(vacrel, blkno, result.deadoffsets,
+						   result.lpdead_items);
+			vacrel->lpdead_items += result.lpdead_items;
+		}
+	}
+
+	vacrel->tuples_frozen += result.nfrozen;
+	vacrel->live_tuples += result.live_tuples;
+	vacrel->recently_dead_tuples += result.recently_dead_tuples;
+	vacrel->missed_dead_tuples += result.dead_tuples;
+	if (result.dead_tuples > 0)
+		vacrel->missed_dead_pages++;
+
+	if (result.hastup)
+		vacrel->nonempty_pages = blkno + 1;
+
+	*has_lpdead_items = (result.lpdead_items > 0);
+
+	return true;
+}
+
 /*
  *	lazy_scan_noprune() -- lazy_scan_prune() without pruning or freezing
  *
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 5176478c295..f389373e913 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -337,6 +337,33 @@ typedef struct PruneFreezeResult
 	OffsetNumber deadoffsets[MaxHeapTuplesPerPage];
 } PruneFreezeResult;
 
+/*
+ * Input parameters for pure freezing.
+ */
+typedef struct HeapPageFreezeParams
+{
+	Relation	relation;
+	Buffer		buffer;
+	GlobalVisState *vistest;
+	VacuumCutoffs *cutoffs;
+} HeapPageFreezeParams;
+
+/*
+ * Per-page result from pure freezing.
+ */
+typedef struct HeapPageFreezeResult
+{
+	int			nfrozen;		/* Number of tuples frozen on the page */
+	int			live_tuples;	/* Number of live tuples on the page */
+	int			recently_dead_tuples;	/* Number of recently dead tuples */
+	int			dead_tuples;	/* Number of DEAD tuples left unpruned */
+	int			lpdead_items;	/* Number of existing LP_DEAD items */
+	bool		hastup;			/* Page prevents relation truncation */
+	bool		needs_cleanup;	/* DEAD tuple requires pruning before
+								 * advancing freeze cutoffs */
+	OffsetNumber deadoffsets[MaxHeapTuplesPerPage];
+} HeapPageFreezeResult;
+
 
 /* ----------------
  *		function prototypes for heap access method
@@ -449,6 +476,11 @@ extern void heap_page_prune_and_freeze(PruneFreezeParams *params,
 									   OffsetNumber *off_loc,
 									   TransactionId *new_relfrozen_xid,
 									   MultiXactId *new_relmin_mxid);
+extern void heap_page_freeze_only(HeapPageFreezeParams *params,
+								  HeapPageFreezeResult *result,
+								  OffsetNumber *off_loc,
+								  TransactionId *new_relfrozen_xid,
+								  MultiXactId *new_relmin_mxid);
 extern void heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
 									OffsetNumber *redirected, int nredirected,
 									OffsetNumber *nowdead, int ndead,
diff --git a/src/test/isolation/expected/vacuum-no-cleanup-lock.out b/src/test/isolation/expected/vacuum-no-cleanup-lock.out
index f7bc93e8f1c..76dd4070659 100644
--- a/src/test/isolation/expected/vacuum-no-cleanup-lock.out
+++ b/src/test/isolation/expected/vacuum-no-cleanup-lock.out
@@ -147,6 +147,63 @@ step pinholder_commit:
   COMMIT;
 
 
+starting permutation: dml_insert dml_insert dml_insert dml_insert dml_insert dml_insert dml_insert dml_insert vacuumer_remember_horizon pinholder_cursor vacuumer_aggressive_vacuum vacuumer_horizon_is_advanced pinholder_commit
+step dml_insert: 
+  INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
+
+step dml_insert: 
+  INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
+
+step dml_insert: 
+  INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
+
+step dml_insert: 
+  INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
+
+step dml_insert: 
+  INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
+
+step dml_insert: 
+  INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
+
+step dml_insert: 
+  INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
+
+step dml_insert: 
+  INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
+
+step vacuumer_remember_horizon: 
+  CREATE TEMP TABLE old_horizon AS
+  SELECT relfrozenxid FROM pg_class
+  WHERE oid = 'smalltbl'::regclass;
+
+step pinholder_cursor: 
+  BEGIN;
+  DECLARE c1 CURSOR FOR SELECT 1 AS dummy FROM smalltbl;
+  FETCH NEXT FROM c1;
+
+dummy
+-----
+    1
+(1 row)
+
+step vacuumer_aggressive_vacuum: 
+  VACUUM (FREEZE) smalltbl;
+
+step vacuumer_horizon_is_advanced: 
+  SELECT age(c.relfrozenxid) < age(o.relfrozenxid) AS advanced
+  FROM pg_class c CROSS JOIN old_horizon o
+  WHERE c.oid = 'smalltbl'::regclass;
+
+advanced
+--------
+t       
+(1 row)
+
+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
 step dml_begin: BEGIN;
 step dml_other_begin: BEGIN;
diff --git a/src/test/isolation/specs/vacuum-no-cleanup-lock.spec b/src/test/isolation/specs/vacuum-no-cleanup-lock.spec
index 05fd280f6a8..97bda1faac4 100644
--- a/src/test/isolation/specs/vacuum-no-cleanup-lock.spec
+++ b/src/test/isolation/specs/vacuum-no-cleanup-lock.spec
@@ -67,10 +67,26 @@ step vacuumer_nonaggressive_vacuum
 {
   VACUUM smalltbl;
 }
+step vacuumer_aggressive_vacuum
+{
+  VACUUM (FREEZE) smalltbl;
+}
 step vacuumer_pg_class_stats
 {
   SELECT relpages, reltuples FROM pg_class WHERE oid = 'smalltbl'::regclass;
 }
+step vacuumer_remember_horizon
+{
+  CREATE TEMP TABLE old_horizon AS
+  SELECT relfrozenxid FROM pg_class
+  WHERE oid = 'smalltbl'::regclass;
+}
+step vacuumer_horizon_is_advanced
+{
+  SELECT age(c.relfrozenxid) < age(o.relfrozenxid) AS advanced
+  FROM pg_class c CROSS JOIN old_horizon o
+  WHERE c.oid = 'smalltbl'::regclass;
+}
 
 # Test VACUUM's reltuples counting mechanism.
 #
@@ -123,6 +139,24 @@ permutation
     vacuumer_pg_class_stats
     pinholder_commit  # order doesn't matter
 
+# Test that aggressive VACUUM can freeze an old tuple without waiting for a
+# cleanup lock.  The cursor keeps a pin on the only heap page until after the
+# freeze and check complete.
+permutation
+    dml_insert
+    dml_insert
+    dml_insert
+    dml_insert
+    dml_insert
+    dml_insert
+    dml_insert
+    dml_insert
+    vacuumer_remember_horizon
+    pinholder_cursor
+    vacuumer_aggressive_vacuum
+    vacuumer_horizon_is_advanced
+    pinholder_commit
+
 # Test VACUUM's mechanism for skipping MultiXact freezing.
 #
 # This provides test coverage for code paths that are only hit when we need to
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d419c9ea401..5aecd00497c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1268,6 +1268,8 @@ HeadlineWordEntry
 HeapCheckContext
 HeapCheckReadStreamData
 HeapPageFreeze
+HeapPageFreezeParams
+HeapPageFreezeResult
 HeapScanDesc
 HeapScanDescData
 HeapTuple
-- 
2.43.7

