postgres.git / summary / log / commit / refs

commit    83c39a1f7f3f507058d5bc9f121a42a1a2494180
Author:   Melanie Plageman <melanieplageman@gmail.com>
Date:     Fri Jul 19 14:18:17 2024 +0000
First released in: 18.0, 14.13, 15.8, 16.4, 17.0

    Ensure vacuum removes all visibly dead tuples older than OldestXmin
    
    If vacuum fails to remove a tuple with xmax older than
    VacuumCutoffs->OldestXmin and younger than GlobalVisState->maybe_needed,
    it may attempt to freeze the tuple's xmax and then ERROR out in
    pre-freeze checks with "cannot freeze committed xmax".
    
    Fix this by having vacuum always remove tuples older than OldestXmin.
    
    It is possible for GlobalVisState->maybe_needed to precede OldestXmin if
    maybe_needed is forced to go backward while vacuum is running. This can
    happen if a disconnected standby with a running transaction older than
    VacuumCutoffs->OldestXmin reconnects to the primary after vacuum
    initially calculates GlobalVisState and OldestXmin.
    
    In back branches starting with 14, the first version using
    GlobalVisState, failing to remove tuples older than OldestXmin during
    pruning caused vacuum to infinitely loop in lazy_scan_prune(), as
    investigated on this [1] thread. After 1ccc1e05ae removed the retry loop
    in lazy_scan_prune() and stopped comparing tuples to OldestXmin, the
    hang could no longer happen, but we could still attempt to freeze dead
    tuples with xmax older than OldestXmin -- resulting in an ERROR.
    
    Fix this by always removing dead tuples with xmax older than
    VacuumCutoffs->OldestXmin. This is okay because the standby won't replay
    the tuple removal until the tuple is removable. Thus, the worst that can
    happen is a recovery conflict.
    
    [1] https://postgr.es/m/20240415173913.4zyyrwaftujxthf2%40awork3.anarazel.de#1b216b7768b5bd577a3d3d51bd5aadee
    
    Back-patch through 14
    
    Author: Melanie Plageman
    Reviewed-by: Peter Geoghegan, Robert Haas, Andres Freund, Heikki Linnakangas, and Noah Misch
    Discussion: https://postgr.es/m/CAAKRu_bDD7oq9ZwB2OJqub5BovMG6UjEYsoK2LVttadjEqyRGg%40mail.gmail.com


src/backend/access/heap/pruneheap.c | 23 ++++++++++++++++++++++- src/backend/access/heap/vacuumlazy.c | 14 +++++++------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 3cdfc5b7f1b..869d82ad667 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -325,6 +325,8 @@ heap_page_prune_opt(Relation relation, Buffer buffer) * * cutoffs contains the freeze cutoffs, established by VACUUM at the beginning * of vacuuming the relation. Required if HEAP_PRUNE_FREEZE option is set. + * cutoffs->OldestXmin is also used to determine if dead tuples are + * HEAPTUPLE_RECENTLY_DEAD or HEAPTUPLE_DEAD. * * presult contains output parameters needed by callers, such as the number of * tuples removed and the offsets of dead items on the page after pruning. @@ -922,8 +924,27 @@ heap_prune_satisfies_vacuum(PruneState *prstate, HeapTuple tup, Buffer buffer) if (res != HEAPTUPLE_RECENTLY_DEAD) return res; + /* + * For VACUUM, we must be sure to prune tuples with xmax older than + * OldestXmin -- a visibility cutoff determined at the beginning of + * vacuuming the relation. OldestXmin is used for freezing determination + * and we cannot freeze dead tuples' xmaxes. + */ + if (prstate->cutoffs && + TransactionIdIsValid(prstate->cutoffs->OldestXmin) && + NormalTransactionIdPrecedes(dead_after, prstate->cutoffs->OldestXmin)) + return HEAPTUPLE_DEAD; + + /* + * Determine whether or not the tuple is considered dead when compared + * with the provided GlobalVisState. On-access pruning does not provide + * VacuumCutoffs. And for vacuum, even if the tuple's xmax is not older + * than OldestXmin, GlobalVisTestIsRemovableXid() could find the row dead + * if the GlobalVisState has been updated since the beginning of vacuuming + * the relation. + */ if (GlobalVisTestIsRemovableXid(prstate->vistest, dead_after)) - res = HEAPTUPLE_DEAD; + return HEAPTUPLE_DEAD; return res; } diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 3f88cf1e8ef..abb47ae5960 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -438,13 +438,13 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, * as an upper bound on the XIDs stored in the pages we'll actually scan * (NewRelfrozenXid tracking must never be allowed to miss unfrozen XIDs). * - * Next acquire vistest, a related cutoff that's used in pruning. We - * expect vistest will always make heap_page_prune_and_freeze() remove any - * deleted tuple whose xmax is < OldestXmin. lazy_scan_prune must never - * become confused about whether a tuple should be frozen or removed. (In - * the future we might want to teach lazy_scan_prune to recompute vistest - * from time to time, to increase the number of dead tuples it can prune - * away.) + * Next acquire vistest, a related cutoff that's used in pruning. We use + * vistest in combination with OldestXmin to ensure that + * heap_page_prune_and_freeze() always removes any deleted tuple whose + * xmax is < OldestXmin. lazy_scan_prune must never become confused about + * whether a tuple should be frozen or removed. (In the future we might + * want to teach lazy_scan_prune to recompute vistest from time to time, + * to increase the number of dead tuples it can prune away.) */ vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs); vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel); [parent: 5784a493f14e]