From 8b527ff196b8752809b92f7073bb7624d2329170 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Thu, 29 May 2025 10:39:35 -0400
Subject: [PATCH] Calculate freeze conflict horizon with final all_frozen value

In phase I of heap vacuuming, while determining whether or not to freeze
tuples on a page, lazy_scan_prune() ignores LP_DEAD items which it
assumes will be set LP_UNUSED and removed by the phase III of vacuum.

That means the local value of prunestate->all_frozen it uses may be true
even when dead items are present. In this case, however, before
returning control to lazy_scan_heap() lazy_scan_prune() must unset
prunsteate->all_visible to avoid lazy_scan_haep() incorrectly updating
the visibility map for that heap page.

The problem was that lazy_scan_prune() calculated the snapshot conflict
horizon for the freeze record using the contingent value of
prunestate->all_frozen. That could result in an overly aggressive
snapshot conflict horizon in the case that some tuples were frozen but
the whole page was not able to be set all-frozen. This would not affect
correctness -- only potentially cancel queries unnecessarily on the
standby.

Unset this sooner to correctly set the snapshot conflict horizon in the
freeze record. This is the minimal change to achieve this affect in
backbranches, however, the value of all_frozen is not corrected before
returning to lazy_scan_prune(). That doesn't affect correctness since it
is not used without all_visible. It seemed wiser to do the smallest
possible fix.
---
 src/backend/access/heap/vacuumlazy.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 9fa88960ada..8c3605eff1f 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -1833,7 +1833,7 @@ retry:
 			 * once we're done with it.  Otherwise we generate a conservative
 			 * cutoff by stepping back from OldestXmin.
 			 */
-			if (prunestate->all_visible && prunestate->all_frozen)
+			if (prunestate->all_visible && prunestate->all_frozen && lpdead_items == 0)
 			{
 				/* Using same cutoff when setting VM is now unnecessary */
 				snapshotConflictHorizon = prunestate->visibility_cutoff_xid;
-- 
2.34.1

