From aa00b95af14cf1adf6d00856ce5eb071ce02d18b Mon Sep 17 00:00:00 2001 From: Jingtang Zhang Date: Tue, 25 Aug 2026 17:04:21 +0800 Subject: [PATCH v1 2/2] vacuum: Allow freezing without a cleanup lock Allow aggressive vacuum to freeze eligible tuple headers after cleanup lock contention. Keep pages containing old dead tuples on the existing cleanup path so pruning and freezing remain combined when needed. --- src/backend/access/heap/pruneheap.c | 158 ++++++++++++++++++ src/backend/access/heap/vacuumlazy.c | 89 +++++++++- src/include/access/heapam.h | 32 ++++ .../expected/vacuum-no-cleanup-lock.out | 60 +++++++ .../specs/vacuum-no-cleanup-lock.spec | 32 ++++ src/tools/pgindent/typedefs.list | 2 + 6 files changed, 369 insertions(+), 4 deletions(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 90d59ca306f..5d200cef607 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -1465,6 +1465,164 @@ heap_page_prune_and_freeze(PruneFreezeParams *params, new_relfrozen_xid, new_relmin_mxid); } +/* + * Freeze tuples on a page without pruning it. + * + * Caller must hold a pin and an exclusive lock on the page. This function + * neither changes line pointers nor updates pruning hints or the visibility + * map. It is intended for VACUUM to make freeze progress on a page that is + * pinned by another backend and therefore cannot be cleanup locked. + * + * result->needs_cleanup is set when a DEAD tuple has XIDs or MultiXactIds + * that must be removed before the relation's freeze cutoffs can advance. + */ +void +heap_page_freeze_only(HeapPageFreezeParams *params, + HeapPageFreezeResult *result, OffsetNumber *off_loc, + TransactionId *new_relfrozen_xid, + MultiXactId *new_relmin_mxid) +{ + Page page = BufferGetPage(params->buffer); + HeapTupleData tuple; + FreezeState state; + TransactionId dead_relfrozen_xid = *new_relfrozen_xid; + MultiXactId dead_relmin_mxid = *new_relmin_mxid; + TransactionId conflict_xid; + OffsetNumber maxoff; + bool totally_frozen; + + Assert(BufferIsLockedByMeInMode(params->buffer, BUFFER_LOCK_EXCLUSIVE)); + Assert(params->cutoffs != NULL); + Assert(TransactionIdIsValid(*new_relfrozen_xid)); + Assert(MultiXactIdIsValid(*new_relmin_mxid)); + + MemSet(result, 0, sizeof(*result)); + tuple.t_tableOid = RelationGetRelid(params->relation); + maxoff = PageGetMaxOffsetNumber(page); + + /* + * Do not split a later cleanup-lock pass into separate freeze and prune + * WAL records. Save freeze plans locally while examining each tuple, and + * discard them if an old DEAD tuple requires a cleanup-lock pass. Process + * items in reverse order, like prune_freeze_plan(), to improve cache + * locality when tuple data was placed at decreasing offsets. + */ + heap_page_freeze_init(&state, true, + *new_relfrozen_xid, *new_relmin_mxid); + + for (OffsetNumber offnum = maxoff; + offnum >= FirstOffsetNumber; + offnum = OffsetNumberPrev(offnum)) + { + ItemId itemid = PageGetItemId(page, offnum); + HeapTupleHeader tupleheader; + HTSV_Result htsv; + + *off_loc = offnum; + if (!ItemIdIsUsed(itemid)) + continue; + if (ItemIdIsDead(itemid)) + { + result->deadoffsets[result->lpdead_items++] = offnum; + continue; + } + if (ItemIdIsRedirected(itemid)) + { + result->hastup = true; + continue; + } + + Assert(ItemIdIsNormal(itemid)); + result->hastup = true; + tupleheader = (HeapTupleHeader) PageGetItem(page, itemid); + tuple.t_data = tupleheader; + tuple.t_len = ItemIdGetLength(itemid); + ItemPointerSet(&tuple.t_self, + BufferGetBlockNumber(params->buffer), offnum); + + htsv = heap_tuple_satisfies_vacuum(&tuple, params->buffer, + params->vistest, params->cutoffs); + switch (htsv) + { + case HEAPTUPLE_DELETE_IN_PROGRESS: + case HEAPTUPLE_LIVE: + result->live_tuples++; + break; + + case HEAPTUPLE_DEAD: + result->dead_tuples++; + if (heap_tuple_should_freeze(tupleheader, params->cutoffs, + &dead_relfrozen_xid, + &dead_relmin_mxid)) + result->needs_cleanup = true; + break; + + case HEAPTUPLE_RECENTLY_DEAD: + result->recently_dead_tuples++; + break; + + case HEAPTUPLE_INSERT_IN_PROGRESS: + break; + + default: + elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); + break; + } + + if (result->needs_cleanup) + break; + if (htsv == HEAPTUPLE_DEAD) + continue; + + if (heap_prepare_freeze_tuple(tupleheader, params->cutoffs, + &state.pagefrz, + &state.frozen[state.nfrozen], + &totally_frozen)) + state.frozen[state.nfrozen++].offset = offnum; + } + + *off_loc = InvalidOffsetNumber; + if (result->needs_cleanup) + return; + + Assert(state.pagefrz.freeze_required); + + if (state.nfrozen > 0) + { + heap_pre_freeze_checks(params->buffer, state.frozen, state.nfrozen); + Assert(TransactionIdPrecedes(state.pagefrz.FreezePageConflictXid, + params->cutoffs->OldestXmin)); + conflict_xid = state.pagefrz.FreezePageConflictXid; + + START_CRIT_SECTION(); + + heap_freeze_prepared_tuples(params->buffer, state.frozen, + state.nfrozen); + MarkBufferDirty(params->buffer); + + if (RelationNeedsWAL(params->relation)) + log_heap_prune_and_freeze(params->relation, params->buffer, + InvalidBuffer, 0, conflict_xid, + false, PRUNE_VACUUM_SCAN, + state.frozen, state.nfrozen, + NULL, 0, NULL, 0, NULL, 0); + + END_CRIT_SECTION(); + } + + result->nfrozen = state.nfrozen; + heap_page_freeze_update_relstats(&state, + new_relfrozen_xid, new_relmin_mxid); + + if (result->dead_tuples > 0) + { + if (TransactionIdPrecedes(dead_relfrozen_xid, *new_relfrozen_xid)) + *new_relfrozen_xid = dead_relfrozen_xid; + if (MultiXactIdPrecedes(dead_relmin_mxid, *new_relmin_mxid)) + *new_relmin_mxid = dead_relmin_mxid; + } +} + /* Perform visibility checks shared by pruning and freezing. */ static HTSV_Result heap_tuple_satisfies_vacuum(HeapTuple tup, diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 063ef2208de..76844dd9576 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -437,6 +437,9 @@ static int lazy_scan_prune(LVRelState *vacrel, Buffer buf, BlockNumber blkno, Page page, Buffer vmbuffer, bool *has_lpdead_items, bool *vm_page_frozen); +static bool lazy_scan_freeze(LVRelState *vacrel, Buffer buf, + BlockNumber blkno, Page page, + bool *has_lpdead_items); static bool lazy_scan_noprune(LVRelState *vacrel, Buffer buf, BlockNumber blkno, Page page, bool *has_lpdead_items); @@ -1456,13 +1459,22 @@ lazy_scan_heap(LVRelState *vacrel) !lazy_scan_noprune(vacrel, buf, blkno, page, &has_lpdead_items)) { /* - * lazy_scan_noprune could not do all required processing. Wait - * for a cleanup lock, and call lazy_scan_prune in the usual way. + * An aggressive VACUUM needs to freeze this page. First try to + * freeze tuple headers with an ordinary exclusive lock. This can + * make progress on pages pinned by row-lock waiters. Fall back to + * the usual cleanup-lock path only when a dead tuple prevents the + * relation freeze cutoffs from advancing. */ Assert(vacrel->aggressive); LockBuffer(buf, BUFFER_LOCK_UNLOCK); - LockBufferForCleanup(buf); - got_cleanup_lock = true; + LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); + if (!lazy_scan_freeze(vacrel, buf, blkno, page, + &has_lpdead_items)) + { + LockBuffer(buf, BUFFER_LOCK_UNLOCK); + LockBufferForCleanup(buf); + got_cleanup_lock = true; + } } /* @@ -2147,6 +2159,75 @@ lazy_scan_prune(LVRelState *vacrel, return presult.ndeleted; } +/* + * lazy_scan_freeze() -- freeze tuple headers without physical pruning. + * + * Caller must hold a pin and an ordinary exclusive lock on the buffer. + * Returns false when a DEAD tuple prevents advancing the relation's freeze + * cutoffs without a cleanup-lock pass. + */ +static bool +lazy_scan_freeze(LVRelState *vacrel, + Buffer buf, + BlockNumber blkno, + Page page, + bool *has_lpdead_items) +{ + HeapPageFreezeParams params; + HeapPageFreezeResult result; + + Assert(BufferGetBlockNumber(buf) == blkno); + Assert(page == BufferGetPage(buf)); + + params.relation = vacrel->rel; + params.buffer = buf; + params.vistest = vacrel->vistest; + params.cutoffs = &vacrel->cutoffs; + + heap_page_freeze_only(¶ms, &result, + &vacrel->offnum, + &vacrel->NewRelfrozenXid, + &vacrel->NewRelminMxid); + + if (result.needs_cleanup) + return false; + + if (result.nfrozen > 0) + vacrel->new_frozen_tuple_pages++; + + if (result.lpdead_items > 0) + { + if (vacrel->nindexes == 0) + { + result.hastup = true; + result.dead_tuples += result.lpdead_items; + } + else + { + vacrel->lpdead_item_pages++; + qsort(result.deadoffsets, result.lpdead_items, + sizeof(OffsetNumber), cmpOffsetNumbers); + dead_items_add(vacrel, blkno, result.deadoffsets, + result.lpdead_items); + vacrel->lpdead_items += result.lpdead_items; + } + } + + vacrel->tuples_frozen += result.nfrozen; + vacrel->live_tuples += result.live_tuples; + vacrel->recently_dead_tuples += result.recently_dead_tuples; + vacrel->missed_dead_tuples += result.dead_tuples; + if (result.dead_tuples > 0) + vacrel->missed_dead_pages++; + + if (result.hastup) + vacrel->nonempty_pages = blkno + 1; + + *has_lpdead_items = (result.lpdead_items > 0); + + return true; +} + /* * lazy_scan_noprune() -- lazy_scan_prune() without pruning or freezing * diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 5176478c295..f389373e913 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -337,6 +337,33 @@ typedef struct PruneFreezeResult OffsetNumber deadoffsets[MaxHeapTuplesPerPage]; } PruneFreezeResult; +/* + * Input parameters for pure freezing. + */ +typedef struct HeapPageFreezeParams +{ + Relation relation; + Buffer buffer; + GlobalVisState *vistest; + VacuumCutoffs *cutoffs; +} HeapPageFreezeParams; + +/* + * Per-page result from pure freezing. + */ +typedef struct HeapPageFreezeResult +{ + int nfrozen; /* Number of tuples frozen on the page */ + int live_tuples; /* Number of live tuples on the page */ + int recently_dead_tuples; /* Number of recently dead tuples */ + int dead_tuples; /* Number of DEAD tuples left unpruned */ + int lpdead_items; /* Number of existing LP_DEAD items */ + bool hastup; /* Page prevents relation truncation */ + bool needs_cleanup; /* DEAD tuple requires pruning before + * advancing freeze cutoffs */ + OffsetNumber deadoffsets[MaxHeapTuplesPerPage]; +} HeapPageFreezeResult; + /* ---------------- * function prototypes for heap access method @@ -449,6 +476,11 @@ extern void heap_page_prune_and_freeze(PruneFreezeParams *params, OffsetNumber *off_loc, TransactionId *new_relfrozen_xid, MultiXactId *new_relmin_mxid); +extern void heap_page_freeze_only(HeapPageFreezeParams *params, + HeapPageFreezeResult *result, + OffsetNumber *off_loc, + TransactionId *new_relfrozen_xid, + MultiXactId *new_relmin_mxid); extern void heap_page_prune_execute(Buffer buffer, bool lp_truncate_only, OffsetNumber *redirected, int nredirected, OffsetNumber *nowdead, int ndead, diff --git a/src/test/isolation/expected/vacuum-no-cleanup-lock.out b/src/test/isolation/expected/vacuum-no-cleanup-lock.out index f7bc93e8f1c..358f29e1081 100644 --- a/src/test/isolation/expected/vacuum-no-cleanup-lock.out +++ b/src/test/isolation/expected/vacuum-no-cleanup-lock.out @@ -147,6 +147,66 @@ step pinholder_commit: COMMIT; +starting permutation: dml_insert dml_insert dml_insert dml_insert dml_insert dml_insert dml_insert dml_insert vacuumer_horizon_is_old pinholder_cursor vacuumer_aggressive_vacuum vacuumer_horizon_is_advanced pinholder_commit +step dml_insert: + INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl; + +step dml_insert: + INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl; + +step dml_insert: + INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl; + +step dml_insert: + INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl; + +step dml_insert: + INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl; + +step dml_insert: + INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl; + +step dml_insert: + INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl; + +step dml_insert: + INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl; + +step vacuumer_horizon_is_old: + SELECT age(relfrozenxid) > 5 AS old FROM pg_class + WHERE oid = 'smalltbl'::regclass; + +old +--- +t +(1 row) + +step pinholder_cursor: + BEGIN; + DECLARE c1 CURSOR FOR SELECT 1 AS dummy FROM smalltbl; + FETCH NEXT FROM c1; + +dummy +----- + 1 +(1 row) + +step vacuumer_aggressive_vacuum: + VACUUM (FREEZE) smalltbl; + +step vacuumer_horizon_is_advanced: + SELECT age(relfrozenxid) <= 2 AS frozen FROM pg_class + WHERE oid = 'smalltbl'::regclass; + +frozen +------ +t +(1 row) + +step pinholder_commit: + COMMIT; + + starting permutation: dml_begin dml_other_begin dml_key_share dml_other_key_share vacuumer_nonaggressive_vacuum pinholder_cursor dml_other_update dml_commit dml_other_commit vacuumer_nonaggressive_vacuum pinholder_commit vacuumer_nonaggressive_vacuum step dml_begin: BEGIN; step dml_other_begin: BEGIN; diff --git a/src/test/isolation/specs/vacuum-no-cleanup-lock.spec b/src/test/isolation/specs/vacuum-no-cleanup-lock.spec index 05fd280f6a8..afc14170715 100644 --- a/src/test/isolation/specs/vacuum-no-cleanup-lock.spec +++ b/src/test/isolation/specs/vacuum-no-cleanup-lock.spec @@ -67,10 +67,24 @@ step vacuumer_nonaggressive_vacuum { VACUUM smalltbl; } +step vacuumer_aggressive_vacuum +{ + VACUUM (FREEZE) smalltbl; +} step vacuumer_pg_class_stats { SELECT relpages, reltuples FROM pg_class WHERE oid = 'smalltbl'::regclass; } +step vacuumer_horizon_is_old +{ + SELECT age(relfrozenxid) > 5 AS old FROM pg_class + WHERE oid = 'smalltbl'::regclass; +} +step vacuumer_horizon_is_advanced +{ + SELECT age(relfrozenxid) <= 2 AS frozen FROM pg_class + WHERE oid = 'smalltbl'::regclass; +} # Test VACUUM's reltuples counting mechanism. # @@ -123,6 +137,24 @@ permutation vacuumer_pg_class_stats pinholder_commit # order doesn't matter +# Test that aggressive VACUUM can freeze an old tuple without waiting for a +# cleanup lock. The cursor keeps a pin on the only heap page until after the +# freeze and check complete. +permutation + dml_insert + dml_insert + dml_insert + dml_insert + dml_insert + dml_insert + dml_insert + dml_insert + vacuumer_horizon_is_old + pinholder_cursor + vacuumer_aggressive_vacuum + vacuumer_horizon_is_advanced + pinholder_commit + # Test VACUUM's mechanism for skipping MultiXact freezing. # # This provides test coverage for code paths that are only hit when we need to diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d419c9ea401..5aecd00497c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1268,6 +1268,8 @@ HeadlineWordEntry HeapCheckContext HeapCheckReadStreamData HeapPageFreeze +HeapPageFreezeParams +HeapPageFreezeResult HeapScanDesc HeapScanDescData HeapTuple -- 2.43.7