From 822591ad22b93dc0502aa68982bbcc600b5019be Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Fri, 11 Sep 2026 12:48:29 -0400
Subject: [PATCH v1] Update FSM in COPY FREEZE replay

d96f87332b3 folded COPY FREEZE visibility map updates into
XLOG_HEAP2_MULTI_INSERT records. Unlike the former XLOG_HEAP2_VISIBLE
redo path, multi-insert redo only updated the FSM when the record did
not contain a heap page FPI and the heap page had less than 20% free
space.

This can leave all-frozen pages with missing or stale FSM entries on a
standby. After promotion, vacuum will skip those pages, preventing
their free space from being discovered.

When a multi-insert record sets the page all-frozen, record the heap
page's free space unconditionally.
---
 src/backend/access/heap/heapam_xlog.c | 46 +++++++++++++++++++--------
 1 file changed, 32 insertions(+), 14 deletions(-)

diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.c
index 7a7bc7ea740..440b3b8546f 100644
--- a/src/backend/access/heap/heapam_xlog.c
+++ b/src/backend/access/heap/heapam_xlog.c
@@ -637,8 +637,6 @@ heap_xlog_multi_insert(XLogReaderState *record)
 		if (tupdata != endptr)
 			elog(PANIC, "total tuple length mismatch");
 
-		freespace = PageGetHeapFreeSpace(page); /* needed to update FSM below */
-
 		PageSetLSN(page, lsn);
 
 		if (xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED)
@@ -660,9 +658,41 @@ heap_xlog_multi_insert(XLogReaderState *record)
 
 		MarkBufferDirty(buffer);
 	}
+
 	if (BufferIsValid(buffer))
+	{
+		/*
+		 * If we are marking the page all-frozen or the page is running low on
+		 * free space, update the FSM as well. Arbitrarily, our definition of
+		 * "low" is less than 20%. We can't do much better than that without
+		 * knowing the fill-factor for the table.
+		 *
+		 * XXX: Unless setting the page all-frozen, we don't do this if the
+		 * page was restored from full page image. We don't bother to update
+		 * the FSM in that case, it doesn't need to be totally accurate
+		 * anyway.
+		 *
+		 * If setting the page all-frozen, we update the FSM regardless since,
+		 * once frozen, we lose the chance to update it during vacuum after
+		 * promotion. See comment in heap_xlog_prune_freeze() for details.
+		 */
+		bool		update_fsm = false;
+
+		if (xlrec->flags & XLH_INSERT_ALL_FROZEN_SET ||
+			action == BLK_NEEDS_REDO)
+		{
+			freespace = PageGetHeapFreeSpace(BufferGetPage(buffer));
+			if (xlrec->flags & XLH_INSERT_ALL_FROZEN_SET ||
+				freespace < BLCKSZ / 5)
+				update_fsm = true;
+		}
+
 		UnlockReleaseBuffer(buffer);
 
+		if (update_fsm)
+			XLogRecordPageWithFreeSpace(rlocator, blkno, freespace);
+	}
+
 	buffer = InvalidBuffer;
 
 	/*
@@ -709,18 +739,6 @@ heap_xlog_multi_insert(XLogReaderState *record)
 
 	if (BufferIsValid(vmbuffer))
 		UnlockReleaseBuffer(vmbuffer);
-
-	/*
-	 * If the page is running low on free space, update the FSM as well.
-	 * Arbitrarily, our definition of "low" is less than 20%. We can't do much
-	 * better than that without knowing the fill-factor for the table.
-	 *
-	 * XXX: Don't do this if the page was restored from full page image. We
-	 * don't bother to update the FSM in that case, it doesn't need to be
-	 * totally accurate anyway.
-	 */
-	if (action == BLK_NEEDS_REDO && freespace < BLCKSZ / 5)
-		XLogRecordPageWithFreeSpace(rlocator, blkno, freespace);
 }
 
 /*
-- 
2.43.0

