Re: relfrozenxid may disagree with row XIDs after 1ccc1e05ae
Melanie Plageman <melanieplageman@gmail.com>
Commits
GET /api/v1/messages/:b64id/commits
the thread's linked commits as JSON, with link sources.
API reference →
-
Ensure vacuum removes all visibly dead tuples older than OldestXmin
- 06bf404cd07b 16.4 landed
- 45ce054c02b8 14.13 landed
- dc6354c67017 15.8 landed
-
Combine freezing and pruning steps in VACUUM
- 6dbb490261a6 17.0 cited
-
Handle non-chain tuples outside of heap_prune_chain()
- 6f47f6883151 17.0 cited
-
Fix false reports in pg_visibility
- e85662df44ff 17.0 cited
-
Remove retry loop in heap_page_prune().
- 1ccc1e05ae8f 17.0 cited
-
vacuumlazy.c: document vistest and OldestXmin.
- 73f6ec3d3c8d 15.0 cited
-
Deduplicate choice of horizon for a relation procarray.c.
- d9d8aa9bb9aa 15.0 cited
-
Remove tupgone special case from vacuumlazy.c.
- 8523492d4e34 14.0 cited
-
Simplify state managed by VACUUM.
- b4af70cb2103 14.0 cited
-
Recycle nbtree pages deleted during same VACUUM.
- 9dd963ae2534 14.0 cited
-
snapshot scalability: Don't compute global horizons while building snapshots.
- dc7420c2c927 14.0 cited
-
Raise error when affecting tuple moved into different partition.
- f16241bef7cc 11.0 cited
On Mon, Apr 15, 2024 at 2:52 PM Andres Freund <andres@anarazel.de> wrote:
>
> Hi,
>
> On 2024-03-29 12:05:31 -0400, Robert Haas wrote:
> > Perhaps, for some reason I'm not grokking at the moment, preventing
> > maybe_needed from retreating would be good enough to prevent trouble
> > in practice, but it doesn't appear to me to be the most principled
> > solution as of this writing.
>
> If we prevent maybe_needed from retreating below the OldestXmin value used for
> cutoff, then pruning should never be less aggressive than what's needed by
> lazy_scan_prune(). So lazy_scan_prune() shouldn't be able to encounter tuples
> that weren't considered removable in heap_page_prune(), in < 17, nor would
> heap_page_prune_and_freeze() have that issue.
>
> I think one fairly easy fix for this would be to pass OldestXmin to
> heap_page_prune[_and_freeze](), and have heap_prune_satisfies_vacuum() first
> check OldestXmin and only if that considers the tuple still running, use
> GlobalVisTestIsRemovableXid(). That continues to allow us to prune more
> aggressively, without any potential risk of IsRemoval being too conservative.
It seems to me that in all stable versions containing the retry loop
(from 8523492d4e34), the following can theoretically happen, causing
an infinite loop in lazy_scan_prune():
1) tuple's xmin and xmax are older than VacuumCutoffs->OldestXmin
2) heap_page_prune()-> heap_prune_satisfies_vacuum()->
HeapTupleSatisfiesVacuumHorizon() returns HEAPTUPLE_RECENTLY_DEAD
3) in GlobalVisTestIsRemovableXid(), dead_after is between
GlobalVisState->maybe_needed and definitely_needed, causing
GlobalVisState to be recomputed
4) GlobalVisState->maybe_needed moves backwards
5) tuple is not removable because dead_after is now newer than maybe_needed
6) in lazy_scan_prune(), HeapTupleSatisfiesVacuum() returns
HEAPTUPLE_DEAD because dead_after is older than OldestXmin
7) infinite loop
One way to fix this (as mentioned earlier in this thread) is to
backport 1ccc1e05ae because it eliminates the retry loop. In our above
example, lazy_scan_prune() reuses the tuple's HEAPTUPLE_RECENTLY_DEAD
HTSV_Result instead of recomputing it. We'd have to rule out any of
the data corruption claims about that commit to justify this, but I am
not yet convinced that 1ccc1e05ae could cause any problems with
relfrozenxid advancement.
Andres' idea of passing VacuumCutoffs->OldestXmin to heap_page_prune()
makes sense. We could add a member to PruneState, oldest_xmin, and
initialize it to InvalidTransactionId for on-access pruning and to
VacuumCutoffs->OldestXmin for vacuum. Then, in
heap_prune_satisfies_vacuum(), we could add this if branch:
if (TransactionIdPrecedes(dead_after, prstate->oldest_xmin))
to here:
- if (GlobalVisTestIsRemovableXid(prstate->vistest, dead_after))
+
+ if (TransactionIdPrecedes(dead_after, prstate->oldest_xmin))
+ res = HEAPTUPLE_DEAD;
+ else if (GlobalVisTestIsRemovableXid(prstate->vistest, dead_after))
res = HEAPTUPLE_DEAD;
else if (OldSnapshotThresholdActive())
This seems like a more targeted fix than backpatching 1ccc1e05ae.
I plan to attempt to write a test case that repros this scenario
(infinite loop on stable branch) next week.
- Melanie