From eb9403676d61c32f45139ef6559366182afd608f Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Mon, 13 Apr 2026 11:50:32 -0400
Subject: [PATCH] Update FSM when updating VM even if freespace is zero

add323da40a started updating the visibility map in the same WAL record
as pruning and freezing. This included updating the freespace map during
replay, which we've done since ab7dbd681.

add323da40a, however, conditioned doing so on there being > 0 freespace,
which differed from the previous state. The FSM is not WAL-logged and is
instead updated heuristically on standbys. In rare cases, if the FSM is
not updated while replaying inserts, there is 0 freespace, and vacuum
replay doesn't update the FSM when setting the page
all-visible/all-frozen, after the standby is promoted and runs vacuum,
it may skip those pages and then propagate overly optimistic numbers up
the FSM, causing slowness when searching for freespace for new tuples.

Fix it by always updating the FSM when replaying setting the VM.

Author: Melanie Plageman <melanieplageman@gmail.com>
Reported-by: Alexey Makhmutov <a.makhmutov@postgrespro.ru>
Discussion: https://postgr.es/m/ead2f110-c736-48f5-99e1-023dc9acbf0b%40postgrespro.ru
---
 src/backend/access/heap/heapam_xlog.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.c
index f3f419d3dc1..9ed7024e814 100644
--- a/src/backend/access/heap/heapam_xlog.c
+++ b/src/backend/access/heap/heapam_xlog.c
@@ -38,6 +38,7 @@ heap_xlog_prune_freeze(XLogReaderState *record)
 	Buffer		vmbuffer = InvalidBuffer;
 	uint8		vmflags = 0;
 	Size		freespace = 0;
+	bool		do_update_fsm = false;
 
 	XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
 	memcpy(&xlrec, maindataptr, SizeOfHeapPrune);
@@ -211,7 +212,10 @@ heap_xlog_prune_freeze(XLogReaderState *record)
 							XLHP_HAS_DEAD_ITEMS |
 							XLHP_HAS_NOW_UNUSED_ITEMS)) ||
 			(vmflags & VISIBILITYMAP_VALID_BITS))
+		{
 			freespace = PageGetHeapFreeSpace(BufferGetPage(buffer));
+			do_update_fsm = true;
+		}
 
 		/*
 		 * We want to avoid holding an exclusive lock on the heap buffer while
@@ -248,7 +252,7 @@ heap_xlog_prune_freeze(XLogReaderState *record)
 	if (BufferIsValid(vmbuffer))
 		UnlockReleaseBuffer(vmbuffer);
 
-	if (freespace > 0)
+	if (do_update_fsm)
 		XLogRecordPageWithFreeSpace(rlocator, blkno, freespace);
 }
 
-- 
2.43.0

