agora inbox for pgsql-hackers@postgresql.orghelp / color / mirror / Atom feed
[PATCH v3 03/17] heap_page_prune sets all_visible and frz_conflict_horizon 7+ messages / 2 participants [nested] [flat]
* [PATCH v3 03/17] heap_page_prune sets all_visible and frz_conflict_horizon @ 2024-01-06 19:01 Melanie Plageman <melanieplageman@gmail.com> 0 siblings, 0 replies; 7+ messages in thread From: Melanie Plageman @ 2024-01-06 19:01 UTC (permalink / raw) In order to combine the prune and freeze records, we must know if the page is eligible to be opportunistically frozen before finishing pruning. Save all_visible in the PruneResult and set it to false when we see non-removable tuples which are not visible to everyone. We will also need to ensure that the snapshotConflictHorizon for the combined prune + freeze record is the more conservative of that calculated for each of pruning and freezing. Calculate the visibility_cutoff_xid for the purposes of freezing -- the newest xmin on the page -- in heap_page_prune() and save it in PruneResult.frz_conflict_horizon. --- src/backend/access/heap/pruneheap.c | 136 +++++++++++++++++++++++++-- src/backend/access/heap/vacuumlazy.c | 130 ++++++------------------- src/include/access/heapam.h | 3 + 3 files changed, 160 insertions(+), 109 deletions(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 4a2bf3dd780..42fd4a74845 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -65,8 +65,10 @@ static int heap_prune_chain(Buffer buffer, static void heap_prune_record_prunable(PruneState *prstate, TransactionId xid); static void heap_prune_record_redirect(PruneState *prstate, OffsetNumber offnum, OffsetNumber rdoffnum); -static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum); -static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum); +static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); +static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); static void heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum); static void page_verify_redirects(Page page); @@ -187,6 +189,20 @@ heap_page_prune_opt(Relation relation, Buffer buffer) } +/* + * Wrap GlobalVisTestIsRemovableXid() to handle FrozenTransactionIds when we + * are examining tuple xmins to determine if the page is all-visible during + * pruning. Old tuples may have FrozenTransactionId xmins. + */ +static inline bool +prune_freeze_xmin_is_removable(GlobalVisState *visstate, TransactionId xmin) +{ + if (xmin == FrozenTransactionId) + return true; + + return GlobalVisTestIsRemovableXid(visstate, xmin); +} + /* * Prune and repair fragmentation in the specified page. * @@ -249,6 +265,14 @@ heap_page_prune(Relation relation, Buffer buffer, presult->ndeleted = 0; presult->nnewlpdead = 0; + /* + * Keep track of whether or not the page is all_visible in case the caller + * wants to use this information to update the VM. + */ + presult->all_visible = true; + /* for recovery conflicts */ + presult->frz_conflict_horizon = InvalidTransactionId; + maxoff = PageGetMaxOffsetNumber(page); tup.t_tableOid = RelationGetRelid(prstate.rel); @@ -300,8 +324,92 @@ heap_page_prune(Relation relation, Buffer buffer, presult->htsv[offnum] = heap_prune_satisfies_vacuum(&prstate, &tup, buffer); + switch (presult->htsv[offnum]) + { + case HEAPTUPLE_DEAD: + + /* + * Deliberately delay unsetting all_visible until later during + * pruning. Removable dead tuples shouldn't preclude freezing + * the page. After finishing this first pass of tuple + * visibility checks, initialize all_visible_except_removable + * with the current value of all_visible to indicate whether + * or not the page is all visible except for dead tuples. This + * will allow us to attempt to freeze the page after pruning. + * Later during pruning, if we encounter an LP_DEAD item or + * are setting an item LP_DEAD, we will unset all_visible. As + * long as we unset it before updating the visibility map, + * this will be correct. + */ + break; + case HEAPTUPLE_LIVE: + + /* + * Is the tuple definitely visible to all transactions? + * + * NB: Like with per-tuple hint bits, we can't set the + * PD_ALL_VISIBLE flag if the inserter committed + * asynchronously. See SetHintBits for more info. Check that + * the tuple is hinted xmin-committed because of that. + */ + if (presult->all_visible) + { + TransactionId xmin; + + if (!HeapTupleHeaderXminCommitted(htup)) + { + presult->all_visible = false; + break; + } + + /* + * The inserter definitely committed. But is it old enough + * that everyone sees it as committed? + */ + xmin = HeapTupleHeaderGetXmin(htup); + if (!prune_freeze_xmin_is_removable(vistest, xmin)) + { + presult->all_visible = false; + break; + } + + /* Track newest xmin on page. */ + if (TransactionIdFollows(xmin, presult->frz_conflict_horizon) && + TransactionIdIsNormal(xmin)) + presult->frz_conflict_horizon = xmin; + } + break; + case HEAPTUPLE_RECENTLY_DEAD: + presult->all_visible = false; + break; + case HEAPTUPLE_INSERT_IN_PROGRESS: + presult->all_visible = false; + break; + case HEAPTUPLE_DELETE_IN_PROGRESS: + /* This is an expected case during concurrent vacuum */ + presult->all_visible = false; + break; + default: + elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); + break; + } } + /* + * For vacuum, if the whole page will become frozen, we consider + * opportunistically freezing tuples. Dead tuples which will be removed by + * the end of vacuuming should not preclude us from opportunistically + * freezing. We will not be able to freeze the whole page if there are + * tuples present which are not visible to everyone or if there are dead + * tuples which are not yet removable. We need all_visible to be false if + * LP_DEAD tuples remain after pruning so that we do not incorrectly + * update the visibility map or page hint bit. So, we will update + * presult->all_visible to reflect the presence of LP_DEAD items while + * pruning and keep all_visible_except_removable to permit freezing if the + * whole page will eventually become all visible after removing tuples. + */ + presult->all_visible_except_removable = presult->all_visible; + /* Scan the page */ for (offnum = FirstOffsetNumber; offnum <= maxoff; @@ -596,10 +704,14 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, /* * If the caller set mark_unused_now true, we can set dead line * pointers LP_UNUSED now. We don't increment ndeleted here since - * the LP was already marked dead. + * the LP was already marked dead. If it will not be marked + * LP_UNUSED, it will remain LP_DEAD, making the page not + * all_visible. */ if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); + else + presult->all_visible = false; break; } @@ -736,7 +848,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect the root to the correct chain member. */ if (i >= nchain) - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); else heap_prune_record_redirect(prstate, rootoffnum, chainitems[i]); } @@ -749,7 +861,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect item. We can clean up by setting the redirect item to * DEAD state or LP_UNUSED if the caller indicated. */ - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); } return ndeleted; @@ -786,13 +898,20 @@ heap_prune_record_redirect(PruneState *prstate, /* Record line pointer to be marked dead */ static void -heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { Assert(prstate->ndead < MaxHeapTuplesPerPage); prstate->nowdead[prstate->ndead] = offnum; prstate->ndead++; Assert(!prstate->marked[offnum]); prstate->marked[offnum] = true; + + /* + * Setting the line pointer LP_DEAD means the page will definitely not be + * all_visible. + */ + presult->all_visible = false; } /* @@ -802,7 +921,8 @@ heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) * pointers LP_DEAD if mark_unused_now is true. */ static void -heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { /* * If the caller set mark_unused_now to true, we can remove dead tuples @@ -813,7 +933,7 @@ heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); else - heap_prune_record_dead(prstate, offnum); + heap_prune_record_dead(prstate, offnum, presult); } /* Record line pointer to be marked unused */ diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index fe31c0125d6..f9892f4cd08 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -1373,20 +1373,6 @@ lazy_scan_new_or_empty(LVRelState *vacrel, Buffer buf, BlockNumber blkno, return false; } -/* - * Wrap GlobalVisTestIsRemovableXid() to handle FrozenTransactionIds when we - * are examining tuple xmins to determine if the page is all-visible during - * pruning. Old tuples may have FrozenTransactionId xmins. - */ -static inline bool -prune_freeze_xmin_is_removable(GlobalVisState *visstate, TransactionId xmin) -{ - if (xmin == FrozenTransactionId) - return true; - - return GlobalVisTestIsRemovableXid(visstate, xmin); -} - /* * lazy_scan_prune() -- lazy_scan_heap() pruning and freezing. * @@ -1436,9 +1422,7 @@ lazy_scan_prune(LVRelState *vacrel, recently_dead_tuples; HeapPageFreeze pagefrz; bool hastup = false; - bool all_visible, - all_frozen; - TransactionId visibility_cutoff_xid; + bool all_frozen; int64 fpi_before = pgWalUsage.wal_fpi; OffsetNumber deadoffsets[MaxHeapTuplesPerPage]; HeapTupleFreeze frozen[MaxHeapTuplesPerPage]; @@ -1479,17 +1463,16 @@ lazy_scan_prune(LVRelState *vacrel, &presult, &vacrel->offnum); /* - * We will update the VM after collecting LP_DEAD items and freezing - * tuples. Keep track of whether or not the page is all_visible and - * all_frozen and use this information to update the VM. all_visible - * implies 0 lpdead_items, but don't trust all_frozen result unless - * all_visible is also set to true. + * Now scan the page to collect LP_DEAD items and check for tuples + * requiring freezing among remaining tuples with storage. We will update + * the VM after collecting LP_DEAD items and freezing tuples. Pruning will + * have determined whether or not the page is all_visible. Keep track of + * whether or not the page is all_frozen and use this information to + * update the VM. all_visible implies lpdead_items == 0, but don't trust + * all_frozen result unless all_visible is also set to true. * - * Also keep track of the visibility cutoff xid for recovery conflicts. */ - all_visible = true; all_frozen = true; - visibility_cutoff_xid = InvalidTransactionId; /* * Now scan the page to collect LP_DEAD items and update the variables set @@ -1530,11 +1513,6 @@ lazy_scan_prune(LVRelState *vacrel, * will only happen every other VACUUM, at most. Besides, VACUUM * must treat hastup/nonempty_pages as provisional no matter how * LP_DEAD items are handled (handled here, or handled later on). - * - * Also deliberately delay unsetting all_visible until just before - * we return to lazy_scan_heap caller, as explained in full below. - * (This is another case where it's useful to anticipate that any - * LP_DEAD items will become LP_UNUSED during the ongoing VACUUM.) */ deadoffsets[lpdead_items++] = offnum; continue; @@ -1572,41 +1550,6 @@ lazy_scan_prune(LVRelState *vacrel, * what acquire_sample_rows() does. */ live_tuples++; - - /* - * Is the tuple definitely visible to all transactions? - * - * NB: Like with per-tuple hint bits, we can't set the - * PD_ALL_VISIBLE flag if the inserter committed - * asynchronously. See SetHintBits for more info. Check that - * the tuple is hinted xmin-committed because of that. - */ - if (all_visible) - { - TransactionId xmin; - - if (!HeapTupleHeaderXminCommitted(htup)) - { - all_visible = false; - break; - } - - /* - * The inserter definitely committed. But is it old enough - * that everyone sees it as committed? - */ - xmin = HeapTupleHeaderGetXmin(htup); - if (!prune_freeze_xmin_is_removable(vacrel->vistest, xmin)) - { - all_visible = false; - break; - } - - /* Track newest xmin on page. */ - if (TransactionIdFollows(xmin, visibility_cutoff_xid) && - TransactionIdIsNormal(xmin)) - visibility_cutoff_xid = xmin; - } break; case HEAPTUPLE_RECENTLY_DEAD: @@ -1616,7 +1559,6 @@ lazy_scan_prune(LVRelState *vacrel, * pruning.) */ recently_dead_tuples++; - all_visible = false; break; case HEAPTUPLE_INSERT_IN_PROGRESS: @@ -1627,16 +1569,13 @@ lazy_scan_prune(LVRelState *vacrel, * results. This assumption is a bit shaky, but it is what * acquire_sample_rows() does, so be consistent. */ - all_visible = false; break; case HEAPTUPLE_DELETE_IN_PROGRESS: - /* This is an expected case during concurrent vacuum */ - all_visible = false; /* - * Count such rows as live. As above, we assume the deleting - * transaction will commit and update the counters after we - * report. + * This an expected case during concurrent vacuum. Count such + * rows as live. As above, we assume the deleting transaction + * will commit and update the counters after we report. */ live_tuples++; break; @@ -1679,7 +1618,7 @@ lazy_scan_prune(LVRelState *vacrel, * page all-frozen afterwards (might not happen until final heap pass). */ if (pagefrz.freeze_required || tuples_frozen == 0 || - (all_visible && all_frozen && + (presult.all_visible_except_removable && all_frozen && fpi_before != pgWalUsage.wal_fpi)) { /* @@ -1712,16 +1651,16 @@ lazy_scan_prune(LVRelState *vacrel, vacrel->frozen_pages++; /* - * We can use visibility_cutoff_xid as our cutoff for conflicts + * We can use frz_conflict_horizon as our cutoff for conflicts * when the whole page is eligible to become all-frozen in the VM * once we're done with it. Otherwise we generate a conservative * cutoff by stepping back from OldestXmin. */ - if (all_visible && all_frozen) + if (presult.all_visible_except_removable && all_frozen) { /* Using same cutoff when setting VM is now unnecessary */ - snapshotConflictHorizon = visibility_cutoff_xid; - visibility_cutoff_xid = InvalidTransactionId; + snapshotConflictHorizon = presult.frz_conflict_horizon; + presult.frz_conflict_horizon = InvalidTransactionId; } else { @@ -1757,17 +1696,19 @@ lazy_scan_prune(LVRelState *vacrel, */ #ifdef USE_ASSERT_CHECKING /* Note that all_frozen value does not matter when !all_visible */ - if (all_visible && lpdead_items == 0) + if (presult.all_visible) { TransactionId debug_cutoff; bool debug_all_frozen; + Assert(lpdead_items == 0); + if (!heap_page_is_all_visible(vacrel, buf, &debug_cutoff, &debug_all_frozen)) Assert(false); Assert(!TransactionIdIsValid(debug_cutoff) || - debug_cutoff == visibility_cutoff_xid); + debug_cutoff == presult.frz_conflict_horizon); } #endif @@ -1792,19 +1733,6 @@ lazy_scan_prune(LVRelState *vacrel, Assert(dead_items->num_items <= dead_items->max_items); pgstat_progress_update_param(PROGRESS_VACUUM_NUM_DEAD_TUPLES, dead_items->num_items); - - /* - * It was convenient to ignore LP_DEAD items in all_visible earlier on - * to make the choice of whether or not to freeze the page unaffected - * by the short-term presence of LP_DEAD items. These LP_DEAD items - * were effectively assumed to be LP_UNUSED items in the making. It - * doesn't matter which heap pass (initial pass or final pass) ends up - * setting the page all-frozen, as long as the ongoing VACUUM does it. - * - * Now that freezing has been finalized, unset all_visible. It needs - * to reflect the present state of things, as expected by our caller. - */ - all_visible = false; } /* Finally, add page-local counts to whole-VACUUM counts */ @@ -1821,20 +1749,20 @@ lazy_scan_prune(LVRelState *vacrel, /* Did we find LP_DEAD items? */ *has_lpdead_items = (lpdead_items > 0); - Assert(!all_visible || !(*has_lpdead_items)); + Assert(!presult.all_visible || !(*has_lpdead_items)); /* * Handle setting visibility map bit based on information from the VM (as * of last heap_vac_scan_next_block() call), and from all_visible and * all_frozen variables */ - if (!all_visible_according_to_vm && all_visible) + if (!all_visible_according_to_vm && presult.all_visible) { uint8 flags = VISIBILITYMAP_ALL_VISIBLE; if (all_frozen) { - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); flags |= VISIBILITYMAP_ALL_FROZEN; } @@ -1854,7 +1782,7 @@ lazy_scan_prune(LVRelState *vacrel, PageSetAllVisible(page); MarkBufferDirty(buf); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, - vmbuffer, visibility_cutoff_xid, + vmbuffer, presult.frz_conflict_horizon, flags); } @@ -1902,7 +1830,7 @@ lazy_scan_prune(LVRelState *vacrel, * it as all-frozen. Note that all_frozen is only valid if all_visible is * true, so we must check both all_visible and all_frozen. */ - else if (all_visible_according_to_vm && all_visible && + else if (all_visible_according_to_vm && presult.all_visible && all_frozen && !VM_ALL_FROZEN(vacrel->rel, blkno, &vmbuffer)) { /* @@ -1919,11 +1847,11 @@ lazy_scan_prune(LVRelState *vacrel, /* * Set the page all-frozen (and all-visible) in the VM. * - * We can pass InvalidTransactionId as our visibility_cutoff_xid, - * since a snapshotConflictHorizon sufficient to make everything safe - * for REDO was logged when the page's tuples were frozen. + * We can pass InvalidTransactionId as our frz_conflict_horizon, since + * a snapshotConflictHorizon sufficient to make everything safe for + * REDO was logged when the page's tuples were frozen. */ - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, vmbuffer, InvalidTransactionId, VISIBILITYMAP_ALL_VISIBLE | diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 4b133f68593..d8e65ae7e35 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -198,6 +198,9 @@ typedef struct PruneResult { int ndeleted; /* Number of tuples deleted from the page */ int nnewlpdead; /* Number of newly LP_DEAD items */ + bool all_visible; /* Whether or not the page is all visible */ + bool all_visible_except_removable; + TransactionId frz_conflict_horizon; /* Newest xmin on the page */ /* * Tuple visibility is only computed once for each tuple, for correctness -- 2.40.1 --racicctn4wry6xe5 Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v3-0004-Add-reference-to-VacuumCutoffs-in-HeapPageFreeze.patch" ^ permalink raw reply [nested|flat] 7+ messages in thread
* [PATCH v2 03/17] heap_page_prune sets all_visible and frz_conflict_horizon @ 2024-01-06 19:01 Melanie Plageman <melanieplageman@gmail.com> 0 siblings, 0 replies; 7+ messages in thread From: Melanie Plageman @ 2024-01-06 19:01 UTC (permalink / raw) In order to combine the prune and freeze records, we must know if the page is eligible to be opportunistically frozen before finishing pruning. Save all_visible in the PruneResult and set it to false when we see non-removable tuples which are not visible to everyone. We will also need to ensure that the snapshotConflictHorizon for the combined prune + freeze record is the more conservative of that calculated for each of pruning and freezing. Calculate the visibility_cutoff_xid for the purposes of freezing -- the newest xmin on the page -- in heap_page_prune() and save it in PruneResult.frz_conflict_horizon. --- src/backend/access/heap/pruneheap.c | 122 +++++++++++++++++++++++++-- src/backend/access/heap/vacuumlazy.c | 116 +++++++------------------ src/include/access/heapam.h | 3 + 3 files changed, 146 insertions(+), 95 deletions(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 4600ee53751..b3a7ce06699 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -65,8 +65,10 @@ static int heap_prune_chain(Buffer buffer, static void heap_prune_record_prunable(PruneState *prstate, TransactionId xid); static void heap_prune_record_redirect(PruneState *prstate, OffsetNumber offnum, OffsetNumber rdoffnum); -static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum); -static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum); +static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); +static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); static void heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum); static void page_verify_redirects(Page page); @@ -249,6 +251,14 @@ heap_page_prune(Relation relation, Buffer buffer, presult->ndeleted = 0; presult->nnewlpdead = 0; + /* + * Keep track of whether or not the page is all_visible in case the caller + * wants to use this information to update the VM. + */ + presult->all_visible = true; + /* for recovery conflicts */ + presult->frz_conflict_horizon = InvalidTransactionId; + maxoff = PageGetMaxOffsetNumber(page); tup.t_tableOid = RelationGetRelid(prstate.rel); @@ -300,8 +310,92 @@ heap_page_prune(Relation relation, Buffer buffer, presult->htsv[offnum] = heap_prune_satisfies_vacuum(&prstate, &tup, buffer); + switch (presult->htsv[offnum]) + { + case HEAPTUPLE_DEAD: + + /* + * Deliberately delay unsetting all_visible until later during + * pruning. Removable dead tuples shouldn't preclude freezing + * the page. After finishing this first pass of tuple + * visibility checks, initialize all_visible_except_removable + * with the current value of all_visible to indicate whether + * or not the page is all visible except for dead tuples. This + * will allow us to attempt to freeze the page after pruning. + * Later during pruning, if we encounter an LP_DEAD item or + * are setting an item LP_DEAD, we will unset all_visible. As + * long as we unset it before updating the visibility map, + * this will be correct. + */ + break; + case HEAPTUPLE_LIVE: + + /* + * Is the tuple definitely visible to all transactions? + * + * NB: Like with per-tuple hint bits, we can't set the + * PD_ALL_VISIBLE flag if the inserter committed + * asynchronously. See SetHintBits for more info. Check that + * the tuple is hinted xmin-committed because of that. + */ + if (presult->all_visible) + { + TransactionId xmin; + + if (!HeapTupleHeaderXminCommitted(htup)) + { + presult->all_visible = false; + break; + } + + /* + * The inserter definitely committed. But is it old enough + * that everyone sees it as committed? + */ + xmin = HeapTupleHeaderGetXmin(htup); + if (!GlobalVisTestIsRemovableXid(vistest, xmin)) + { + presult->all_visible = false; + break; + } + + /* Track newest xmin on page. */ + if (TransactionIdFollows(xmin, presult->frz_conflict_horizon) && + TransactionIdIsNormal(xmin)) + presult->frz_conflict_horizon = xmin; + } + break; + case HEAPTUPLE_RECENTLY_DEAD: + presult->all_visible = false; + break; + case HEAPTUPLE_INSERT_IN_PROGRESS: + presult->all_visible = false; + break; + case HEAPTUPLE_DELETE_IN_PROGRESS: + /* This is an expected case during concurrent vacuum */ + presult->all_visible = false; + break; + default: + elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); + break; + } } + /* + * For vacuum, if the whole page will become frozen, we consider + * opportunistically freezing tuples. Dead tuples which will be removed by + * the end of vacuuming should not preclude us from opportunistically + * freezing. We will not be able to freeze the whole page if there are + * tuples present which are not visible to everyone or if there are dead + * tuples which are not yet removable. We need all_visible to be false if + * LP_DEAD tuples remain after pruning so that we do not incorrectly + * update the visibility map or page hint bit. So, we will update + * presult->all_visible to reflect the presence of LP_DEAD items while + * pruning and keep all_visible_except_removable to permit freezing if the + * whole page will eventually become all visible after removing tuples. + */ + presult->all_visible_except_removable = presult->all_visible; + /* Scan the page */ for (offnum = FirstOffsetNumber; offnum <= maxoff; @@ -596,10 +690,14 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, /* * If the caller set mark_unused_now true, we can set dead line * pointers LP_UNUSED now. We don't increment ndeleted here since - * the LP was already marked dead. + * the LP was already marked dead. If it will not be marked + * LP_UNUSED, it will remain LP_DEAD, making the page not + * all_visible. */ if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); + else + presult->all_visible = false; break; } @@ -736,7 +834,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect the root to the correct chain member. */ if (i >= nchain) - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); else heap_prune_record_redirect(prstate, rootoffnum, chainitems[i]); } @@ -749,7 +847,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect item. We can clean up by setting the redirect item to * DEAD state or LP_UNUSED if the caller indicated. */ - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); } return ndeleted; @@ -786,13 +884,20 @@ heap_prune_record_redirect(PruneState *prstate, /* Record line pointer to be marked dead */ static void -heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { Assert(prstate->ndead < MaxHeapTuplesPerPage); prstate->nowdead[prstate->ndead] = offnum; prstate->ndead++; Assert(!prstate->marked[offnum]); prstate->marked[offnum] = true; + + /* + * Setting the line pointer LP_DEAD means the page will definitely not be + * all_visible. + */ + presult->all_visible = false; } /* @@ -802,7 +907,8 @@ heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) * pointers LP_DEAD if mark_unused_now is true. */ static void -heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { /* * If the caller set mark_unused_now to true, we can remove dead tuples @@ -813,7 +919,7 @@ heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); else - heap_prune_record_dead(prstate, offnum); + heap_prune_record_dead(prstate, offnum, presult); } /* Record line pointer to be marked unused */ diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index d1efd885c88..f9892f4cd08 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -1422,9 +1422,7 @@ lazy_scan_prune(LVRelState *vacrel, recently_dead_tuples; HeapPageFreeze pagefrz; bool hastup = false; - bool all_visible, - all_frozen; - TransactionId visibility_cutoff_xid; + bool all_frozen; int64 fpi_before = pgWalUsage.wal_fpi; OffsetNumber deadoffsets[MaxHeapTuplesPerPage]; HeapTupleFreeze frozen[MaxHeapTuplesPerPage]; @@ -1465,17 +1463,16 @@ lazy_scan_prune(LVRelState *vacrel, &presult, &vacrel->offnum); /* - * We will update the VM after collecting LP_DEAD items and freezing - * tuples. Keep track of whether or not the page is all_visible and - * all_frozen and use this information to update the VM. all_visible - * implies 0 lpdead_items, but don't trust all_frozen result unless - * all_visible is also set to true. + * Now scan the page to collect LP_DEAD items and check for tuples + * requiring freezing among remaining tuples with storage. We will update + * the VM after collecting LP_DEAD items and freezing tuples. Pruning will + * have determined whether or not the page is all_visible. Keep track of + * whether or not the page is all_frozen and use this information to + * update the VM. all_visible implies lpdead_items == 0, but don't trust + * all_frozen result unless all_visible is also set to true. * - * Also keep track of the visibility cutoff xid for recovery conflicts. */ - all_visible = true; all_frozen = true; - visibility_cutoff_xid = InvalidTransactionId; /* * Now scan the page to collect LP_DEAD items and update the variables set @@ -1516,11 +1513,6 @@ lazy_scan_prune(LVRelState *vacrel, * will only happen every other VACUUM, at most. Besides, VACUUM * must treat hastup/nonempty_pages as provisional no matter how * LP_DEAD items are handled (handled here, or handled later on). - * - * Also deliberately delay unsetting all_visible until just before - * we return to lazy_scan_heap caller, as explained in full below. - * (This is another case where it's useful to anticipate that any - * LP_DEAD items will become LP_UNUSED during the ongoing VACUUM.) */ deadoffsets[lpdead_items++] = offnum; continue; @@ -1558,41 +1550,6 @@ lazy_scan_prune(LVRelState *vacrel, * what acquire_sample_rows() does. */ live_tuples++; - - /* - * Is the tuple definitely visible to all transactions? - * - * NB: Like with per-tuple hint bits, we can't set the - * PD_ALL_VISIBLE flag if the inserter committed - * asynchronously. See SetHintBits for more info. Check that - * the tuple is hinted xmin-committed because of that. - */ - if (all_visible) - { - TransactionId xmin; - - if (!HeapTupleHeaderXminCommitted(htup)) - { - all_visible = false; - break; - } - - /* - * The inserter definitely committed. But is it old enough - * that everyone sees it as committed? - */ - xmin = HeapTupleHeaderGetXmin(htup); - if (!GlobalVisTestIsRemovableXid(vacrel->vistest, xmin)) - { - all_visible = false; - break; - } - - /* Track newest xmin on page. */ - if (TransactionIdFollows(xmin, visibility_cutoff_xid) && - TransactionIdIsNormal(xmin)) - visibility_cutoff_xid = xmin; - } break; case HEAPTUPLE_RECENTLY_DEAD: @@ -1602,7 +1559,6 @@ lazy_scan_prune(LVRelState *vacrel, * pruning.) */ recently_dead_tuples++; - all_visible = false; break; case HEAPTUPLE_INSERT_IN_PROGRESS: @@ -1613,16 +1569,13 @@ lazy_scan_prune(LVRelState *vacrel, * results. This assumption is a bit shaky, but it is what * acquire_sample_rows() does, so be consistent. */ - all_visible = false; break; case HEAPTUPLE_DELETE_IN_PROGRESS: - /* This is an expected case during concurrent vacuum */ - all_visible = false; /* - * Count such rows as live. As above, we assume the deleting - * transaction will commit and update the counters after we - * report. + * This an expected case during concurrent vacuum. Count such + * rows as live. As above, we assume the deleting transaction + * will commit and update the counters after we report. */ live_tuples++; break; @@ -1665,7 +1618,7 @@ lazy_scan_prune(LVRelState *vacrel, * page all-frozen afterwards (might not happen until final heap pass). */ if (pagefrz.freeze_required || tuples_frozen == 0 || - (all_visible && all_frozen && + (presult.all_visible_except_removable && all_frozen && fpi_before != pgWalUsage.wal_fpi)) { /* @@ -1698,16 +1651,16 @@ lazy_scan_prune(LVRelState *vacrel, vacrel->frozen_pages++; /* - * We can use visibility_cutoff_xid as our cutoff for conflicts + * We can use frz_conflict_horizon as our cutoff for conflicts * when the whole page is eligible to become all-frozen in the VM * once we're done with it. Otherwise we generate a conservative * cutoff by stepping back from OldestXmin. */ - if (all_visible && all_frozen) + if (presult.all_visible_except_removable && all_frozen) { /* Using same cutoff when setting VM is now unnecessary */ - snapshotConflictHorizon = visibility_cutoff_xid; - visibility_cutoff_xid = InvalidTransactionId; + snapshotConflictHorizon = presult.frz_conflict_horizon; + presult.frz_conflict_horizon = InvalidTransactionId; } else { @@ -1743,17 +1696,19 @@ lazy_scan_prune(LVRelState *vacrel, */ #ifdef USE_ASSERT_CHECKING /* Note that all_frozen value does not matter when !all_visible */ - if (all_visible && lpdead_items == 0) + if (presult.all_visible) { TransactionId debug_cutoff; bool debug_all_frozen; + Assert(lpdead_items == 0); + if (!heap_page_is_all_visible(vacrel, buf, &debug_cutoff, &debug_all_frozen)) Assert(false); Assert(!TransactionIdIsValid(debug_cutoff) || - debug_cutoff == visibility_cutoff_xid); + debug_cutoff == presult.frz_conflict_horizon); } #endif @@ -1778,19 +1733,6 @@ lazy_scan_prune(LVRelState *vacrel, Assert(dead_items->num_items <= dead_items->max_items); pgstat_progress_update_param(PROGRESS_VACUUM_NUM_DEAD_TUPLES, dead_items->num_items); - - /* - * It was convenient to ignore LP_DEAD items in all_visible earlier on - * to make the choice of whether or not to freeze the page unaffected - * by the short-term presence of LP_DEAD items. These LP_DEAD items - * were effectively assumed to be LP_UNUSED items in the making. It - * doesn't matter which heap pass (initial pass or final pass) ends up - * setting the page all-frozen, as long as the ongoing VACUUM does it. - * - * Now that freezing has been finalized, unset all_visible. It needs - * to reflect the present state of things, as expected by our caller. - */ - all_visible = false; } /* Finally, add page-local counts to whole-VACUUM counts */ @@ -1807,20 +1749,20 @@ lazy_scan_prune(LVRelState *vacrel, /* Did we find LP_DEAD items? */ *has_lpdead_items = (lpdead_items > 0); - Assert(!all_visible || !(*has_lpdead_items)); + Assert(!presult.all_visible || !(*has_lpdead_items)); /* * Handle setting visibility map bit based on information from the VM (as * of last heap_vac_scan_next_block() call), and from all_visible and * all_frozen variables */ - if (!all_visible_according_to_vm && all_visible) + if (!all_visible_according_to_vm && presult.all_visible) { uint8 flags = VISIBILITYMAP_ALL_VISIBLE; if (all_frozen) { - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); flags |= VISIBILITYMAP_ALL_FROZEN; } @@ -1840,7 +1782,7 @@ lazy_scan_prune(LVRelState *vacrel, PageSetAllVisible(page); MarkBufferDirty(buf); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, - vmbuffer, visibility_cutoff_xid, + vmbuffer, presult.frz_conflict_horizon, flags); } @@ -1888,7 +1830,7 @@ lazy_scan_prune(LVRelState *vacrel, * it as all-frozen. Note that all_frozen is only valid if all_visible is * true, so we must check both all_visible and all_frozen. */ - else if (all_visible_according_to_vm && all_visible && + else if (all_visible_according_to_vm && presult.all_visible && all_frozen && !VM_ALL_FROZEN(vacrel->rel, blkno, &vmbuffer)) { /* @@ -1905,11 +1847,11 @@ lazy_scan_prune(LVRelState *vacrel, /* * Set the page all-frozen (and all-visible) in the VM. * - * We can pass InvalidTransactionId as our visibility_cutoff_xid, - * since a snapshotConflictHorizon sufficient to make everything safe - * for REDO was logged when the page's tuples were frozen. + * We can pass InvalidTransactionId as our frz_conflict_horizon, since + * a snapshotConflictHorizon sufficient to make everything safe for + * REDO was logged when the page's tuples were frozen. */ - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, vmbuffer, InvalidTransactionId, VISIBILITYMAP_ALL_VISIBLE | diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 4b133f68593..4cfaf9ea46c 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -198,6 +198,8 @@ typedef struct PruneResult { int ndeleted; /* Number of tuples deleted from the page */ int nnewlpdead; /* Number of newly LP_DEAD items */ + bool all_visible; /* Whether or not the page is all visible */ + TransactionId frz_conflict_horizon; /* Newest xmin on the page */ /* * Tuple visibility is only computed once for each tuple, for correctness @@ -209,6 +211,7 @@ typedef struct PruneResult * 1. Otherwise every access would need to subtract 1. */ int8 htsv[MaxHeapTuplesPerPage + 1]; + bool all_visible_except_removable; } PruneResult; /* -- 2.40.1 --rdqtp5puvxqotfdw Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v2-0004-Add-reference-to-VacuumCutoffs-in-HeapPageFreeze.patch" ^ permalink raw reply [nested|flat] 7+ messages in thread
* [PATCH v2 03/17] heap_page_prune sets all_visible and frz_conflict_horizon @ 2024-01-06 19:01 Melanie Plageman <melanieplageman@gmail.com> 0 siblings, 0 replies; 7+ messages in thread From: Melanie Plageman @ 2024-01-06 19:01 UTC (permalink / raw) In order to combine the prune and freeze records, we must know if the page is eligible to be opportunistically frozen before finishing pruning. Save all_visible in the PruneResult and set it to false when we see non-removable tuples which are not visible to everyone. We will also need to ensure that the snapshotConflictHorizon for the combined prune + freeze record is the more conservative of that calculated for each of pruning and freezing. Calculate the visibility_cutoff_xid for the purposes of freezing -- the newest xmin on the page -- in heap_page_prune() and save it in PruneResult.frz_conflict_horizon. --- src/backend/access/heap/pruneheap.c | 122 +++++++++++++++++++++++++-- src/backend/access/heap/vacuumlazy.c | 116 +++++++------------------ src/include/access/heapam.h | 3 + 3 files changed, 146 insertions(+), 95 deletions(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 4600ee53751..b3a7ce06699 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -65,8 +65,10 @@ static int heap_prune_chain(Buffer buffer, static void heap_prune_record_prunable(PruneState *prstate, TransactionId xid); static void heap_prune_record_redirect(PruneState *prstate, OffsetNumber offnum, OffsetNumber rdoffnum); -static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum); -static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum); +static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); +static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); static void heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum); static void page_verify_redirects(Page page); @@ -249,6 +251,14 @@ heap_page_prune(Relation relation, Buffer buffer, presult->ndeleted = 0; presult->nnewlpdead = 0; + /* + * Keep track of whether or not the page is all_visible in case the caller + * wants to use this information to update the VM. + */ + presult->all_visible = true; + /* for recovery conflicts */ + presult->frz_conflict_horizon = InvalidTransactionId; + maxoff = PageGetMaxOffsetNumber(page); tup.t_tableOid = RelationGetRelid(prstate.rel); @@ -300,8 +310,92 @@ heap_page_prune(Relation relation, Buffer buffer, presult->htsv[offnum] = heap_prune_satisfies_vacuum(&prstate, &tup, buffer); + switch (presult->htsv[offnum]) + { + case HEAPTUPLE_DEAD: + + /* + * Deliberately delay unsetting all_visible until later during + * pruning. Removable dead tuples shouldn't preclude freezing + * the page. After finishing this first pass of tuple + * visibility checks, initialize all_visible_except_removable + * with the current value of all_visible to indicate whether + * or not the page is all visible except for dead tuples. This + * will allow us to attempt to freeze the page after pruning. + * Later during pruning, if we encounter an LP_DEAD item or + * are setting an item LP_DEAD, we will unset all_visible. As + * long as we unset it before updating the visibility map, + * this will be correct. + */ + break; + case HEAPTUPLE_LIVE: + + /* + * Is the tuple definitely visible to all transactions? + * + * NB: Like with per-tuple hint bits, we can't set the + * PD_ALL_VISIBLE flag if the inserter committed + * asynchronously. See SetHintBits for more info. Check that + * the tuple is hinted xmin-committed because of that. + */ + if (presult->all_visible) + { + TransactionId xmin; + + if (!HeapTupleHeaderXminCommitted(htup)) + { + presult->all_visible = false; + break; + } + + /* + * The inserter definitely committed. But is it old enough + * that everyone sees it as committed? + */ + xmin = HeapTupleHeaderGetXmin(htup); + if (!GlobalVisTestIsRemovableXid(vistest, xmin)) + { + presult->all_visible = false; + break; + } + + /* Track newest xmin on page. */ + if (TransactionIdFollows(xmin, presult->frz_conflict_horizon) && + TransactionIdIsNormal(xmin)) + presult->frz_conflict_horizon = xmin; + } + break; + case HEAPTUPLE_RECENTLY_DEAD: + presult->all_visible = false; + break; + case HEAPTUPLE_INSERT_IN_PROGRESS: + presult->all_visible = false; + break; + case HEAPTUPLE_DELETE_IN_PROGRESS: + /* This is an expected case during concurrent vacuum */ + presult->all_visible = false; + break; + default: + elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); + break; + } } + /* + * For vacuum, if the whole page will become frozen, we consider + * opportunistically freezing tuples. Dead tuples which will be removed by + * the end of vacuuming should not preclude us from opportunistically + * freezing. We will not be able to freeze the whole page if there are + * tuples present which are not visible to everyone or if there are dead + * tuples which are not yet removable. We need all_visible to be false if + * LP_DEAD tuples remain after pruning so that we do not incorrectly + * update the visibility map or page hint bit. So, we will update + * presult->all_visible to reflect the presence of LP_DEAD items while + * pruning and keep all_visible_except_removable to permit freezing if the + * whole page will eventually become all visible after removing tuples. + */ + presult->all_visible_except_removable = presult->all_visible; + /* Scan the page */ for (offnum = FirstOffsetNumber; offnum <= maxoff; @@ -596,10 +690,14 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, /* * If the caller set mark_unused_now true, we can set dead line * pointers LP_UNUSED now. We don't increment ndeleted here since - * the LP was already marked dead. + * the LP was already marked dead. If it will not be marked + * LP_UNUSED, it will remain LP_DEAD, making the page not + * all_visible. */ if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); + else + presult->all_visible = false; break; } @@ -736,7 +834,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect the root to the correct chain member. */ if (i >= nchain) - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); else heap_prune_record_redirect(prstate, rootoffnum, chainitems[i]); } @@ -749,7 +847,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect item. We can clean up by setting the redirect item to * DEAD state or LP_UNUSED if the caller indicated. */ - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); } return ndeleted; @@ -786,13 +884,20 @@ heap_prune_record_redirect(PruneState *prstate, /* Record line pointer to be marked dead */ static void -heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { Assert(prstate->ndead < MaxHeapTuplesPerPage); prstate->nowdead[prstate->ndead] = offnum; prstate->ndead++; Assert(!prstate->marked[offnum]); prstate->marked[offnum] = true; + + /* + * Setting the line pointer LP_DEAD means the page will definitely not be + * all_visible. + */ + presult->all_visible = false; } /* @@ -802,7 +907,8 @@ heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) * pointers LP_DEAD if mark_unused_now is true. */ static void -heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { /* * If the caller set mark_unused_now to true, we can remove dead tuples @@ -813,7 +919,7 @@ heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); else - heap_prune_record_dead(prstate, offnum); + heap_prune_record_dead(prstate, offnum, presult); } /* Record line pointer to be marked unused */ diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index d1efd885c88..f9892f4cd08 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -1422,9 +1422,7 @@ lazy_scan_prune(LVRelState *vacrel, recently_dead_tuples; HeapPageFreeze pagefrz; bool hastup = false; - bool all_visible, - all_frozen; - TransactionId visibility_cutoff_xid; + bool all_frozen; int64 fpi_before = pgWalUsage.wal_fpi; OffsetNumber deadoffsets[MaxHeapTuplesPerPage]; HeapTupleFreeze frozen[MaxHeapTuplesPerPage]; @@ -1465,17 +1463,16 @@ lazy_scan_prune(LVRelState *vacrel, &presult, &vacrel->offnum); /* - * We will update the VM after collecting LP_DEAD items and freezing - * tuples. Keep track of whether or not the page is all_visible and - * all_frozen and use this information to update the VM. all_visible - * implies 0 lpdead_items, but don't trust all_frozen result unless - * all_visible is also set to true. + * Now scan the page to collect LP_DEAD items and check for tuples + * requiring freezing among remaining tuples with storage. We will update + * the VM after collecting LP_DEAD items and freezing tuples. Pruning will + * have determined whether or not the page is all_visible. Keep track of + * whether or not the page is all_frozen and use this information to + * update the VM. all_visible implies lpdead_items == 0, but don't trust + * all_frozen result unless all_visible is also set to true. * - * Also keep track of the visibility cutoff xid for recovery conflicts. */ - all_visible = true; all_frozen = true; - visibility_cutoff_xid = InvalidTransactionId; /* * Now scan the page to collect LP_DEAD items and update the variables set @@ -1516,11 +1513,6 @@ lazy_scan_prune(LVRelState *vacrel, * will only happen every other VACUUM, at most. Besides, VACUUM * must treat hastup/nonempty_pages as provisional no matter how * LP_DEAD items are handled (handled here, or handled later on). - * - * Also deliberately delay unsetting all_visible until just before - * we return to lazy_scan_heap caller, as explained in full below. - * (This is another case where it's useful to anticipate that any - * LP_DEAD items will become LP_UNUSED during the ongoing VACUUM.) */ deadoffsets[lpdead_items++] = offnum; continue; @@ -1558,41 +1550,6 @@ lazy_scan_prune(LVRelState *vacrel, * what acquire_sample_rows() does. */ live_tuples++; - - /* - * Is the tuple definitely visible to all transactions? - * - * NB: Like with per-tuple hint bits, we can't set the - * PD_ALL_VISIBLE flag if the inserter committed - * asynchronously. See SetHintBits for more info. Check that - * the tuple is hinted xmin-committed because of that. - */ - if (all_visible) - { - TransactionId xmin; - - if (!HeapTupleHeaderXminCommitted(htup)) - { - all_visible = false; - break; - } - - /* - * The inserter definitely committed. But is it old enough - * that everyone sees it as committed? - */ - xmin = HeapTupleHeaderGetXmin(htup); - if (!GlobalVisTestIsRemovableXid(vacrel->vistest, xmin)) - { - all_visible = false; - break; - } - - /* Track newest xmin on page. */ - if (TransactionIdFollows(xmin, visibility_cutoff_xid) && - TransactionIdIsNormal(xmin)) - visibility_cutoff_xid = xmin; - } break; case HEAPTUPLE_RECENTLY_DEAD: @@ -1602,7 +1559,6 @@ lazy_scan_prune(LVRelState *vacrel, * pruning.) */ recently_dead_tuples++; - all_visible = false; break; case HEAPTUPLE_INSERT_IN_PROGRESS: @@ -1613,16 +1569,13 @@ lazy_scan_prune(LVRelState *vacrel, * results. This assumption is a bit shaky, but it is what * acquire_sample_rows() does, so be consistent. */ - all_visible = false; break; case HEAPTUPLE_DELETE_IN_PROGRESS: - /* This is an expected case during concurrent vacuum */ - all_visible = false; /* - * Count such rows as live. As above, we assume the deleting - * transaction will commit and update the counters after we - * report. + * This an expected case during concurrent vacuum. Count such + * rows as live. As above, we assume the deleting transaction + * will commit and update the counters after we report. */ live_tuples++; break; @@ -1665,7 +1618,7 @@ lazy_scan_prune(LVRelState *vacrel, * page all-frozen afterwards (might not happen until final heap pass). */ if (pagefrz.freeze_required || tuples_frozen == 0 || - (all_visible && all_frozen && + (presult.all_visible_except_removable && all_frozen && fpi_before != pgWalUsage.wal_fpi)) { /* @@ -1698,16 +1651,16 @@ lazy_scan_prune(LVRelState *vacrel, vacrel->frozen_pages++; /* - * We can use visibility_cutoff_xid as our cutoff for conflicts + * We can use frz_conflict_horizon as our cutoff for conflicts * when the whole page is eligible to become all-frozen in the VM * once we're done with it. Otherwise we generate a conservative * cutoff by stepping back from OldestXmin. */ - if (all_visible && all_frozen) + if (presult.all_visible_except_removable && all_frozen) { /* Using same cutoff when setting VM is now unnecessary */ - snapshotConflictHorizon = visibility_cutoff_xid; - visibility_cutoff_xid = InvalidTransactionId; + snapshotConflictHorizon = presult.frz_conflict_horizon; + presult.frz_conflict_horizon = InvalidTransactionId; } else { @@ -1743,17 +1696,19 @@ lazy_scan_prune(LVRelState *vacrel, */ #ifdef USE_ASSERT_CHECKING /* Note that all_frozen value does not matter when !all_visible */ - if (all_visible && lpdead_items == 0) + if (presult.all_visible) { TransactionId debug_cutoff; bool debug_all_frozen; + Assert(lpdead_items == 0); + if (!heap_page_is_all_visible(vacrel, buf, &debug_cutoff, &debug_all_frozen)) Assert(false); Assert(!TransactionIdIsValid(debug_cutoff) || - debug_cutoff == visibility_cutoff_xid); + debug_cutoff == presult.frz_conflict_horizon); } #endif @@ -1778,19 +1733,6 @@ lazy_scan_prune(LVRelState *vacrel, Assert(dead_items->num_items <= dead_items->max_items); pgstat_progress_update_param(PROGRESS_VACUUM_NUM_DEAD_TUPLES, dead_items->num_items); - - /* - * It was convenient to ignore LP_DEAD items in all_visible earlier on - * to make the choice of whether or not to freeze the page unaffected - * by the short-term presence of LP_DEAD items. These LP_DEAD items - * were effectively assumed to be LP_UNUSED items in the making. It - * doesn't matter which heap pass (initial pass or final pass) ends up - * setting the page all-frozen, as long as the ongoing VACUUM does it. - * - * Now that freezing has been finalized, unset all_visible. It needs - * to reflect the present state of things, as expected by our caller. - */ - all_visible = false; } /* Finally, add page-local counts to whole-VACUUM counts */ @@ -1807,20 +1749,20 @@ lazy_scan_prune(LVRelState *vacrel, /* Did we find LP_DEAD items? */ *has_lpdead_items = (lpdead_items > 0); - Assert(!all_visible || !(*has_lpdead_items)); + Assert(!presult.all_visible || !(*has_lpdead_items)); /* * Handle setting visibility map bit based on information from the VM (as * of last heap_vac_scan_next_block() call), and from all_visible and * all_frozen variables */ - if (!all_visible_according_to_vm && all_visible) + if (!all_visible_according_to_vm && presult.all_visible) { uint8 flags = VISIBILITYMAP_ALL_VISIBLE; if (all_frozen) { - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); flags |= VISIBILITYMAP_ALL_FROZEN; } @@ -1840,7 +1782,7 @@ lazy_scan_prune(LVRelState *vacrel, PageSetAllVisible(page); MarkBufferDirty(buf); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, - vmbuffer, visibility_cutoff_xid, + vmbuffer, presult.frz_conflict_horizon, flags); } @@ -1888,7 +1830,7 @@ lazy_scan_prune(LVRelState *vacrel, * it as all-frozen. Note that all_frozen is only valid if all_visible is * true, so we must check both all_visible and all_frozen. */ - else if (all_visible_according_to_vm && all_visible && + else if (all_visible_according_to_vm && presult.all_visible && all_frozen && !VM_ALL_FROZEN(vacrel->rel, blkno, &vmbuffer)) { /* @@ -1905,11 +1847,11 @@ lazy_scan_prune(LVRelState *vacrel, /* * Set the page all-frozen (and all-visible) in the VM. * - * We can pass InvalidTransactionId as our visibility_cutoff_xid, - * since a snapshotConflictHorizon sufficient to make everything safe - * for REDO was logged when the page's tuples were frozen. + * We can pass InvalidTransactionId as our frz_conflict_horizon, since + * a snapshotConflictHorizon sufficient to make everything safe for + * REDO was logged when the page's tuples were frozen. */ - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, vmbuffer, InvalidTransactionId, VISIBILITYMAP_ALL_VISIBLE | diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 4b133f68593..4cfaf9ea46c 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -198,6 +198,8 @@ typedef struct PruneResult { int ndeleted; /* Number of tuples deleted from the page */ int nnewlpdead; /* Number of newly LP_DEAD items */ + bool all_visible; /* Whether or not the page is all visible */ + TransactionId frz_conflict_horizon; /* Newest xmin on the page */ /* * Tuple visibility is only computed once for each tuple, for correctness @@ -209,6 +211,7 @@ typedef struct PruneResult * 1. Otherwise every access would need to subtract 1. */ int8 htsv[MaxHeapTuplesPerPage + 1]; + bool all_visible_except_removable; } PruneResult; /* -- 2.40.1 --rdqtp5puvxqotfdw Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v2-0004-Add-reference-to-VacuumCutoffs-in-HeapPageFreeze.patch" ^ permalink raw reply [nested|flat] 7+ messages in thread
* [PATCH v4 05/19] heap_page_prune sets all_visible and frz_conflict_horizon @ 2024-01-06 19:01 Melanie Plageman <melanieplageman@gmail.com> 0 siblings, 0 replies; 7+ messages in thread From: Melanie Plageman @ 2024-01-06 19:01 UTC (permalink / raw) In order to combine the prune and freeze records, we must know if the page is eligible to be opportunistically frozen before finishing pruning. Save all_visible in the PruneResult and set it to false when we see non-removable tuples which are not visible to everyone. We will also need to ensure that the snapshotConflictHorizon for the combined prune + freeze record is the more conservative of that calculated for each of pruning and freezing. Calculate the visibility_cutoff_xid for the purposes of freezing -- the newest xmin on the page -- in heap_page_prune() and save it in PruneResult.frz_conflict_horizon. --- src/backend/access/heap/pruneheap.c | 127 +++++++++++++++++++++++++-- src/backend/access/heap/vacuumlazy.c | 121 ++++++------------------- src/include/access/heapam.h | 3 + 3 files changed, 151 insertions(+), 100 deletions(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 7eb21b603ba..bd30296ef1a 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -63,8 +63,10 @@ static int heap_prune_chain(Buffer buffer, static void heap_prune_record_prunable(PruneState *prstate, TransactionId xid); static void heap_prune_record_redirect(PruneState *prstate, OffsetNumber offnum, OffsetNumber rdoffnum); -static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum); -static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum); +static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); +static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); static void heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum); static void page_verify_redirects(Page page); @@ -246,6 +248,14 @@ heap_page_prune(Relation relation, Buffer buffer, presult->ndeleted = 0; presult->nnewlpdead = 0; + /* + * Keep track of whether or not the page is all_visible in case the caller + * wants to use this information to update the VM. + */ + presult->all_visible = true; + /* for recovery conflicts */ + presult->frz_conflict_horizon = InvalidTransactionId; + maxoff = PageGetMaxOffsetNumber(page); tup.t_tableOid = RelationGetRelid(relation); @@ -297,8 +307,97 @@ heap_page_prune(Relation relation, Buffer buffer, presult->htsv[offnum] = heap_prune_satisfies_vacuum(&prstate, &tup, buffer); + switch (presult->htsv[offnum]) + { + case HEAPTUPLE_DEAD: + + /* + * Deliberately delay unsetting all_visible until later during + * pruning. Removable dead tuples shouldn't preclude freezing + * the page. After finishing this first pass of tuple + * visibility checks, initialize all_visible_except_removable + * with the current value of all_visible to indicate whether + * or not the page is all visible except for dead tuples. This + * will allow us to attempt to freeze the page after pruning. + * Later during pruning, if we encounter an LP_DEAD item or + * are setting an item LP_DEAD, we will unset all_visible. As + * long as we unset it before updating the visibility map, + * this will be correct. + */ + break; + case HEAPTUPLE_LIVE: + + /* + * Is the tuple definitely visible to all transactions? + * + * NB: Like with per-tuple hint bits, we can't set the + * PD_ALL_VISIBLE flag if the inserter committed + * asynchronously. See SetHintBits for more info. Check that + * the tuple is hinted xmin-committed because of that. + */ + if (presult->all_visible) + { + TransactionId xmin; + + if (!HeapTupleHeaderXminCommitted(htup)) + { + presult->all_visible = false; + break; + } + + /* + * The inserter definitely committed. But is it old enough + * that everyone sees it as committed? A + * FrozenTransactionId is seen as committed to everyone. + * Otherwise, we check if there is a snapshot that + * considers this xid to still be running, and if so, we + * don't consider the page all-visible. + */ + xmin = HeapTupleHeaderGetXmin(htup); + if (xmin != FrozenTransactionId && + !GlobalVisTestIsRemovableXid(vistest, xmin)) + { + presult->all_visible = false; + break; + } + + /* Track newest xmin on page. */ + if (TransactionIdFollows(xmin, presult->frz_conflict_horizon) && + TransactionIdIsNormal(xmin)) + presult->frz_conflict_horizon = xmin; + } + break; + case HEAPTUPLE_RECENTLY_DEAD: + presult->all_visible = false; + break; + case HEAPTUPLE_INSERT_IN_PROGRESS: + presult->all_visible = false; + break; + case HEAPTUPLE_DELETE_IN_PROGRESS: + /* This is an expected case during concurrent vacuum */ + presult->all_visible = false; + break; + default: + elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); + break; + } } + /* + * For vacuum, if the whole page will become frozen, we consider + * opportunistically freezing tuples. Dead tuples which will be removed by + * the end of vacuuming should not preclude us from opportunistically + * freezing. We will not be able to freeze the whole page if there are + * tuples present which are not visible to everyone or if there are dead + * tuples which are not yet removable. We need all_visible to be false if + * LP_DEAD tuples remain after pruning so that we do not incorrectly + * update the visibility map or page hint bit. So, we will update + * presult->all_visible to reflect the presence of LP_DEAD items while + * pruning and keep all_visible_except_removable to permit freezing if the + * whole page will eventually become all visible after removing tuples. + */ + presult->all_visible_except_removable = presult->all_visible; + /* Scan the page */ for (offnum = FirstOffsetNumber; offnum <= maxoff; @@ -593,10 +692,14 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, /* * If the caller set mark_unused_now true, we can set dead line * pointers LP_UNUSED now. We don't increment ndeleted here since - * the LP was already marked dead. + * the LP was already marked dead. If it will not be marked + * LP_UNUSED, it will remain LP_DEAD, making the page not + * all_visible. */ if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); + else + presult->all_visible = false; break; } @@ -733,7 +836,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect the root to the correct chain member. */ if (i >= nchain) - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); else heap_prune_record_redirect(prstate, rootoffnum, chainitems[i]); } @@ -746,7 +849,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect item. We can clean up by setting the redirect item to * DEAD state or LP_UNUSED if the caller indicated. */ - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); } return ndeleted; @@ -783,13 +886,20 @@ heap_prune_record_redirect(PruneState *prstate, /* Record line pointer to be marked dead */ static void -heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { Assert(prstate->ndead < MaxHeapTuplesPerPage); prstate->nowdead[prstate->ndead] = offnum; prstate->ndead++; Assert(!prstate->marked[offnum]); prstate->marked[offnum] = true; + + /* + * Setting the line pointer LP_DEAD means the page will definitely not be + * all_visible. + */ + presult->all_visible = false; } /* @@ -799,7 +909,8 @@ heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) * pointers LP_DEAD if mark_unused_now is true. */ static void -heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { /* * If the caller set mark_unused_now to true, we can remove dead tuples @@ -810,7 +921,7 @@ heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); else - heap_prune_record_dead(prstate, offnum); + heap_prune_record_dead(prstate, offnum, presult); } /* Record line pointer to be marked unused */ diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 3a991f0ea71..f9892f4cd08 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -1422,9 +1422,7 @@ lazy_scan_prune(LVRelState *vacrel, recently_dead_tuples; HeapPageFreeze pagefrz; bool hastup = false; - bool all_visible, - all_frozen; - TransactionId visibility_cutoff_xid; + bool all_frozen; int64 fpi_before = pgWalUsage.wal_fpi; OffsetNumber deadoffsets[MaxHeapTuplesPerPage]; HeapTupleFreeze frozen[MaxHeapTuplesPerPage]; @@ -1465,17 +1463,16 @@ lazy_scan_prune(LVRelState *vacrel, &presult, &vacrel->offnum); /* - * We will update the VM after collecting LP_DEAD items and freezing - * tuples. Keep track of whether or not the page is all_visible and - * all_frozen and use this information to update the VM. all_visible - * implies 0 lpdead_items, but don't trust all_frozen result unless - * all_visible is also set to true. + * Now scan the page to collect LP_DEAD items and check for tuples + * requiring freezing among remaining tuples with storage. We will update + * the VM after collecting LP_DEAD items and freezing tuples. Pruning will + * have determined whether or not the page is all_visible. Keep track of + * whether or not the page is all_frozen and use this information to + * update the VM. all_visible implies lpdead_items == 0, but don't trust + * all_frozen result unless all_visible is also set to true. * - * Also keep track of the visibility cutoff xid for recovery conflicts. */ - all_visible = true; all_frozen = true; - visibility_cutoff_xid = InvalidTransactionId; /* * Now scan the page to collect LP_DEAD items and update the variables set @@ -1516,11 +1513,6 @@ lazy_scan_prune(LVRelState *vacrel, * will only happen every other VACUUM, at most. Besides, VACUUM * must treat hastup/nonempty_pages as provisional no matter how * LP_DEAD items are handled (handled here, or handled later on). - * - * Also deliberately delay unsetting all_visible until just before - * we return to lazy_scan_heap caller, as explained in full below. - * (This is another case where it's useful to anticipate that any - * LP_DEAD items will become LP_UNUSED during the ongoing VACUUM.) */ deadoffsets[lpdead_items++] = offnum; continue; @@ -1558,46 +1550,6 @@ lazy_scan_prune(LVRelState *vacrel, * what acquire_sample_rows() does. */ live_tuples++; - - /* - * Is the tuple definitely visible to all transactions? - * - * NB: Like with per-tuple hint bits, we can't set the - * PD_ALL_VISIBLE flag if the inserter committed - * asynchronously. See SetHintBits for more info. Check that - * the tuple is hinted xmin-committed because of that. - */ - if (all_visible) - { - TransactionId xmin; - - if (!HeapTupleHeaderXminCommitted(htup)) - { - all_visible = false; - break; - } - - /* - * The inserter definitely committed. But is it old enough - * that everyone sees it as committed? A - * FrozenTransactionId is seen as committed to everyone. - * Otherwise, we check if there is a snapshot that - * considers this xid to still be running, and if so, we - * don't consider the page all-visible. - */ - xmin = HeapTupleHeaderGetXmin(htup); - if (xmin != FrozenTransactionId && - !GlobalVisTestIsRemovableXid(vacrel->vistest, xmin)) - { - all_visible = false; - break; - } - - /* Track newest xmin on page. */ - if (TransactionIdFollows(xmin, visibility_cutoff_xid) && - TransactionIdIsNormal(xmin)) - visibility_cutoff_xid = xmin; - } break; case HEAPTUPLE_RECENTLY_DEAD: @@ -1607,7 +1559,6 @@ lazy_scan_prune(LVRelState *vacrel, * pruning.) */ recently_dead_tuples++; - all_visible = false; break; case HEAPTUPLE_INSERT_IN_PROGRESS: @@ -1618,16 +1569,13 @@ lazy_scan_prune(LVRelState *vacrel, * results. This assumption is a bit shaky, but it is what * acquire_sample_rows() does, so be consistent. */ - all_visible = false; break; case HEAPTUPLE_DELETE_IN_PROGRESS: - /* This is an expected case during concurrent vacuum */ - all_visible = false; /* - * Count such rows as live. As above, we assume the deleting - * transaction will commit and update the counters after we - * report. + * This an expected case during concurrent vacuum. Count such + * rows as live. As above, we assume the deleting transaction + * will commit and update the counters after we report. */ live_tuples++; break; @@ -1670,7 +1618,7 @@ lazy_scan_prune(LVRelState *vacrel, * page all-frozen afterwards (might not happen until final heap pass). */ if (pagefrz.freeze_required || tuples_frozen == 0 || - (all_visible && all_frozen && + (presult.all_visible_except_removable && all_frozen && fpi_before != pgWalUsage.wal_fpi)) { /* @@ -1703,16 +1651,16 @@ lazy_scan_prune(LVRelState *vacrel, vacrel->frozen_pages++; /* - * We can use visibility_cutoff_xid as our cutoff for conflicts + * We can use frz_conflict_horizon as our cutoff for conflicts * when the whole page is eligible to become all-frozen in the VM * once we're done with it. Otherwise we generate a conservative * cutoff by stepping back from OldestXmin. */ - if (all_visible && all_frozen) + if (presult.all_visible_except_removable && all_frozen) { /* Using same cutoff when setting VM is now unnecessary */ - snapshotConflictHorizon = visibility_cutoff_xid; - visibility_cutoff_xid = InvalidTransactionId; + snapshotConflictHorizon = presult.frz_conflict_horizon; + presult.frz_conflict_horizon = InvalidTransactionId; } else { @@ -1748,17 +1696,19 @@ lazy_scan_prune(LVRelState *vacrel, */ #ifdef USE_ASSERT_CHECKING /* Note that all_frozen value does not matter when !all_visible */ - if (all_visible && lpdead_items == 0) + if (presult.all_visible) { TransactionId debug_cutoff; bool debug_all_frozen; + Assert(lpdead_items == 0); + if (!heap_page_is_all_visible(vacrel, buf, &debug_cutoff, &debug_all_frozen)) Assert(false); Assert(!TransactionIdIsValid(debug_cutoff) || - debug_cutoff == visibility_cutoff_xid); + debug_cutoff == presult.frz_conflict_horizon); } #endif @@ -1783,19 +1733,6 @@ lazy_scan_prune(LVRelState *vacrel, Assert(dead_items->num_items <= dead_items->max_items); pgstat_progress_update_param(PROGRESS_VACUUM_NUM_DEAD_TUPLES, dead_items->num_items); - - /* - * It was convenient to ignore LP_DEAD items in all_visible earlier on - * to make the choice of whether or not to freeze the page unaffected - * by the short-term presence of LP_DEAD items. These LP_DEAD items - * were effectively assumed to be LP_UNUSED items in the making. It - * doesn't matter which heap pass (initial pass or final pass) ends up - * setting the page all-frozen, as long as the ongoing VACUUM does it. - * - * Now that freezing has been finalized, unset all_visible. It needs - * to reflect the present state of things, as expected by our caller. - */ - all_visible = false; } /* Finally, add page-local counts to whole-VACUUM counts */ @@ -1812,20 +1749,20 @@ lazy_scan_prune(LVRelState *vacrel, /* Did we find LP_DEAD items? */ *has_lpdead_items = (lpdead_items > 0); - Assert(!all_visible || !(*has_lpdead_items)); + Assert(!presult.all_visible || !(*has_lpdead_items)); /* * Handle setting visibility map bit based on information from the VM (as * of last heap_vac_scan_next_block() call), and from all_visible and * all_frozen variables */ - if (!all_visible_according_to_vm && all_visible) + if (!all_visible_according_to_vm && presult.all_visible) { uint8 flags = VISIBILITYMAP_ALL_VISIBLE; if (all_frozen) { - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); flags |= VISIBILITYMAP_ALL_FROZEN; } @@ -1845,7 +1782,7 @@ lazy_scan_prune(LVRelState *vacrel, PageSetAllVisible(page); MarkBufferDirty(buf); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, - vmbuffer, visibility_cutoff_xid, + vmbuffer, presult.frz_conflict_horizon, flags); } @@ -1893,7 +1830,7 @@ lazy_scan_prune(LVRelState *vacrel, * it as all-frozen. Note that all_frozen is only valid if all_visible is * true, so we must check both all_visible and all_frozen. */ - else if (all_visible_according_to_vm && all_visible && + else if (all_visible_according_to_vm && presult.all_visible && all_frozen && !VM_ALL_FROZEN(vacrel->rel, blkno, &vmbuffer)) { /* @@ -1910,11 +1847,11 @@ lazy_scan_prune(LVRelState *vacrel, /* * Set the page all-frozen (and all-visible) in the VM. * - * We can pass InvalidTransactionId as our visibility_cutoff_xid, - * since a snapshotConflictHorizon sufficient to make everything safe - * for REDO was logged when the page's tuples were frozen. + * We can pass InvalidTransactionId as our frz_conflict_horizon, since + * a snapshotConflictHorizon sufficient to make everything safe for + * REDO was logged when the page's tuples were frozen. */ - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, vmbuffer, InvalidTransactionId, VISIBILITYMAP_ALL_VISIBLE | diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 4b133f68593..d8e65ae7e35 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -198,6 +198,9 @@ typedef struct PruneResult { int ndeleted; /* Number of tuples deleted from the page */ int nnewlpdead; /* Number of newly LP_DEAD items */ + bool all_visible; /* Whether or not the page is all visible */ + bool all_visible_except_removable; + TransactionId frz_conflict_horizon; /* Newest xmin on the page */ /* * Tuple visibility is only computed once for each tuple, for correctness -- 2.40.1 --tez7m2a73jtztiij Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v4-0006-Add-reference-to-VacuumCutoffs-in-HeapPageFreeze.patch" ^ permalink raw reply [nested|flat] 7+ messages in thread
* [PATCH v4 05/19] heap_page_prune sets all_visible and frz_conflict_horizon @ 2024-01-06 19:01 Melanie Plageman <melanieplageman@gmail.com> 0 siblings, 0 replies; 7+ messages in thread From: Melanie Plageman @ 2024-01-06 19:01 UTC (permalink / raw) In order to combine the prune and freeze records, we must know if the page is eligible to be opportunistically frozen before finishing pruning. Save all_visible in the PruneResult and set it to false when we see non-removable tuples which are not visible to everyone. We will also need to ensure that the snapshotConflictHorizon for the combined prune + freeze record is the more conservative of that calculated for each of pruning and freezing. Calculate the visibility_cutoff_xid for the purposes of freezing -- the newest xmin on the page -- in heap_page_prune() and save it in PruneResult.frz_conflict_horizon. --- src/backend/access/heap/pruneheap.c | 127 +++++++++++++++++++++++++-- src/backend/access/heap/vacuumlazy.c | 121 ++++++------------------- src/include/access/heapam.h | 3 + 3 files changed, 151 insertions(+), 100 deletions(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 7eb21b603ba..bd30296ef1a 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -63,8 +63,10 @@ static int heap_prune_chain(Buffer buffer, static void heap_prune_record_prunable(PruneState *prstate, TransactionId xid); static void heap_prune_record_redirect(PruneState *prstate, OffsetNumber offnum, OffsetNumber rdoffnum); -static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum); -static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum); +static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); +static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); static void heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum); static void page_verify_redirects(Page page); @@ -246,6 +248,14 @@ heap_page_prune(Relation relation, Buffer buffer, presult->ndeleted = 0; presult->nnewlpdead = 0; + /* + * Keep track of whether or not the page is all_visible in case the caller + * wants to use this information to update the VM. + */ + presult->all_visible = true; + /* for recovery conflicts */ + presult->frz_conflict_horizon = InvalidTransactionId; + maxoff = PageGetMaxOffsetNumber(page); tup.t_tableOid = RelationGetRelid(relation); @@ -297,8 +307,97 @@ heap_page_prune(Relation relation, Buffer buffer, presult->htsv[offnum] = heap_prune_satisfies_vacuum(&prstate, &tup, buffer); + switch (presult->htsv[offnum]) + { + case HEAPTUPLE_DEAD: + + /* + * Deliberately delay unsetting all_visible until later during + * pruning. Removable dead tuples shouldn't preclude freezing + * the page. After finishing this first pass of tuple + * visibility checks, initialize all_visible_except_removable + * with the current value of all_visible to indicate whether + * or not the page is all visible except for dead tuples. This + * will allow us to attempt to freeze the page after pruning. + * Later during pruning, if we encounter an LP_DEAD item or + * are setting an item LP_DEAD, we will unset all_visible. As + * long as we unset it before updating the visibility map, + * this will be correct. + */ + break; + case HEAPTUPLE_LIVE: + + /* + * Is the tuple definitely visible to all transactions? + * + * NB: Like with per-tuple hint bits, we can't set the + * PD_ALL_VISIBLE flag if the inserter committed + * asynchronously. See SetHintBits for more info. Check that + * the tuple is hinted xmin-committed because of that. + */ + if (presult->all_visible) + { + TransactionId xmin; + + if (!HeapTupleHeaderXminCommitted(htup)) + { + presult->all_visible = false; + break; + } + + /* + * The inserter definitely committed. But is it old enough + * that everyone sees it as committed? A + * FrozenTransactionId is seen as committed to everyone. + * Otherwise, we check if there is a snapshot that + * considers this xid to still be running, and if so, we + * don't consider the page all-visible. + */ + xmin = HeapTupleHeaderGetXmin(htup); + if (xmin != FrozenTransactionId && + !GlobalVisTestIsRemovableXid(vistest, xmin)) + { + presult->all_visible = false; + break; + } + + /* Track newest xmin on page. */ + if (TransactionIdFollows(xmin, presult->frz_conflict_horizon) && + TransactionIdIsNormal(xmin)) + presult->frz_conflict_horizon = xmin; + } + break; + case HEAPTUPLE_RECENTLY_DEAD: + presult->all_visible = false; + break; + case HEAPTUPLE_INSERT_IN_PROGRESS: + presult->all_visible = false; + break; + case HEAPTUPLE_DELETE_IN_PROGRESS: + /* This is an expected case during concurrent vacuum */ + presult->all_visible = false; + break; + default: + elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); + break; + } } + /* + * For vacuum, if the whole page will become frozen, we consider + * opportunistically freezing tuples. Dead tuples which will be removed by + * the end of vacuuming should not preclude us from opportunistically + * freezing. We will not be able to freeze the whole page if there are + * tuples present which are not visible to everyone or if there are dead + * tuples which are not yet removable. We need all_visible to be false if + * LP_DEAD tuples remain after pruning so that we do not incorrectly + * update the visibility map or page hint bit. So, we will update + * presult->all_visible to reflect the presence of LP_DEAD items while + * pruning and keep all_visible_except_removable to permit freezing if the + * whole page will eventually become all visible after removing tuples. + */ + presult->all_visible_except_removable = presult->all_visible; + /* Scan the page */ for (offnum = FirstOffsetNumber; offnum <= maxoff; @@ -593,10 +692,14 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, /* * If the caller set mark_unused_now true, we can set dead line * pointers LP_UNUSED now. We don't increment ndeleted here since - * the LP was already marked dead. + * the LP was already marked dead. If it will not be marked + * LP_UNUSED, it will remain LP_DEAD, making the page not + * all_visible. */ if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); + else + presult->all_visible = false; break; } @@ -733,7 +836,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect the root to the correct chain member. */ if (i >= nchain) - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); else heap_prune_record_redirect(prstate, rootoffnum, chainitems[i]); } @@ -746,7 +849,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect item. We can clean up by setting the redirect item to * DEAD state or LP_UNUSED if the caller indicated. */ - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); } return ndeleted; @@ -783,13 +886,20 @@ heap_prune_record_redirect(PruneState *prstate, /* Record line pointer to be marked dead */ static void -heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { Assert(prstate->ndead < MaxHeapTuplesPerPage); prstate->nowdead[prstate->ndead] = offnum; prstate->ndead++; Assert(!prstate->marked[offnum]); prstate->marked[offnum] = true; + + /* + * Setting the line pointer LP_DEAD means the page will definitely not be + * all_visible. + */ + presult->all_visible = false; } /* @@ -799,7 +909,8 @@ heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) * pointers LP_DEAD if mark_unused_now is true. */ static void -heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { /* * If the caller set mark_unused_now to true, we can remove dead tuples @@ -810,7 +921,7 @@ heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); else - heap_prune_record_dead(prstate, offnum); + heap_prune_record_dead(prstate, offnum, presult); } /* Record line pointer to be marked unused */ diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 3a991f0ea71..f9892f4cd08 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -1422,9 +1422,7 @@ lazy_scan_prune(LVRelState *vacrel, recently_dead_tuples; HeapPageFreeze pagefrz; bool hastup = false; - bool all_visible, - all_frozen; - TransactionId visibility_cutoff_xid; + bool all_frozen; int64 fpi_before = pgWalUsage.wal_fpi; OffsetNumber deadoffsets[MaxHeapTuplesPerPage]; HeapTupleFreeze frozen[MaxHeapTuplesPerPage]; @@ -1465,17 +1463,16 @@ lazy_scan_prune(LVRelState *vacrel, &presult, &vacrel->offnum); /* - * We will update the VM after collecting LP_DEAD items and freezing - * tuples. Keep track of whether or not the page is all_visible and - * all_frozen and use this information to update the VM. all_visible - * implies 0 lpdead_items, but don't trust all_frozen result unless - * all_visible is also set to true. + * Now scan the page to collect LP_DEAD items and check for tuples + * requiring freezing among remaining tuples with storage. We will update + * the VM after collecting LP_DEAD items and freezing tuples. Pruning will + * have determined whether or not the page is all_visible. Keep track of + * whether or not the page is all_frozen and use this information to + * update the VM. all_visible implies lpdead_items == 0, but don't trust + * all_frozen result unless all_visible is also set to true. * - * Also keep track of the visibility cutoff xid for recovery conflicts. */ - all_visible = true; all_frozen = true; - visibility_cutoff_xid = InvalidTransactionId; /* * Now scan the page to collect LP_DEAD items and update the variables set @@ -1516,11 +1513,6 @@ lazy_scan_prune(LVRelState *vacrel, * will only happen every other VACUUM, at most. Besides, VACUUM * must treat hastup/nonempty_pages as provisional no matter how * LP_DEAD items are handled (handled here, or handled later on). - * - * Also deliberately delay unsetting all_visible until just before - * we return to lazy_scan_heap caller, as explained in full below. - * (This is another case where it's useful to anticipate that any - * LP_DEAD items will become LP_UNUSED during the ongoing VACUUM.) */ deadoffsets[lpdead_items++] = offnum; continue; @@ -1558,46 +1550,6 @@ lazy_scan_prune(LVRelState *vacrel, * what acquire_sample_rows() does. */ live_tuples++; - - /* - * Is the tuple definitely visible to all transactions? - * - * NB: Like with per-tuple hint bits, we can't set the - * PD_ALL_VISIBLE flag if the inserter committed - * asynchronously. See SetHintBits for more info. Check that - * the tuple is hinted xmin-committed because of that. - */ - if (all_visible) - { - TransactionId xmin; - - if (!HeapTupleHeaderXminCommitted(htup)) - { - all_visible = false; - break; - } - - /* - * The inserter definitely committed. But is it old enough - * that everyone sees it as committed? A - * FrozenTransactionId is seen as committed to everyone. - * Otherwise, we check if there is a snapshot that - * considers this xid to still be running, and if so, we - * don't consider the page all-visible. - */ - xmin = HeapTupleHeaderGetXmin(htup); - if (xmin != FrozenTransactionId && - !GlobalVisTestIsRemovableXid(vacrel->vistest, xmin)) - { - all_visible = false; - break; - } - - /* Track newest xmin on page. */ - if (TransactionIdFollows(xmin, visibility_cutoff_xid) && - TransactionIdIsNormal(xmin)) - visibility_cutoff_xid = xmin; - } break; case HEAPTUPLE_RECENTLY_DEAD: @@ -1607,7 +1559,6 @@ lazy_scan_prune(LVRelState *vacrel, * pruning.) */ recently_dead_tuples++; - all_visible = false; break; case HEAPTUPLE_INSERT_IN_PROGRESS: @@ -1618,16 +1569,13 @@ lazy_scan_prune(LVRelState *vacrel, * results. This assumption is a bit shaky, but it is what * acquire_sample_rows() does, so be consistent. */ - all_visible = false; break; case HEAPTUPLE_DELETE_IN_PROGRESS: - /* This is an expected case during concurrent vacuum */ - all_visible = false; /* - * Count such rows as live. As above, we assume the deleting - * transaction will commit and update the counters after we - * report. + * This an expected case during concurrent vacuum. Count such + * rows as live. As above, we assume the deleting transaction + * will commit and update the counters after we report. */ live_tuples++; break; @@ -1670,7 +1618,7 @@ lazy_scan_prune(LVRelState *vacrel, * page all-frozen afterwards (might not happen until final heap pass). */ if (pagefrz.freeze_required || tuples_frozen == 0 || - (all_visible && all_frozen && + (presult.all_visible_except_removable && all_frozen && fpi_before != pgWalUsage.wal_fpi)) { /* @@ -1703,16 +1651,16 @@ lazy_scan_prune(LVRelState *vacrel, vacrel->frozen_pages++; /* - * We can use visibility_cutoff_xid as our cutoff for conflicts + * We can use frz_conflict_horizon as our cutoff for conflicts * when the whole page is eligible to become all-frozen in the VM * once we're done with it. Otherwise we generate a conservative * cutoff by stepping back from OldestXmin. */ - if (all_visible && all_frozen) + if (presult.all_visible_except_removable && all_frozen) { /* Using same cutoff when setting VM is now unnecessary */ - snapshotConflictHorizon = visibility_cutoff_xid; - visibility_cutoff_xid = InvalidTransactionId; + snapshotConflictHorizon = presult.frz_conflict_horizon; + presult.frz_conflict_horizon = InvalidTransactionId; } else { @@ -1748,17 +1696,19 @@ lazy_scan_prune(LVRelState *vacrel, */ #ifdef USE_ASSERT_CHECKING /* Note that all_frozen value does not matter when !all_visible */ - if (all_visible && lpdead_items == 0) + if (presult.all_visible) { TransactionId debug_cutoff; bool debug_all_frozen; + Assert(lpdead_items == 0); + if (!heap_page_is_all_visible(vacrel, buf, &debug_cutoff, &debug_all_frozen)) Assert(false); Assert(!TransactionIdIsValid(debug_cutoff) || - debug_cutoff == visibility_cutoff_xid); + debug_cutoff == presult.frz_conflict_horizon); } #endif @@ -1783,19 +1733,6 @@ lazy_scan_prune(LVRelState *vacrel, Assert(dead_items->num_items <= dead_items->max_items); pgstat_progress_update_param(PROGRESS_VACUUM_NUM_DEAD_TUPLES, dead_items->num_items); - - /* - * It was convenient to ignore LP_DEAD items in all_visible earlier on - * to make the choice of whether or not to freeze the page unaffected - * by the short-term presence of LP_DEAD items. These LP_DEAD items - * were effectively assumed to be LP_UNUSED items in the making. It - * doesn't matter which heap pass (initial pass or final pass) ends up - * setting the page all-frozen, as long as the ongoing VACUUM does it. - * - * Now that freezing has been finalized, unset all_visible. It needs - * to reflect the present state of things, as expected by our caller. - */ - all_visible = false; } /* Finally, add page-local counts to whole-VACUUM counts */ @@ -1812,20 +1749,20 @@ lazy_scan_prune(LVRelState *vacrel, /* Did we find LP_DEAD items? */ *has_lpdead_items = (lpdead_items > 0); - Assert(!all_visible || !(*has_lpdead_items)); + Assert(!presult.all_visible || !(*has_lpdead_items)); /* * Handle setting visibility map bit based on information from the VM (as * of last heap_vac_scan_next_block() call), and from all_visible and * all_frozen variables */ - if (!all_visible_according_to_vm && all_visible) + if (!all_visible_according_to_vm && presult.all_visible) { uint8 flags = VISIBILITYMAP_ALL_VISIBLE; if (all_frozen) { - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); flags |= VISIBILITYMAP_ALL_FROZEN; } @@ -1845,7 +1782,7 @@ lazy_scan_prune(LVRelState *vacrel, PageSetAllVisible(page); MarkBufferDirty(buf); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, - vmbuffer, visibility_cutoff_xid, + vmbuffer, presult.frz_conflict_horizon, flags); } @@ -1893,7 +1830,7 @@ lazy_scan_prune(LVRelState *vacrel, * it as all-frozen. Note that all_frozen is only valid if all_visible is * true, so we must check both all_visible and all_frozen. */ - else if (all_visible_according_to_vm && all_visible && + else if (all_visible_according_to_vm && presult.all_visible && all_frozen && !VM_ALL_FROZEN(vacrel->rel, blkno, &vmbuffer)) { /* @@ -1910,11 +1847,11 @@ lazy_scan_prune(LVRelState *vacrel, /* * Set the page all-frozen (and all-visible) in the VM. * - * We can pass InvalidTransactionId as our visibility_cutoff_xid, - * since a snapshotConflictHorizon sufficient to make everything safe - * for REDO was logged when the page's tuples were frozen. + * We can pass InvalidTransactionId as our frz_conflict_horizon, since + * a snapshotConflictHorizon sufficient to make everything safe for + * REDO was logged when the page's tuples were frozen. */ - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, vmbuffer, InvalidTransactionId, VISIBILITYMAP_ALL_VISIBLE | diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 4b133f68593..d8e65ae7e35 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -198,6 +198,9 @@ typedef struct PruneResult { int ndeleted; /* Number of tuples deleted from the page */ int nnewlpdead; /* Number of newly LP_DEAD items */ + bool all_visible; /* Whether or not the page is all visible */ + bool all_visible_except_removable; + TransactionId frz_conflict_horizon; /* Newest xmin on the page */ /* * Tuple visibility is only computed once for each tuple, for correctness -- 2.40.1 --tez7m2a73jtztiij Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v4-0006-Add-reference-to-VacuumCutoffs-in-HeapPageFreeze.patch" ^ permalink raw reply [nested|flat] 7+ messages in thread
* [PATCH v3 03/17] heap_page_prune sets all_visible and frz_conflict_horizon @ 2024-01-06 19:01 Melanie Plageman <melanieplageman@gmail.com> 0 siblings, 0 replies; 7+ messages in thread From: Melanie Plageman @ 2024-01-06 19:01 UTC (permalink / raw) In order to combine the prune and freeze records, we must know if the page is eligible to be opportunistically frozen before finishing pruning. Save all_visible in the PruneResult and set it to false when we see non-removable tuples which are not visible to everyone. We will also need to ensure that the snapshotConflictHorizon for the combined prune + freeze record is the more conservative of that calculated for each of pruning and freezing. Calculate the visibility_cutoff_xid for the purposes of freezing -- the newest xmin on the page -- in heap_page_prune() and save it in PruneResult.frz_conflict_horizon. --- src/backend/access/heap/pruneheap.c | 136 +++++++++++++++++++++++++-- src/backend/access/heap/vacuumlazy.c | 130 ++++++------------------- src/include/access/heapam.h | 3 + 3 files changed, 160 insertions(+), 109 deletions(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 4a2bf3dd780..42fd4a74845 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -65,8 +65,10 @@ static int heap_prune_chain(Buffer buffer, static void heap_prune_record_prunable(PruneState *prstate, TransactionId xid); static void heap_prune_record_redirect(PruneState *prstate, OffsetNumber offnum, OffsetNumber rdoffnum); -static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum); -static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum); +static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); +static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult); static void heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum); static void page_verify_redirects(Page page); @@ -187,6 +189,20 @@ heap_page_prune_opt(Relation relation, Buffer buffer) } +/* + * Wrap GlobalVisTestIsRemovableXid() to handle FrozenTransactionIds when we + * are examining tuple xmins to determine if the page is all-visible during + * pruning. Old tuples may have FrozenTransactionId xmins. + */ +static inline bool +prune_freeze_xmin_is_removable(GlobalVisState *visstate, TransactionId xmin) +{ + if (xmin == FrozenTransactionId) + return true; + + return GlobalVisTestIsRemovableXid(visstate, xmin); +} + /* * Prune and repair fragmentation in the specified page. * @@ -249,6 +265,14 @@ heap_page_prune(Relation relation, Buffer buffer, presult->ndeleted = 0; presult->nnewlpdead = 0; + /* + * Keep track of whether or not the page is all_visible in case the caller + * wants to use this information to update the VM. + */ + presult->all_visible = true; + /* for recovery conflicts */ + presult->frz_conflict_horizon = InvalidTransactionId; + maxoff = PageGetMaxOffsetNumber(page); tup.t_tableOid = RelationGetRelid(prstate.rel); @@ -300,8 +324,92 @@ heap_page_prune(Relation relation, Buffer buffer, presult->htsv[offnum] = heap_prune_satisfies_vacuum(&prstate, &tup, buffer); + switch (presult->htsv[offnum]) + { + case HEAPTUPLE_DEAD: + + /* + * Deliberately delay unsetting all_visible until later during + * pruning. Removable dead tuples shouldn't preclude freezing + * the page. After finishing this first pass of tuple + * visibility checks, initialize all_visible_except_removable + * with the current value of all_visible to indicate whether + * or not the page is all visible except for dead tuples. This + * will allow us to attempt to freeze the page after pruning. + * Later during pruning, if we encounter an LP_DEAD item or + * are setting an item LP_DEAD, we will unset all_visible. As + * long as we unset it before updating the visibility map, + * this will be correct. + */ + break; + case HEAPTUPLE_LIVE: + + /* + * Is the tuple definitely visible to all transactions? + * + * NB: Like with per-tuple hint bits, we can't set the + * PD_ALL_VISIBLE flag if the inserter committed + * asynchronously. See SetHintBits for more info. Check that + * the tuple is hinted xmin-committed because of that. + */ + if (presult->all_visible) + { + TransactionId xmin; + + if (!HeapTupleHeaderXminCommitted(htup)) + { + presult->all_visible = false; + break; + } + + /* + * The inserter definitely committed. But is it old enough + * that everyone sees it as committed? + */ + xmin = HeapTupleHeaderGetXmin(htup); + if (!prune_freeze_xmin_is_removable(vistest, xmin)) + { + presult->all_visible = false; + break; + } + + /* Track newest xmin on page. */ + if (TransactionIdFollows(xmin, presult->frz_conflict_horizon) && + TransactionIdIsNormal(xmin)) + presult->frz_conflict_horizon = xmin; + } + break; + case HEAPTUPLE_RECENTLY_DEAD: + presult->all_visible = false; + break; + case HEAPTUPLE_INSERT_IN_PROGRESS: + presult->all_visible = false; + break; + case HEAPTUPLE_DELETE_IN_PROGRESS: + /* This is an expected case during concurrent vacuum */ + presult->all_visible = false; + break; + default: + elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); + break; + } } + /* + * For vacuum, if the whole page will become frozen, we consider + * opportunistically freezing tuples. Dead tuples which will be removed by + * the end of vacuuming should not preclude us from opportunistically + * freezing. We will not be able to freeze the whole page if there are + * tuples present which are not visible to everyone or if there are dead + * tuples which are not yet removable. We need all_visible to be false if + * LP_DEAD tuples remain after pruning so that we do not incorrectly + * update the visibility map or page hint bit. So, we will update + * presult->all_visible to reflect the presence of LP_DEAD items while + * pruning and keep all_visible_except_removable to permit freezing if the + * whole page will eventually become all visible after removing tuples. + */ + presult->all_visible_except_removable = presult->all_visible; + /* Scan the page */ for (offnum = FirstOffsetNumber; offnum <= maxoff; @@ -596,10 +704,14 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, /* * If the caller set mark_unused_now true, we can set dead line * pointers LP_UNUSED now. We don't increment ndeleted here since - * the LP was already marked dead. + * the LP was already marked dead. If it will not be marked + * LP_UNUSED, it will remain LP_DEAD, making the page not + * all_visible. */ if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); + else + presult->all_visible = false; break; } @@ -736,7 +848,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect the root to the correct chain member. */ if (i >= nchain) - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); else heap_prune_record_redirect(prstate, rootoffnum, chainitems[i]); } @@ -749,7 +861,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, * redirect item. We can clean up by setting the redirect item to * DEAD state or LP_UNUSED if the caller indicated. */ - heap_prune_record_dead_or_unused(prstate, rootoffnum); + heap_prune_record_dead_or_unused(prstate, rootoffnum, presult); } return ndeleted; @@ -786,13 +898,20 @@ heap_prune_record_redirect(PruneState *prstate, /* Record line pointer to be marked dead */ static void -heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { Assert(prstate->ndead < MaxHeapTuplesPerPage); prstate->nowdead[prstate->ndead] = offnum; prstate->ndead++; Assert(!prstate->marked[offnum]); prstate->marked[offnum] = true; + + /* + * Setting the line pointer LP_DEAD means the page will definitely not be + * all_visible. + */ + presult->all_visible = false; } /* @@ -802,7 +921,8 @@ heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum) * pointers LP_DEAD if mark_unused_now is true. */ static void -heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) +heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, + PruneResult *presult) { /* * If the caller set mark_unused_now to true, we can remove dead tuples @@ -813,7 +933,7 @@ heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum) if (unlikely(prstate->mark_unused_now)) heap_prune_record_unused(prstate, offnum); else - heap_prune_record_dead(prstate, offnum); + heap_prune_record_dead(prstate, offnum, presult); } /* Record line pointer to be marked unused */ diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index fe31c0125d6..f9892f4cd08 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -1373,20 +1373,6 @@ lazy_scan_new_or_empty(LVRelState *vacrel, Buffer buf, BlockNumber blkno, return false; } -/* - * Wrap GlobalVisTestIsRemovableXid() to handle FrozenTransactionIds when we - * are examining tuple xmins to determine if the page is all-visible during - * pruning. Old tuples may have FrozenTransactionId xmins. - */ -static inline bool -prune_freeze_xmin_is_removable(GlobalVisState *visstate, TransactionId xmin) -{ - if (xmin == FrozenTransactionId) - return true; - - return GlobalVisTestIsRemovableXid(visstate, xmin); -} - /* * lazy_scan_prune() -- lazy_scan_heap() pruning and freezing. * @@ -1436,9 +1422,7 @@ lazy_scan_prune(LVRelState *vacrel, recently_dead_tuples; HeapPageFreeze pagefrz; bool hastup = false; - bool all_visible, - all_frozen; - TransactionId visibility_cutoff_xid; + bool all_frozen; int64 fpi_before = pgWalUsage.wal_fpi; OffsetNumber deadoffsets[MaxHeapTuplesPerPage]; HeapTupleFreeze frozen[MaxHeapTuplesPerPage]; @@ -1479,17 +1463,16 @@ lazy_scan_prune(LVRelState *vacrel, &presult, &vacrel->offnum); /* - * We will update the VM after collecting LP_DEAD items and freezing - * tuples. Keep track of whether or not the page is all_visible and - * all_frozen and use this information to update the VM. all_visible - * implies 0 lpdead_items, but don't trust all_frozen result unless - * all_visible is also set to true. + * Now scan the page to collect LP_DEAD items and check for tuples + * requiring freezing among remaining tuples with storage. We will update + * the VM after collecting LP_DEAD items and freezing tuples. Pruning will + * have determined whether or not the page is all_visible. Keep track of + * whether or not the page is all_frozen and use this information to + * update the VM. all_visible implies lpdead_items == 0, but don't trust + * all_frozen result unless all_visible is also set to true. * - * Also keep track of the visibility cutoff xid for recovery conflicts. */ - all_visible = true; all_frozen = true; - visibility_cutoff_xid = InvalidTransactionId; /* * Now scan the page to collect LP_DEAD items and update the variables set @@ -1530,11 +1513,6 @@ lazy_scan_prune(LVRelState *vacrel, * will only happen every other VACUUM, at most. Besides, VACUUM * must treat hastup/nonempty_pages as provisional no matter how * LP_DEAD items are handled (handled here, or handled later on). - * - * Also deliberately delay unsetting all_visible until just before - * we return to lazy_scan_heap caller, as explained in full below. - * (This is another case where it's useful to anticipate that any - * LP_DEAD items will become LP_UNUSED during the ongoing VACUUM.) */ deadoffsets[lpdead_items++] = offnum; continue; @@ -1572,41 +1550,6 @@ lazy_scan_prune(LVRelState *vacrel, * what acquire_sample_rows() does. */ live_tuples++; - - /* - * Is the tuple definitely visible to all transactions? - * - * NB: Like with per-tuple hint bits, we can't set the - * PD_ALL_VISIBLE flag if the inserter committed - * asynchronously. See SetHintBits for more info. Check that - * the tuple is hinted xmin-committed because of that. - */ - if (all_visible) - { - TransactionId xmin; - - if (!HeapTupleHeaderXminCommitted(htup)) - { - all_visible = false; - break; - } - - /* - * The inserter definitely committed. But is it old enough - * that everyone sees it as committed? - */ - xmin = HeapTupleHeaderGetXmin(htup); - if (!prune_freeze_xmin_is_removable(vacrel->vistest, xmin)) - { - all_visible = false; - break; - } - - /* Track newest xmin on page. */ - if (TransactionIdFollows(xmin, visibility_cutoff_xid) && - TransactionIdIsNormal(xmin)) - visibility_cutoff_xid = xmin; - } break; case HEAPTUPLE_RECENTLY_DEAD: @@ -1616,7 +1559,6 @@ lazy_scan_prune(LVRelState *vacrel, * pruning.) */ recently_dead_tuples++; - all_visible = false; break; case HEAPTUPLE_INSERT_IN_PROGRESS: @@ -1627,16 +1569,13 @@ lazy_scan_prune(LVRelState *vacrel, * results. This assumption is a bit shaky, but it is what * acquire_sample_rows() does, so be consistent. */ - all_visible = false; break; case HEAPTUPLE_DELETE_IN_PROGRESS: - /* This is an expected case during concurrent vacuum */ - all_visible = false; /* - * Count such rows as live. As above, we assume the deleting - * transaction will commit and update the counters after we - * report. + * This an expected case during concurrent vacuum. Count such + * rows as live. As above, we assume the deleting transaction + * will commit and update the counters after we report. */ live_tuples++; break; @@ -1679,7 +1618,7 @@ lazy_scan_prune(LVRelState *vacrel, * page all-frozen afterwards (might not happen until final heap pass). */ if (pagefrz.freeze_required || tuples_frozen == 0 || - (all_visible && all_frozen && + (presult.all_visible_except_removable && all_frozen && fpi_before != pgWalUsage.wal_fpi)) { /* @@ -1712,16 +1651,16 @@ lazy_scan_prune(LVRelState *vacrel, vacrel->frozen_pages++; /* - * We can use visibility_cutoff_xid as our cutoff for conflicts + * We can use frz_conflict_horizon as our cutoff for conflicts * when the whole page is eligible to become all-frozen in the VM * once we're done with it. Otherwise we generate a conservative * cutoff by stepping back from OldestXmin. */ - if (all_visible && all_frozen) + if (presult.all_visible_except_removable && all_frozen) { /* Using same cutoff when setting VM is now unnecessary */ - snapshotConflictHorizon = visibility_cutoff_xid; - visibility_cutoff_xid = InvalidTransactionId; + snapshotConflictHorizon = presult.frz_conflict_horizon; + presult.frz_conflict_horizon = InvalidTransactionId; } else { @@ -1757,17 +1696,19 @@ lazy_scan_prune(LVRelState *vacrel, */ #ifdef USE_ASSERT_CHECKING /* Note that all_frozen value does not matter when !all_visible */ - if (all_visible && lpdead_items == 0) + if (presult.all_visible) { TransactionId debug_cutoff; bool debug_all_frozen; + Assert(lpdead_items == 0); + if (!heap_page_is_all_visible(vacrel, buf, &debug_cutoff, &debug_all_frozen)) Assert(false); Assert(!TransactionIdIsValid(debug_cutoff) || - debug_cutoff == visibility_cutoff_xid); + debug_cutoff == presult.frz_conflict_horizon); } #endif @@ -1792,19 +1733,6 @@ lazy_scan_prune(LVRelState *vacrel, Assert(dead_items->num_items <= dead_items->max_items); pgstat_progress_update_param(PROGRESS_VACUUM_NUM_DEAD_TUPLES, dead_items->num_items); - - /* - * It was convenient to ignore LP_DEAD items in all_visible earlier on - * to make the choice of whether or not to freeze the page unaffected - * by the short-term presence of LP_DEAD items. These LP_DEAD items - * were effectively assumed to be LP_UNUSED items in the making. It - * doesn't matter which heap pass (initial pass or final pass) ends up - * setting the page all-frozen, as long as the ongoing VACUUM does it. - * - * Now that freezing has been finalized, unset all_visible. It needs - * to reflect the present state of things, as expected by our caller. - */ - all_visible = false; } /* Finally, add page-local counts to whole-VACUUM counts */ @@ -1821,20 +1749,20 @@ lazy_scan_prune(LVRelState *vacrel, /* Did we find LP_DEAD items? */ *has_lpdead_items = (lpdead_items > 0); - Assert(!all_visible || !(*has_lpdead_items)); + Assert(!presult.all_visible || !(*has_lpdead_items)); /* * Handle setting visibility map bit based on information from the VM (as * of last heap_vac_scan_next_block() call), and from all_visible and * all_frozen variables */ - if (!all_visible_according_to_vm && all_visible) + if (!all_visible_according_to_vm && presult.all_visible) { uint8 flags = VISIBILITYMAP_ALL_VISIBLE; if (all_frozen) { - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); flags |= VISIBILITYMAP_ALL_FROZEN; } @@ -1854,7 +1782,7 @@ lazy_scan_prune(LVRelState *vacrel, PageSetAllVisible(page); MarkBufferDirty(buf); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, - vmbuffer, visibility_cutoff_xid, + vmbuffer, presult.frz_conflict_horizon, flags); } @@ -1902,7 +1830,7 @@ lazy_scan_prune(LVRelState *vacrel, * it as all-frozen. Note that all_frozen is only valid if all_visible is * true, so we must check both all_visible and all_frozen. */ - else if (all_visible_according_to_vm && all_visible && + else if (all_visible_according_to_vm && presult.all_visible && all_frozen && !VM_ALL_FROZEN(vacrel->rel, blkno, &vmbuffer)) { /* @@ -1919,11 +1847,11 @@ lazy_scan_prune(LVRelState *vacrel, /* * Set the page all-frozen (and all-visible) in the VM. * - * We can pass InvalidTransactionId as our visibility_cutoff_xid, - * since a snapshotConflictHorizon sufficient to make everything safe - * for REDO was logged when the page's tuples were frozen. + * We can pass InvalidTransactionId as our frz_conflict_horizon, since + * a snapshotConflictHorizon sufficient to make everything safe for + * REDO was logged when the page's tuples were frozen. */ - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + Assert(!TransactionIdIsValid(presult.frz_conflict_horizon)); visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, vmbuffer, InvalidTransactionId, VISIBILITYMAP_ALL_VISIBLE | diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 4b133f68593..d8e65ae7e35 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -198,6 +198,9 @@ typedef struct PruneResult { int ndeleted; /* Number of tuples deleted from the page */ int nnewlpdead; /* Number of newly LP_DEAD items */ + bool all_visible; /* Whether or not the page is all visible */ + bool all_visible_except_removable; + TransactionId frz_conflict_horizon; /* Newest xmin on the page */ /* * Tuple visibility is only computed once for each tuple, for correctness -- 2.40.1 --racicctn4wry6xe5 Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v3-0004-Add-reference-to-VacuumCutoffs-in-HeapPageFreeze.patch" ^ permalink raw reply [nested|flat] 7+ messages in thread
* [PATCH 1/2] Error out any process that would block at REPACK @ 2026-04-01 15:35 Antonin Houska <ah@cybertec.at> 0 siblings, 0 replies; 7+ messages in thread From: Antonin Houska @ 2026-04-01 15:35 UTC (permalink / raw) Any process waiting on REPACK to release its lock would actually cause it to deadlock when it tries to upgrade its lock to AEL, losing all work done to that point. We avoid this by teaching the deadlock detector to raise an error when this condition is detected. --- src/backend/commands/repack.c | 47 ++++++++--- src/backend/storage/lmgr/deadlock.c | 15 ++++ src/include/storage/proc.h | 6 +- src/test/modules/injection_points/Makefile | 1 + .../expected/repack_deadlock.out | 63 ++++++++++++++ src/test/modules/injection_points/meson.build | 1 + .../specs/repack_deadlock.spec | 83 +++++++++++++++++++ 7 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 20dad22c4b7..a5f5df77291 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -285,6 +285,18 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) * to understand and we don't lose any functionality. */ PreventInTransactionBlock(isTopLevel, "REPACK (CONCURRENTLY)"); + + /* + * Also set the PROC_IN_CONCURRENT_REPACK flag. This makes the + * deadlock checker cause anyone that would conflict with us to error + * out. It's important to set this flag ahead of actually locking the + * relation; it won't of course affect anyone until we do have a lock + * that others can conflict with. + */ + LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); + MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK; + ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags; + LWLockRelease(ProcArrayLock); } /* @@ -489,11 +501,8 @@ RepackLockLevel(bool concurrent) * If indexOid is InvalidOid, the table will be rewritten in physical order * instead of index order. * - * Note that, in the concurrent case, the function releases the lock at some - * point, in order to get AccessExclusiveLock for the final steps (i.e. to - * swap the relation files). To make things simpler, the caller should expect - * OldHeap to be closed on return, regardless CLUOPT_CONCURRENT. (The - * AccessExclusiveLock is kept till the end of the transaction.) + * On return, OldHeap is closed but locked with AccessExclusiveLock - the lock + * will be released at end of the transaction. * * 'cmd' indicates which command is being executed, to be used for error * messages. @@ -1002,10 +1011,8 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, * Note that the worker has to wait for all transactions with XID * already assigned to finish. If some of those transactions is * waiting for a lock conflicting with ShareUpdateExclusiveLock on our - * table (e.g. it runs CREATE INDEX), we can end up in a deadlock. - * Not sure this risk is worth unlocking/locking the table (and its - * clustering index) and checking again if it's still eligible for - * REPACK CONCURRENTLY. + * table (e.g. it runs CREATE INDEX), it should encounter ERROR in the + * deadlock checking code. */ start_repack_decoding_worker(tableOid); @@ -3090,7 +3097,19 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock); /* - * Tuples and pages of the old heap will be gone, but the heap will stay. + * Now that we have all access-exclusive locks on all relations, we no + * longer want other processes to error out when trying to acquire a + * conflicting lock. Therefore, unset our flag. + */ + LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); + MyProc->statusFlags &= ~PROC_IN_CONCURRENT_REPACK; + ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags; + LWLockRelease(ProcArrayLock); + + /* + * Tuples and pages of the old heap will be gone, but the heap itself will + * stay. In order for predicate locks to continue to work, convert them + * to relation-level locks. We do this both for table and indexes. */ TransferPredicateLocksToHeapRelation(OldHeap); foreach_ptr(RelationData, index, indexrels) @@ -3364,9 +3383,11 @@ start_repack_decoding_worker(Oid relid) /* * The decoding setup must be done before the caller can have XID assigned - * for any reason, otherwise the worker might end up in a deadlock, - * waiting for the caller's transaction to end. Therefore wait here until - * the worker indicates that it has the logical decoding initialized. + * for any reason, otherwise the worker might end up waiting for the + * caller's transaction to end. (Deadlock detector does not consider this + * a conflict because the worker is in the same locking group as the + * backend that launched it.) Therefore wait here until the worker + * indicates that it has the logical decoding initialized. */ ConditionVariablePrepareToSleep(&shared->cv); for (;;) diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c index b8962d875b6..c20ac682b0d 100644 --- a/src/backend/storage/lmgr/deadlock.c +++ b/src/backend/storage/lmgr/deadlock.c @@ -620,6 +620,21 @@ FindLockCycleRecurseMember(PGPROC *checkProc, proc->statusFlags & PROC_IS_AUTOVACUUM) blocking_autovacuum_proc = proc; + /* + * Similarly, if we note that we're blocked by some + * process running REPACK (CONCURRENTLY), just fail. That + * process is going to upgrade its lock at some point, and + * it would be inappropriate for any other process to + * cause that to fail. + */ + if (checkProc == MyProc && + proc->statusFlags & PROC_IN_CONCURRENT_REPACK) + ereport(ERROR, + errcode(ERRCODE_OBJECT_IN_USE), + errmsg("could not wait for concurrent REPACK"), + errdetail("Process %d waits for REPACK running on process %d", + MyProc->pid, proc->pid)); + /* We're done looking at this proclock */ break; } diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..76c6bb44251 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -70,10 +70,12 @@ struct XidCache #define PROC_AFFECTS_ALL_HORIZONS 0x20 /* this proc's xmin must be * included in vacuum horizons * in all databases */ +#define PROC_IN_CONCURRENT_REPACK 0x40 /* REPACK (CONCURRENTLY) */ -/* flags reset at EOXact */ +/* flags reset at EOXact. A bit of a misnomer ... */ #define PROC_VACUUM_STATE_MASK \ - (PROC_IN_VACUUM | PROC_IN_SAFE_IC | PROC_VACUUM_FOR_WRAPAROUND) + (PROC_IN_VACUUM | PROC_IN_SAFE_IC | PROC_VACUUM_FOR_WRAPAROUND | \ + PROC_IN_CONCURRENT_REPACK) /* * Xmin-related flags. Make sure any flags that affect how the process' Xmin diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index f057d143d1a..13c873969d1 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress ISOLATION = basic \ inplace \ repack \ + repack_deadlock \ repack_toast \ syscache-update-pruned \ heap_lock_update diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out new file mode 100644 index 00000000000..a86e4767536 --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_deadlock.out @@ -0,0 +1,63 @@ +Parsed test spec with 2 sessions + +starting permutation: wait_before_lock add_column wakeup_before_lock check1 +injection_points_attach +----------------------- + +(1 row) + +step wait_before_lock: + REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey; + <waiting ...> +step add_column: + alter table repack_deadlock add column noise text; + <waiting ...> +step add_column: <... completed> +ERROR: could not wait for concurrent REPACK +step wakeup_before_lock: + SELECT injection_points_wakeup('repack-concurrently-before-lock'); + +injection_points_wakeup +----------------------- + +(1 row) + +step wait_before_lock: <... completed> +step check1: + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + SELECT i, j FROM repack_deadlock ORDER BY i, j; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_deadlock; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; + +count +----- + 1 +(1 row) + +i|j +-+- +1|1 +2|2 +3|3 +4|4 +(4 rows) + +count +----- + 4 +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index fb1418e2caa..ead18818c83 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -46,6 +46,7 @@ tests += { 'basic', 'inplace', 'repack', + 'repack_deadlock', 'repack_toast', 'syscache-update-pruned', 'heap_lock_update', diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec new file mode 100644 index 00000000000..9d23a6588c2 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_deadlock.spec @@ -0,0 +1,83 @@ +# Test REPACK with a concurrent transaction that would cause a deadlock +setup +{ + CREATE EXTENSION injection_points; + + CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int); + INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); + + CREATE TABLE relfilenodes(node oid); + + CREATE TABLE data_s1(i int, j int); + CREATE TABLE data_s2(i int, j int); +} + +teardown +{ + DROP TABLE repack_deadlock; + DROP EXTENSION injection_points; + + DROP TABLE relfilenodes; + DROP TABLE data_s1; + DROP TABLE data_s2; +} + +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('repack-concurrently-before-lock', 'wait'); +} +# Perform the initial load and wait for s2 to do some data changes. +step wait_before_lock +{ + REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey; +} +# Check the table from the perspective of s1. +# +# Besides the contents, we also check that relfilenode has changed. + +# Have each session write the contents into a table and use FULL JOIN to check +# if the outputs are identical. +step check1 +{ + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + SELECT i, j FROM repack_deadlock ORDER BY i, j; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_deadlock; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; +} +teardown +{ + SELECT injection_points_detach('repack-concurrently-before-lock'); +} + +session s2 +# Change the existing data. UPDATE changes both key and non-key columns. Also +# update one row twice to test whether tuple version generated by this session +# can be found. +step add_column +{ + alter table repack_deadlock add column noise text; +} + +step wakeup_before_lock +{ + SELECT injection_points_wakeup('repack-concurrently-before-lock'); +} + +# Test if data changes introduced while one session is performing REPACK +# CONCURRENTLY find their way into the table. +permutation + wait_before_lock + add_column + wakeup_before_lock + check1 -- 2.47.3 --kdrcpfmkbkc4lqhu Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="0002-Publish-list-of-tables-being-repacked-in-shared-memo.patch" Content-Transfer-Encoding: 8bit ^ permalink raw reply [nested|flat] 7+ messages in thread
end of thread, other threads:[~2026-04-01 15:35 UTC | newest] Thread overview: 7+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2024-01-06 19:01 [PATCH v3 03/17] heap_page_prune sets all_visible and frz_conflict_horizon Melanie Plageman <melanieplageman@gmail.com> 2024-01-06 19:01 [PATCH v2 03/17] heap_page_prune sets all_visible and frz_conflict_horizon Melanie Plageman <melanieplageman@gmail.com> 2024-01-06 19:01 [PATCH v2 03/17] heap_page_prune sets all_visible and frz_conflict_horizon Melanie Plageman <melanieplageman@gmail.com> 2024-01-06 19:01 [PATCH v4 05/19] heap_page_prune sets all_visible and frz_conflict_horizon Melanie Plageman <melanieplageman@gmail.com> 2024-01-06 19:01 [PATCH v4 05/19] heap_page_prune sets all_visible and frz_conflict_horizon Melanie Plageman <melanieplageman@gmail.com> 2024-01-06 19:01 [PATCH v3 03/17] heap_page_prune sets all_visible and frz_conflict_horizon Melanie Plageman <melanieplageman@gmail.com> 2026-04-01 15:35 [PATCH 1/2] Error out any process that would block at REPACK Antonin Houska <ah@cybertec.at>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox