typedefstruct LVRelState
{ /* Target heap relation and its indexes */
Relation rel;
Relation *indrels; int nindexes;
/* Buffer access strategy and parallel vacuum state */
BufferAccessStrategy bstrategy;
ParallelVacuumState *pvs;
/* Aggressive VACUUM? (must set relfrozenxid >= FreezeLimit) */ bool aggressive; /* Use visibility map to skip? (disabled by DISABLE_PAGE_SKIPPING) */ bool skipwithvm; /* Consider index vacuuming bypass optimization? */ bool consider_bypass_optimization;
/* Doing index vacuuming, index cleanup, rel truncation? */ bool do_index_vacuuming; bool do_index_cleanup; bool do_rel_truncate;
/* VACUUM operation's cutoffs for freezing and pruning */ struct VacuumCutoffs cutoffs;
GlobalVisState *vistest; /* Tracks oldest extant XID/MXID for setting relfrozenxid/relminmxid */
TransactionId NewRelfrozenXid;
MultiXactId NewRelminMxid; bool skippedallvis;
/* Error reporting state */ char *dbname; char *relnamespace; char *relname; char *indname; /* Current index name */
BlockNumber blkno; /* used only for heap operations */
OffsetNumber offnum; /* used only for heap operations */
VacErrPhase phase; bool verbose; /* VACUUM VERBOSE? */
/* # all-visible pages newly set all-frozen in the VM */
BlockNumber vm_new_frozen_pages;
BlockNumber lpdead_item_pages; /* # pages with LP_DEAD items */
BlockNumber missed_dead_pages; /* # pages with missed dead tuples */
BlockNumber nonempty_pages; /* actually, last nonempty page + 1 */
/* Statistics output by us, for table */ double new_rel_tuples; /* new estimated total # of tuples */ double new_live_tuples; /* new estimated total # of live tuples */ /* Statistics output by index AMs */
IndexBulkDeleteResult **indstats;
/* Instrumentation counters */ int num_index_scans; /* Counters that follow are only for scanned_pages */
int64 tuples_deleted; /* # deleted from table */
int64 tuples_frozen; /* # newly frozen */
int64 lpdead_items; /* # deleted from indexes */
int64 live_tuples; /* # live tuples remaining */
int64 recently_dead_tuples; /* # dead, but not yet removable */
int64 missed_dead_tuples; /* # removable, but not removed */
/* State maintained by heap_vac_scan_next_block() */
BlockNumber current_block; /* last block returned */
BlockNumber next_unskippable_block; /* next unskippable block */ bool next_unskippable_allvis; /* its visibility status */ bool next_unskippable_eager_scanned; /* if it was eagerly scanned */
Buffer next_unskippable_vmbuffer; /* buffer containing its VM bit */
/* State related to managing eager scanning of all-visible pages */
/* Set up high level stuff about rel and its indexes */
vacrel->rel = rel;
vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
&vacrel->indrels);
vacrel->bstrategy = bstrategy; if (instrument && vacrel->nindexes > 0)
{ /* Copy index names used by instrumentation (not error reporting) */
indnames = palloc(sizeof(char *) * vacrel->nindexes); for (int i = 0; i < vacrel->nindexes; i++)
indnames[i] = pstrdup(RelationGetRelationName(vacrel->indrels[i]));
}
initStringInfo(&buf); if (verbose)
{
/*
* Aggressiveness already reported earlier, in dedicated
* VACUUM VERBOSE ereport
*/
Assert(!params->is_wraparound);
msgfmt = _("finished vacuuming \"%s.%s.%s\": index scans: %d\n");
}
else if (params->is_wraparound)
{
/*
* While it's possible for a VACUUM to be both is_wraparound
* and !aggressive, that's just a corner-case -- is_wraparound
* implies aggressive. Produce distinct output for the corner
* case all the same, just in case.
*/
if (vacrel->aggressive)
msgfmt = _("automatic aggressive vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n");
else
msgfmt = _("automatic vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n");
}
else
{
if (vacrel->aggressive)
msgfmt = _("automatic aggressive vacuum of table \"%s.%s.%s\": index scans: %d\n");
else
msgfmt = _("automatic vacuum of table \"%s.%s.%s\": index scans: %d\n");
}
appendStringInfo(&buf, msgfmt,
vacrel->dbname,
vacrel->relnamespace,
vacrel->relname,
vacrel->num_index_scans);
appendStringInfo(&buf, _("pages: %u removed, %u remain, %u scanned (%.2f%% of total), %u eagerly scanned\n"),
vacrel->removed_pages,
new_rel_pages,
vacrel->scanned_pages,
orig_rel_pages == 0 ? 100.0 : 100.0 * vacrel->scanned_pages /
orig_rel_pages,
vacrel->eager_scanned_pages);
appendStringInfo(&buf,
_("tuples: %" PRId64 " removed, %" PRId64 " remain, %" PRId64 " are dead but not yet removable\n"),
vacrel->tuples_deleted,
(int64) vacrel->new_rel_tuples,
vacrel->recently_dead_tuples);
if (vacrel->missed_dead_tuples > 0)
appendStringInfo(&buf,
_("tuples missed: %" PRId64 " dead from %u pages not removed due to cleanup lock contention\n"),
vacrel->missed_dead_tuples,
vacrel->missed_dead_pages);
diff = (int32) (ReadNextTransactionId() -
vacrel->cutoffs.OldestXmin);
appendStringInfo(&buf,
_("removable cutoff: %u, which was %d XIDs old when operation ended\n"),
vacrel->cutoffs.OldestXmin, diff);
if (frozenxid_updated)
{
diff = (int32) (vacrel->NewRelfrozenXid -
vacrel->cutoffs.relfrozenxid);
appendStringInfo(&buf,
_("new relfrozenxid: %u, which is %d XIDs ahead of previous value\n"),
vacrel->NewRelfrozenXid, diff);
}
if (minmulti_updated)
{
diff = (int32) (vacrel->NewRelminMxid -
vacrel->cutoffs.relminmxid);
appendStringInfo(&buf,
_("new relminmxid: %u, which is %d MXIDs ahead of previous value\n"),
vacrel->NewRelminMxid, diff);
}
appendStringInfo(&buf, _("frozen: %u pages from table (%.2f%% of total) had %" PRId64 " tuples frozen\n"),
vacrel->new_frozen_tuple_pages,
orig_rel_pages == 0 ? 100.0 : 100.0 * vacrel->new_frozen_tuple_pages /
orig_rel_pages,
vacrel->tuples_frozen);
appendStringInfo(&buf,
_("visibility map: %u pages set all-visible, %u pages set all-frozen (%u were all-visible)\n"),
vacrel->vm_new_visible_pages,
vacrel->vm_new_visible_frozen_pages +
vacrel->vm_new_frozen_pages,
vacrel->vm_new_frozen_pages);
if (vacrel->do_index_vacuuming)
{
if (vacrel->nindexes == 0 || vacrel->num_index_scans == 0)
appendStringInfoString(&buf, _("index scan not needed: "));
else
appendStringInfoString(&buf, _("index scan needed: "));
msgfmt = _("%u pages from table (%.2f%% of total) had %" PRId64 " dead item identifiers removed\n");
}
else
{
if (!VacuumFailsafeActive)
appendStringInfoString(&buf, _("index scan bypassed: "));
else
appendStringInfoString(&buf, _("index scan bypassed by failsafe: "));
msgfmt = _("%u pages from table (%.2f%% of total) have %" PRId64 " dead item identifiers\n");
}
appendStringInfo(&buf, msgfmt,
vacrel->lpdead_item_pages,
orig_rel_pages == 0 ? 100.0 : 100.0 * vacrel->lpdead_item_pages / orig_rel_pages,
vacrel->lpdead_items);
for (int i = 0; i < vacrel->nindexes; i++)
{
IndexBulkDeleteResult *istat = vacrel->indstats[i];
if (!istat)
continue;
appendStringInfo(&buf,
_("index \"%s\": pages: %u in total, %u newly deleted, %u currently deleted, %u reusable\n"),
indnames[i],
istat->num_pages,
istat->pages_newly_deleted,
istat->pages_deleted,
istat->pages_free);
}
if (track_cost_delay_timing)
{
/*
* We bypass the changecount mechanism because this value is
* only updated by the calling process. We also rely on the
* above call to pgstat_progress_end_command() to not clear
* the st_progress_param array.
*/
appendStringInfo(&buf, _("delay time: %.3f ms\n"),
(double) MyBEEntry->st_progress_param[PROGRESS_VACUUM_DELAY_TIME] / 1000000.0);
}
if (track_io_timing)
{
double read_ms = (double) (pgStatBlockReadTime - startreadtime) / 1000;
double write_ms = (double) (pgStatBlockWriteTime - startwritetime) / 1000;
ereport(verbose ? INFO : LOG,
(errmsg_internal("%s", buf.data)));
pfree(buf.data);
}
}
/* Cleanup index statistics and index names */
for (int i = 0; i < vacrel->nindexes; i++)
{
if (vacrel->indstats[i])
pfree(vacrel->indstats[i]);
if (instrument)
pfree(indnames[i]);
}
}
/*
* lazy_scan_heap() -- workhorse function for VACUUM
*
* This routine prunes each page in the heap, and considers the need to
* freeze remaining tuples with storage (not including pages that can be
* skipped using the visibility map). Also performs related maintenance
* of the FSM and visibility map. These steps all take place during an
* initial pass over the target heap relation.
*
* Also invokes lazy_vacuum_all_indexes to vacuum indexes, which largely
* consists of deleting index tuples that point to LP_DEAD items left in
* heap pages following pruning. Earlier initial pass over the heap will
* have collected the TIDs whose index tuples need to be removed.
*
* Finally, invokes lazy_vacuum_heap_rel to vacuum heap pages, which
* largely consists of marking LP_DEAD items (from vacrel->dead_items)
* as LP_UNUSED. This has to happen in a second, final pass over the
* heap, to preserve a basic invariant that all index AMs rely on: no
* extant index tuple can ever be allowed to contain a TID that points to
* an LP_UNUSED line pointer in the heap. We must disallow premature
* recycling of line pointers to avoid index scans that get confused
* about which TID points to which tuple immediately after recycling.
* (Actually, this isn't a concern when target heap relation happens to
* have no indexes, which allows us to safely apply the one-pass strategy
* as an optimization).
*
* In practice we often have enough space to fit all TIDs, and so won't
* need to call lazy_vacuum more than once, after our initial pass over
* the heap has totally finished. Otherwise things are slightly more
* complicated: our "initial pass" over the heap applies only to those
* pages that were pruned before we needed to call lazy_vacuum, and our
* "final pass" over the heap only vacuums these same heap pages.
* However, we process indexes in full every time lazy_vacuum is called,
* which makes index processing very inefficient when memory is in short
* supply.
*/
static void
lazy_scan_heap(LVRelState *vacrel)
{
ReadStream *stream;
BlockNumber rel_pages = vacrel->rel_pages,
blkno = 0,
next_fsm_block_to_vacuum = 0;
BlockNumber orig_eager_scan_success_limit =
vacrel->eager_scan_remaining_successes; /* for logging */
Buffer vmbuffer = InvalidBuffer;
const int initprog_index[] = {
PROGRESS_VACUUM_PHASE,
PROGRESS_VACUUM_TOTAL_HEAP_BLKS,
PROGRESS_VACUUM_MAX_DEAD_TUPLE_BYTES
};
int64 initprog_val[3];
/* Report that we're scanning the heap, advertising total # of blocks */
initprog_val[0] = PROGRESS_VACUUM_PHASE_SCAN_HEAP;
initprog_val[1] = rel_pages;
initprog_val[2] = vacrel->dead_items_info->max_bytes;
pgstat_progress_update_multi_param(3, initprog_index, initprog_val);
/* Initialize for the first heap_vac_scan_next_block() call */
vacrel->current_block = InvalidBlockNumber;
vacrel->next_unskippable_block = InvalidBlockNumber;
vacrel->next_unskippable_allvis = false;
vacrel->next_unskippable_eager_scanned = false;
vacrel->next_unskippable_vmbuffer = InvalidBuffer;
/*
* Set up the read stream for vacuum's first pass through the heap.
*
* This could be made safe for READ_STREAM_USE_BATCHING, but only with
* explicit work in heap_vac_scan_next_block.
*/
stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE,
vacrel->bstrategy,
vacrel->rel,
MAIN_FORKNUM,
heap_vac_scan_next_block,
vacrel,
sizeof(uint8));
/*
* Regularly check if wraparound failsafe should trigger.
*
* There is a similar check inside lazy_vacuum_all_indexes(), but
* relfrozenxid might start to look dangerously old before we reach
* that point. This check also provides failsafe coverage for the
* one-pass strategy, and the two-pass strategy with the index_cleanup
* param set to 'off'.
*/
if (vacrel->scanned_pages > 0 &&
vacrel->scanned_pages % FAILSAFE_EVERY_PAGES == 0)
lazy_check_wraparound_failsafe(vacrel);
/*
* Consider if we definitely have enough space to process TIDs on page
* already. If we are close to overrunning the available space for
* dead_items TIDs, pause and do a cycle of vacuuming before we tackle
* this page. However, let's force at least one page-worth of tuples
* to be stored as to ensure we do at least some work when the memory
* configured is so low that we run out before storing anything.
*/
if (vacrel->dead_items_info->num_items > 0 &&
TidStoreMemoryUsage(vacrel->dead_items) > vacrel->dead_items_info->max_bytes)
{
/*
* Before beginning index vacuuming, we release any pin we may
* hold on the visibility map page. This isn't necessary for
* correctness, but we do it anyway to avoid holding the pin
* across a lengthy, unrelated operation.
*/
if (BufferIsValid(vmbuffer))
{
ReleaseBuffer(vmbuffer);
vmbuffer = InvalidBuffer;
}
/* Perform a round of index and heap vacuuming */
vacrel->consider_bypass_optimization = false;
lazy_vacuum(vacrel);
/*
* Vacuum the Free Space Map to make newly-freed space visible on
* upper-level FSM pages. Note that blkno is the previously
* processed block.
*/
FreeSpaceMapVacuumRange(vacrel->rel, next_fsm_block_to_vacuum,
blkno + 1);
next_fsm_block_to_vacuum = blkno;
/* Report that we are once again scanning the heap */
pgstat_progress_update_param(PROGRESS_VACUUM_PHASE,
PROGRESS_VACUUM_PHASE_SCAN_HEAP);
}
vacrel->scanned_pages++;
if (blk_info & VAC_BLK_WAS_EAGER_SCANNED)
vacrel->eager_scanned_pages++;
/* Report as block scanned, update error traceback information */
pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_SCANNED, blkno);
update_vacuum_error_info(vacrel, NULL, VACUUM_ERRCB_PHASE_SCAN_HEAP,
blkno, InvalidOffsetNumber);
/*
* Pin the visibility map page in case we need to mark the page
* all-visible. In most cases this will be very cheap, because we'll
* already have the correct page pinned anyway.
*/
visibilitymap_pin(vacrel->rel, blkno, &vmbuffer);
/*
* We need a buffer cleanup lock to prune HOT chains and defragment
* the page in lazy_scan_prune. But when it's not possible to acquire
* a cleanup lock right away, we may be able to settle for reduced
* processing using lazy_scan_noprune.
*/
got_cleanup_lock = ConditionalLockBufferForCleanup(buf);
if (!got_cleanup_lock)
LockBuffer(buf, BUFFER_LOCK_SHARE);
/* Check for new or empty pages before lazy_scan_[no]prune call */
if (lazy_scan_new_or_empty(vacrel, buf, blkno, page, !got_cleanup_lock,
vmbuffer))
{
/* Processed as new/empty page (lock and pin released) */
continue;
}
/*
* If we didn't get the cleanup lock, we can still collect LP_DEAD
* items in the dead_items area for later vacuuming, count live and
* recently dead tuples for vacuum logging, and determine if this
* block could later be truncated. If we encounter any xid/mxids that
* require advancing the relfrozenxid/relminxid, we'll have to wait
* for a cleanup lock and call lazy_scan_prune().
*/
if (!got_cleanup_lock &&
!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.
*/
Assert(vacrel->aggressive);
LockBuffer(buf, BUFFER_LOCK_UNLOCK);
LockBufferForCleanup(buf);
got_cleanup_lock = true;
}
/*
* If we have a cleanup lock, we must now prune, freeze, and count
* tuples. We may have acquired the cleanup lock originally, or we may
* have gone back and acquired it after lazy_scan_noprune() returned
* false. Either way, the page hasn't been processed yet.
*
* Like lazy_scan_noprune(), lazy_scan_prune() will count
* recently_dead_tuples and live tuples for vacuum logging, determine
* if the block can later be truncated, and accumulate the details of
* remaining LP_DEAD line pointers on the page into dead_items. These
* dead items include those pruned by lazy_scan_prune() as well as
* line pointers previously marked LP_DEAD.
*/
if (got_cleanup_lock)
ndeleted = lazy_scan_prune(vacrel, buf, blkno, page,
vmbuffer,
blk_info & VAC_BLK_ALL_VISIBLE_ACCORDING_TO_VM,
&has_lpdead_items, &vm_page_frozen);
/*
* Count an eagerly scanned page as a failure or a success.
*
* Only lazy_scan_prune() freezes pages, so if we didn't get the
* cleanup lock, we won't have frozen the page. However, we only count
* pages that were too new to require freezing as eager freeze
* failures.
*
* We could gather more information from lazy_scan_noprune() about
* whether or not there were tuples with XIDs or MXIDs older than the
* FreezeLimit or MultiXactCutoff. However, for simplicity, we simply
* exclude pages skipped due to cleanup lock contention from eager
* freeze algorithm caps.
*/
if (got_cleanup_lock &&
(blk_info & VAC_BLK_WAS_EAGER_SCANNED))
{
/* Aggressive vacuums do not eager scan. */
Assert(!vacrel->aggressive);
if (vm_page_frozen)
{
if (vacrel->eager_scan_remaining_successes > 0)
vacrel->eager_scan_remaining_successes--;
if (vacrel->eager_scan_remaining_successes == 0)
{
/*
* Report only once that we disabled eager scanning. We
* may eagerly read ahead blocks in excess of the success
* or failure caps before attempting to freeze them, so we
* could reach here even after disabling additional eager
* scanning.
*/
if (vacrel->eager_scan_max_fails_per_region > 0)
ereport(vacrel->verbose ? INFO : DEBUG2,
(errmsg("disabling eager scanning after freezing %u eagerly scanned blocks of relation \"%s.%s.%s\"",
orig_eager_scan_success_limit,
vacrel->dbname, vacrel->relnamespace,
vacrel->relname)));
/*
* If we hit our success cap, permanently disable eager
* scanning by setting the other eager scan management
* fields to their disabled values.
*/
vacrel->eager_scan_remaining_fails = 0;
vacrel->next_eager_scan_region_start = InvalidBlockNumber;
vacrel->eager_scan_max_fails_per_region = 0;
}
}
else if (vacrel->eager_scan_remaining_fails > 0)
vacrel->eager_scan_remaining_fails--;
}
/*
* Now drop the buffer lock and, potentially, update the FSM.
*
* Our goal is to update the freespace map the last time we touch the
* page. If we'll process a block in the second pass, we may free up
* additional space on the page, so it is better to update the FSM
* after the second pass. If the relation has no indexes, or if index
* vacuuming is disabled, there will be no second heap pass; if this
* particular page has no dead items, the second heap pass will not
* touch this page. So, in those cases, update the FSM now.
*
* Note: In corner cases, it's possible to miss updating the FSM
* entirely. If index vacuuming is currently enabled, we'll skip the
* FSM update now. But if failsafe mode is later activated, or there
* are so few dead tuples that index vacuuming is bypassed, there will
* also be no opportunity to update the FSM later, because we'll never
* revisit this page. Since updating the FSM is desirable but not
* absolutely required, that's OK.
*/
if (vacrel->nindexes == 0
|| !vacrel->do_index_vacuuming
|| !has_lpdead_items)
{
Size freespace = PageGetHeapFreeSpace(page);
/*
* Periodically perform FSM vacuuming to make newly-freed space
* visible on upper FSM pages. This is done after vacuuming if the
* table has indexes. There will only be newly-freed space if we
* held the cleanup lock and lazy_scan_prune() was called.
*/
if (got_cleanup_lock && vacrel->nindexes == 0 && ndeleted > 0 &&
blkno - next_fsm_block_to_vacuum >= VACUUM_FSM_EVERY_PAGES)
{
FreeSpaceMapVacuumRange(vacrel->rel, next_fsm_block_to_vacuum,
blkno);
next_fsm_block_to_vacuum = blkno;
}
}
else
UnlockReleaseBuffer(buf);
}
vacrel->blkno = InvalidBlockNumber;
if (BufferIsValid(vmbuffer))
ReleaseBuffer(vmbuffer);
/*
* Report that everything is now scanned. We never skip scanning the last
* block in the relation, so we can pass rel_pages here.
*/
pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_SCANNED,
rel_pages);
/* now we can compute the new value for pg_class.reltuples */
vacrel->new_live_tuples = vac_estimate_reltuples(vacrel->rel, rel_pages,
vacrel->scanned_pages,
vacrel->live_tuples);
/*
* Also compute the total number of surviving heap entries. In the
* (unlikely) scenario that new_live_tuples is -1, take it as zero.
*/
vacrel->new_rel_tuples =
Max(vacrel->new_live_tuples, 0) + vacrel->recently_dead_tuples +
vacrel->missed_dead_tuples;
read_stream_end(stream);
/*
* Do index vacuuming (call each index's ambulkdelete routine), then do
* related heap vacuuming
*/
if (vacrel->dead_items_info->num_items > 0)
lazy_vacuum(vacrel);
/*
* Vacuum the remainder of the Free Space Map. We must do this whether or
* not there were indexes, and whether or not we bypassed index vacuuming.
* We can pass rel_pages here because we never skip scanning the last
* block of the relation.
*/
if (rel_pages > next_fsm_block_to_vacuum)
FreeSpaceMapVacuumRange(vacrel->rel, next_fsm_block_to_vacuum, rel_pages);
/* report all blocks vacuumed */
pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_VACUUMED, rel_pages);
/* Do final index cleanup (call each index's amvacuumcleanup routine) */
if (vacrel->nindexes > 0 && vacrel->do_index_cleanup)
lazy_cleanup_all_indexes(vacrel);
}
/*
* heap_vac_scan_next_block() -- read stream callback to get the next block
* for vacuum to process
*
* Every time lazy_scan_heap() needs a new block to process during its first
* phase, it invokes read_stream_next_buffer() with a stream set up to call
* heap_vac_scan_next_block() to get the next block.
*
* heap_vac_scan_next_block() uses the visibility map, vacuum options, and
* various thresholds to skip blocks which do not need to be processed and
* returns the next block to process or InvalidBlockNumber if there are no
* remaining blocks.
*
* The visibility status of the next block to process and whether or not it
* was eager scanned is set in the per_buffer_data.
*
* callback_private_data contains a reference to the LVRelState, passed to the
* read stream API during stream setup. The LVRelState is an in/out parameter
* here (locally named `vacrel`). Vacuum options and information about the
* relation are read from it. vacrel->skippedallvis is set if we skip a block
* that's all-visible but not all-frozen (to ensure that we don't update
* relfrozenxid in that case). vacrel also holds information about the next
* unskippable block -- as bookkeeping for this function.
*/
static BlockNumber
heap_vac_scan_next_block(ReadStream *stream,
void *callback_private_data,
void *per_buffer_data)
{
BlockNumber next_block;
LVRelState *vacrel = callback_private_data;
uint8 blk_info = 0;
/* relies on InvalidBlockNumber + 1 overflowing to 0 on first call */
next_block = vacrel->current_block + 1;
/* Have we reached the end of the relation? */
if (next_block >= vacrel->rel_pages)
{
if (BufferIsValid(vacrel->next_unskippable_vmbuffer))
{
ReleaseBuffer(vacrel->next_unskippable_vmbuffer);
vacrel->next_unskippable_vmbuffer = InvalidBuffer;
}
return InvalidBlockNumber;
}
/*
* We must be in one of the three following states:
*/
if (next_block > vacrel->next_unskippable_block ||
vacrel->next_unskippable_block == InvalidBlockNumber)
{
/*
* 1. We have just processed an unskippable block (or we're at the
* beginning of the scan). Find the next unskippable block using the
* visibility map.
*/
bool skipsallvis;
/*
* We now know the next block that we must process. It can be the
* next block after the one we just processed, or something further
* ahead. If it's further ahead, we can jump to it, but we choose to
* do so only if we can skip at least SKIP_PAGES_THRESHOLD consecutive
* pages. Since we're reading sequentially, the OS should be doing
* readahead for us, so there's no gain in skipping a page now and
* then. Skipping such a range might even discourage sequential
* detection.
*
* This test also enables more frequent relfrozenxid advancement
* during non-aggressive VACUUMs. If the range has any all-visible
* pages then skipping makes updating relfrozenxid unsafe, which is a
* real downside.
*/
if (vacrel->next_unskippable_block - next_block >= SKIP_PAGES_THRESHOLD)
{
next_block = vacrel->next_unskippable_block;
if (skipsallvis)
vacrel->skippedallvis = true;
}
}
/* Now we must be in one of the two remaining states: */
if (next_block < vacrel->next_unskippable_block)
{
/*
* 2. We are processing a range of blocks that we could have skipped
* but chose not to. We know that they are all-visible in the VM,
* otherwise they would've been unskippable.
*/
vacrel->current_block = next_block;
blk_info |= VAC_BLK_ALL_VISIBLE_ACCORDING_TO_VM;
*((uint8 *) per_buffer_data) = blk_info;
return vacrel->current_block;
}
else
{
/*
* 3. We reached the next unskippable block. Process it. On next
* iteration, we will be back in state 1.
*/
Assert(next_block == vacrel->next_unskippable_block);
/*
* Find the next unskippable block in a vacuum scan using the visibility map.
* The next unskippable block and its visibility information is updated in
* vacrel.
*
* Note: our opinion of which blocks can be skipped can go stale immediately.
* It's okay if caller "misses" a page whose all-visible or all-frozen marking
* was concurrently cleared, though. All that matters is that caller scan all
* pages whose tuples might contain XIDs < OldestXmin, or MXIDs < OldestMxact.
* (Actually, non-aggressive VACUUMs can choose to skip all-visible pages with
* older XIDs/MXIDs. The *skippedallvis flag will be set here when the choice
* to skip such a range is actually made, making everything safe.)
*/
static void
find_next_unskippable_block(LVRelState *vacrel, bool *skipsallvis)
{
BlockNumber rel_pages = vacrel->rel_pages;
BlockNumber next_unskippable_block = vacrel->next_unskippable_block + 1;
Buffer next_unskippable_vmbuffer = vacrel->next_unskippable_vmbuffer;
bool next_unskippable_eager_scanned = false;
bool next_unskippable_allvis;
*skipsallvis = false;
for (;; next_unskippable_block++)
{
uint8 mapbits = visibilitymap_get_status(vacrel->rel,
next_unskippable_block,
&next_unskippable_vmbuffer);
/*
* At the start of each eager scan region, normal vacuums with eager
* scanning enabled reset the failure counter, allowing vacuum to
* resume eager scanning if it had been suspended in the previous
* region.
*/
if (next_unskippable_block >= vacrel->next_eager_scan_region_start)
{
vacrel->eager_scan_remaining_fails =
vacrel->eager_scan_max_fails_per_region;
vacrel->next_eager_scan_region_start += EAGER_SCAN_REGION_SIZE;
}
/*
* A block is unskippable if it is not all visible according to the
* visibility map.
*/
if (!next_unskippable_allvis)
{
Assert((mapbits & VISIBILITYMAP_ALL_FROZEN) == 0);
break;
}
/*
* Caller must scan the last page to determine whether it has tuples
* (caller must have the opportunity to set vacrel->nonempty_pages).
* This rule avoids having lazy_truncate_heap() take access-exclusive
* lock on rel to attempt a truncation that fails anyway, just because
* there are tuples on the last page (it is likely that there will be
* tuples on other nearby pages as well, but those can be skipped).
*
* Implement this by always treating the last block as unsafe to skip.
*/
if (next_unskippable_block == rel_pages - 1)
break;
/* DISABLE_PAGE_SKIPPING makes all skipping unsafe */
if (!vacrel->skipwithvm)
break;
/*
* All-frozen pages cannot contain XIDs < OldestXmin (XIDs that aren't
* already frozen by now), so this page can be skipped.
*/
if ((mapbits & VISIBILITYMAP_ALL_FROZEN) != 0)
continue;
/*
* Aggressive vacuums cannot skip any all-visible pages that are not
* also all-frozen.
*/
if (vacrel->aggressive)
break;
/*
* Normal vacuums with eager scanning enabled only skip all-visible
* but not all-frozen pages if they have hit the failure limit for the
* current eager scan region.
*/
if (vacrel->eager_scan_remaining_fails > 0)
{
next_unskippable_eager_scanned = true;
break;
}
/*
* All-visible blocks are safe to skip in a normal vacuum. But
* remember that the final range contains such a block for later.
*/
*skipsallvis = true;
}
/* write the local variables back to vacrel */
vacrel->next_unskippable_block = next_unskippable_block;
vacrel->next_unskippable_allvis = next_unskippable_allvis;
vacrel->next_unskippable_eager_scanned = next_unskippable_eager_scanned;
vacrel->next_unskippable_vmbuffer = next_unskippable_vmbuffer;
}
/*
* lazy_scan_new_or_empty() -- lazy_scan_heap() new/empty page handling.
*
* Must call here to handle both new and empty pages before calling
* lazy_scan_prune or lazy_scan_noprune, since they're not prepared to deal
* with new or empty pages.
*
* It's necessary to consider new pages as a special case, since the rules for
* maintaining the visibility map and FSM with empty pages are a little
* different (though new pages can be truncated away during rel truncation).
*
* Empty pages are not really a special case -- they're just heap pages that
* have no allocated tuples (including even LP_UNUSED items). You might
* wonder why we need to handle them here all the same. It's only necessary
* because of a corner-case involving a hard crash during heap relation
* extension. If we ever make relation-extension crash safe, then it should
* no longer be necessary to deal with empty pages here (or new pages, for
* that matter).
*
* Caller must hold at least a shared lock. We might need to escalate the
* lock in that case, so the type of lock caller holds needs to be specified
* using 'sharelock' argument.
*
* Returns false in common case where caller should go on to call
* lazy_scan_prune (or lazy_scan_noprune). Otherwise returns true, indicating
* that lazy_scan_heap is done processing the page, releasing lock on caller's
* behalf.
*
* No vm_page_frozen output parameter (like that passed to lazy_scan_prune())
* is passed here because neither empty nor new pages can be eagerly frozen.
* New pages are never frozen. Empty pages are always set frozen in the VM at
* the same time that they are set all-visible, and we don't eagerly scan
* frozen pages.
*/
static bool
lazy_scan_new_or_empty(LVRelState *vacrel, Buffer buf, BlockNumber blkno,
Page page, bool sharelock, Buffer vmbuffer)
{
Size freespace;
if (PageIsNew(page))
{
/*
* All-zeroes pages can be left over if either a backend extends the
* relation by a single page, but crashes before the newly initialized
* page has been written out, or when bulk-extending the relation
* (which creates a number of empty pages at the tail end of the
* relation), and then enters them into the FSM.
*
* Note we do not enter the page into the visibilitymap. That has the
* downside that we repeatedly visit this page in subsequent vacuums,
* but otherwise we'll never discover the space on a promoted standby.
* The harm of repeated checking ought to normally not be too bad. The
* space usually should be used at some point, otherwise there
* wouldn't be any regular vacuums.
*
* Make sure these pages are in the FSM, to ensure they can be reused.
* Do that by testing if there's any space recorded for the page. If
* not, enter it. We do so after releasing the lock on the heap page,
* the FSM is approximate, after all.
*/
UnlockReleaseBuffer(buf);
if (PageIsEmpty(page))
{
/*
* It seems likely that caller will always be able to get a cleanup
* lock on an empty page. But don't take any chances -- escalate to
* an exclusive lock (still don't need a cleanup lock, though).
*/
if (sharelock)
{
LockBuffer(buf, BUFFER_LOCK_UNLOCK);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
if (!PageIsEmpty(page))
{
/* page isn't new or empty -- keep lock and pin for now */
return false;
}
}
else
{
/* Already have a full cleanup lock (which is more than enough) */
}
/*
* Unlike new pages, empty pages are always set all-visible and
* all-frozen.
*/
if (!PageIsAllVisible(page))
{
START_CRIT_SECTION();
/* mark buffer dirty before writing a WAL record */
MarkBufferDirty(buf);
/*
* It's possible that another backend has extended the heap,
* initialized the page, and then failed to WAL-log the page due
* to an ERROR. Since heap extension is not WAL-logged, recovery
* might try to replay our record setting the page all-visible and
* find that the page isn't initialized, which will cause a PANIC.
* To prevent that, check whether the page has been previously
* WAL-logged, and if not, do that now.
*/
if (RelationNeedsWAL(vacrel->rel) &&
PageGetLSN(page) == InvalidXLogRecPtr)
log_newpage_buffer(buf, true);
/* page isn't new or empty -- keep lock and pin */
return false;
}
/* qsort comparator for sorting OffsetNumbers */
static int
cmpOffsetNumbers(const void *a, const void *b)
{
return pg_cmp_u16(*(const OffsetNumber *) a, *(const OffsetNumber *) b);
}
/*
* lazy_scan_prune() -- lazy_scan_heap() pruning and freezing.
*
* Caller must hold pin and buffer cleanup lock on the buffer.
*
* vmbuffer is the buffer containing the VM block with visibility information
* for the heap block, blkno. all_visible_according_to_vm is the saved
* visibility status of the heap block looked up earlier by the caller. We
* won't rely entirely on this status, as it may be out of date.
*
* *has_lpdead_items is set to true or false depending on whether, upon return
* from this function, any LP_DEAD items are still present on the page.
*
* *vm_page_frozen is set to true if the page is newly set all-frozen in the
* VM. The caller currently only uses this for determining whether an eagerly
* scanned page was successfully set all-frozen.
*
* Returns the number of tuples deleted from the page during HOT pruning.
*/
static int
lazy_scan_prune(LVRelState *vacrel,
Buffer buf,
BlockNumber blkno,
Page page,
Buffer vmbuffer,
bool all_visible_according_to_vm,
bool *has_lpdead_items,
bool *vm_page_frozen)
{
Relation rel = vacrel->rel;
PruneFreezeResult presult;
int prune_options = 0;
Assert(BufferGetBlockNumber(buf) == blkno);
/*
* Prune all HOT-update chains and potentially freeze tuples on this page.
*
* If the relation has no indexes, we can immediately mark would-be dead
* items LP_UNUSED.
*
* The number of tuples removed from the page is returned in
* presult.ndeleted. It should not be confused with presult.lpdead_items;
* presult.lpdead_items's final value can be thought of as the number of
* tuples that were deleted from indexes.
*
* 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.
*/
prune_options = HEAP_PAGE_PRUNE_FREEZE;
if (vacrel->nindexes == 0)
prune_options |= HEAP_PAGE_PRUNE_MARK_UNUSED_NOW;
if (presult.nfrozen > 0)
{
/*
* We don't increment the new_frozen_tuple_pages instrumentation
* counter when nfrozen == 0, since it only counts pages with newly
* frozen tuples (don't confuse that with pages newly set all-frozen
* in VM).
*/
vacrel->new_frozen_tuple_pages++;
}
/*
* VACUUM will call heap_page_is_all_visible() during the second pass over
* the heap to determine all_visible and all_frozen for the page -- this
* is a specialized version of the logic from this function. Now that
* we've finished pruning and freezing, make sure that we're in total
* agreement with heap_page_is_all_visible() using an assertion.
*/
#ifdef USE_ASSERT_CHECKING
/* Note that all_frozen value does not matter when !all_visible */
if (presult.all_visible)
{
TransactionId debug_cutoff;
bool debug_all_frozen;
Assert(presult.lpdead_items == 0);
if (!heap_page_is_all_visible(vacrel, buf,
&debug_cutoff, &debug_all_frozen))
Assert(false);
/*
* Now save details of the LP_DEAD items from the page in vacrel
*/
if (presult.lpdead_items > 0)
{
vacrel->lpdead_item_pages++;
/*
* deadoffsets are collected incrementally in
* heap_page_prune_and_freeze() as each dead line pointer is recorded,
* with an indeterminate order, but dead_items_add requires them to be
* sorted.
*/
qsort(presult.deadoffsets, presult.lpdead_items, sizeof(OffsetNumber),
cmpOffsetNumbers);
/*
* 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 && presult.all_visible)
{
uint8 old_vmbits;
uint8 flags = VISIBILITYMAP_ALL_VISIBLE;
if (presult.all_frozen)
{
Assert(!TransactionIdIsValid(presult.vm_conflict_horizon));
flags |= VISIBILITYMAP_ALL_FROZEN;
}
/*
* It should never be the case that the visibility map page is set
* while the page-level bit is clear, but the reverse is allowed (if
* checksums are not enabled). Regardless, set both bits so that we
* get back in sync.
*
* NB: If the heap page is all-visible but the VM bit is not set, we
* don't need to dirty the heap page. However, if checksums are
* enabled, we do need to make sure that the heap page is dirtied
* before passing it to visibilitymap_set(), because it may be logged.
* Given that this situation should only happen in rare cases after a
* crash, it is not worth optimizing.
*/
PageSetAllVisible(page);
MarkBufferDirty(buf);
old_vmbits = visibilitymap_set(vacrel->rel, blkno, buf,
InvalidXLogRecPtr,
vmbuffer, presult.vm_conflict_horizon,
flags);
/*
* If the page wasn't already set all-visible and/or all-frozen in the
* VM, count it as newly set for logging.
*/
if ((old_vmbits & VISIBILITYMAP_ALL_VISIBLE) == 0)
{
vacrel->vm_new_visible_pages++;
if (presult.all_frozen)
{
vacrel->vm_new_visible_frozen_pages++;
*vm_page_frozen = true;
}
}
else if ((old_vmbits & VISIBILITYMAP_ALL_FROZEN) == 0 &&
presult.all_frozen)
{
vacrel->vm_new_frozen_pages++;
*vm_page_frozen = true;
}
}
/*
* As of PostgreSQL 9.2, the visibility map bit should never be set if the
* page-level bit is clear. However, it's possible that the bit got
* cleared after heap_vac_scan_next_block() was called, so we must recheck
* with buffer lock before concluding that the VM is corrupt.
*/
else if (all_visible_according_to_vm && !PageIsAllVisible(page) &&
visibilitymap_get_status(vacrel->rel, blkno, &vmbuffer) != 0)
{
elog(WARNING, "page is not marked all-visible but visibility map bit is set in relation \"%s\" page %u",
vacrel->relname, blkno);
visibilitymap_clear(vacrel->rel, blkno, vmbuffer,
VISIBILITYMAP_VALID_BITS);
}
/*
* It's possible for the value returned by
* GetOldestNonRemovableTransactionId() to move backwards, so it's not
* wrong for us to see tuples that appear to not be visible to everyone
* yet, while PD_ALL_VISIBLE is already set. The real safe xmin value
* never moves backwards, but GetOldestNonRemovableTransactionId() is
* conservative and sometimes returns a value that's unnecessarily small,
* so if we see that contradiction it just means that the tuples that we
* think are not visible to everyone yet actually are, and the
* PD_ALL_VISIBLE flag is correct.
*
* There should never be LP_DEAD items on a page with PD_ALL_VISIBLE set,
* however.
*/
else if (presult.lpdead_items > 0 && PageIsAllVisible(page))
{
elog(WARNING, "page containing LP_DEAD items is marked as all-visible in relation \"%s\" page %u",
vacrel->relname, blkno);
PageClearAllVisible(page);
MarkBufferDirty(buf);
visibilitymap_clear(vacrel->rel, blkno, vmbuffer,
VISIBILITYMAP_VALID_BITS);
}
/*
* If the all-visible page is all-frozen but not marked as such yet, mark
* 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 && presult.all_visible &&
presult.all_frozen && !VM_ALL_FROZEN(vacrel->rel, blkno, &vmbuffer))
{
uint8 old_vmbits;
/*
* Avoid relying on all_visible_according_to_vm as a proxy for the
* page-level PD_ALL_VISIBLE bit being set, since it might have become
* stale -- even when all_visible is set
*/
if (!PageIsAllVisible(page))
{
PageSetAllVisible(page);
MarkBufferDirty(buf);
}
/*
* Set the page all-frozen (and all-visible) in the VM.
*
* We can pass InvalidTransactionId as our cutoff_xid, since a
* snapshotConflictHorizon sufficient to make everything safe for REDO
* was logged when the page's tuples were frozen.
*/
Assert(!TransactionIdIsValid(presult.vm_conflict_horizon));
old_vmbits = visibilitymap_set(vacrel->rel, blkno, buf,
InvalidXLogRecPtr,
vmbuffer, InvalidTransactionId,
VISIBILITYMAP_ALL_VISIBLE |
VISIBILITYMAP_ALL_FROZEN);
/*
* The page was likely already set all-visible in the VM. However,
* there is a small chance that it was modified sometime between
* setting all_visible_according_to_vm and checking the visibility
* during pruning. Check the return value of old_vmbits anyway to
* ensure the visibility map counters used for logging are accurate.
*/
if ((old_vmbits & VISIBILITYMAP_ALL_VISIBLE) == 0)
{
vacrel->vm_new_visible_pages++;
vacrel->vm_new_visible_frozen_pages++;
*vm_page_frozen = true;
}
/*
* We already checked that the page was not set all-frozen in the VM
* above, so we don't need to test the value of old_vmbits.
*/
else
{
vacrel->vm_new_frozen_pages++;
*vm_page_frozen = true;
}
}
return presult.ndeleted;
}
/*
* lazy_scan_noprune() -- lazy_scan_prune() without pruning or freezing
*
* Caller need only hold a pin and share lock on the buffer, unlike
* lazy_scan_prune, which requires a full cleanup lock. While pruning isn't
* performed here, it's quite possible that an earlier opportunistic pruning
* operation left LP_DEAD items behind. We'll at least collect any such items
* in dead_items for removal from indexes.
*
* For aggressive VACUUM callers, we may return false to indicate that a full
* cleanup lock is required for processing by lazy_scan_prune. This is only
* necessary when the aggressive VACUUM needs to freeze some tuple XIDs from
* one or more tuples on the page. We always return true for non-aggressive
* callers.
*
* If this function returns true, *has_lpdead_items gets set to true or false
* depending on whether, upon return from this function, any LP_DEAD items are
* present on the page. If this function returns false, *has_lpdead_items
* is not updated.
*/
static bool
lazy_scan_noprune(LVRelState *vacrel,
Buffer buf,
BlockNumber blkno,
Page page,
bool *has_lpdead_items)
{
OffsetNumber offnum,
maxoff;
int lpdead_items,
live_tuples,
recently_dead_tuples,
missed_dead_tuples;
bool hastup;
HeapTupleHeader tupleheader;
TransactionId NoFreezePageRelfrozenXid = vacrel->NewRelfrozenXid;
MultiXactId NoFreezePageRelminMxid = vacrel->NewRelminMxid;
OffsetNumber deadoffsets[MaxHeapTuplesPerPage];
if (ItemIdIsRedirected(itemid))
{
hastup = true;
continue;
}
if (ItemIdIsDead(itemid))
{
/*
* Deliberately don't set hastup=true here. See same point in
* lazy_scan_prune for an explanation.
*/
deadoffsets[lpdead_items++] = offnum;
continue;
}
hastup = true; /* page prevents rel truncation */
tupleheader = (HeapTupleHeader) PageGetItem(page, itemid);
if (heap_tuple_should_freeze(tupleheader, &vacrel->cutoffs,
&NoFreezePageRelfrozenXid,
&NoFreezePageRelminMxid))
{
/* Tuple with XID < FreezeLimit (or MXID < MultiXactCutoff) */
if (vacrel->aggressive)
{
/*
* Aggressive VACUUMs must always be able to advance rel's
* relfrozenxid to a value >= FreezeLimit (and be able to
* advance rel's relminmxid to a value >= MultiXactCutoff).
* The ongoing aggressive VACUUM won't be able to do that
* unless it can freeze an XID (or MXID) from this tuple now.
*
* The only safe option is to have caller perform processing
* of this page using lazy_scan_prune. Caller might have to
* wait a while for a cleanup lock, but it can't be helped.
*/
vacrel->offnum = InvalidOffsetNumber;
return false;
}
/*
* Non-aggressive VACUUMs are under no obligation to advance
* relfrozenxid (even by one XID). We can be much laxer here.
*
* Currently we always just accept an older final relfrozenxid
* and/or relminmxid value. We never make caller wait or work a
* little harder, even when it likely makes sense to do so.
*/
}
switch (HeapTupleSatisfiesVacuum(&tuple, vacrel->cutoffs.OldestXmin,
buf))
{
case HEAPTUPLE_DELETE_IN_PROGRESS:
case HEAPTUPLE_LIVE:
/*
* Count both cases as live, just like lazy_scan_prune
*/
live_tuples++;
break;
case HEAPTUPLE_DEAD:
/*
* There is some useful work for pruning to do, that won't be
* done due to failure to get a cleanup lock.
*/
missed_dead_tuples++;
break;
case HEAPTUPLE_RECENTLY_DEAD:
/*
* Count in recently_dead_tuples, just like lazy_scan_prune
*/
recently_dead_tuples++;
break;
case HEAPTUPLE_INSERT_IN_PROGRESS:
/*
* Do not count these rows as live, just like lazy_scan_prune
*/
break;
default:
elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
break;
}
}
vacrel->offnum = InvalidOffsetNumber;
/*
* By here we know for sure that caller can put off freezing and pruning
* this particular page until the next VACUUM. Remember its details now.
* (lazy_scan_prune expects a clean slate, so we have to do this last.)
*/
vacrel->NewRelfrozenXid = NoFreezePageRelfrozenXid;
vacrel->NewRelminMxid = NoFreezePageRelminMxid;
/* Save any LP_DEAD items found on the page in dead_items */
if (vacrel->nindexes == 0)
{
/* Using one-pass strategy (since table has no indexes) */
if (lpdead_items > 0)
{
/*
* Perfunctory handling for the corner case where a single pass
* strategy VACUUM cannot get a cleanup lock, and it turns out
* that there is one or more LP_DEAD items: just count the LP_DEAD
* items as missed_dead_tuples instead. (This is a bit dishonest,
* but it beats having to maintain specialized heap vacuuming code
* forever, for vanishingly little benefit.)
*/
hastup = true;
missed_dead_tuples += lpdead_items;
}
}
else if (lpdead_items > 0)
{
/*
* Page has LP_DEAD items, and so any references/TIDs that remain in
* indexes will be deleted during index vacuuming (and then marked
* LP_UNUSED in the heap)
*/
vacrel->lpdead_item_pages++;
/* Can't truncate this page */
if (hastup)
vacrel->nonempty_pages = blkno + 1;
/* Did we find LP_DEAD items? */
*has_lpdead_items = (lpdead_items > 0);
/* Caller won't need to call lazy_scan_prune with same page */
return true;
}
/*
* Main entry point for index vacuuming and heap vacuuming.
*
* Removes items collected in dead_items from table's indexes, then marks the
* same items LP_UNUSED in the heap. See the comments above lazy_scan_heap
* for full details.
*
* Also empties dead_items, freeing up space for later TIDs.
*
* We may choose to bypass index vacuuming at this point, though only when the
* ongoing VACUUM operation will definitely only have one index scan/round of
* index vacuuming.
*/
static void
lazy_vacuum(LVRelState *vacrel)
{
bool bypass;
/* Should not end up here with no indexes */
Assert(vacrel->nindexes > 0);
Assert(vacrel->lpdead_item_pages > 0);
if (!vacrel->do_index_vacuuming)
{
Assert(!vacrel->do_index_cleanup);
dead_items_reset(vacrel);
return;
}
/*
* Consider bypassing index vacuuming (and heap vacuuming) entirely.
*
* We currently only do this in cases where the number of LP_DEAD items
* for the entire VACUUM operation is close to zero. This avoids sharp
* discontinuities in the duration and overhead of successive VACUUM
* operations that run against the same table with a fixed workload.
* Ideally, successive VACUUM operations will behave as if there are
* exactly zero LP_DEAD items in cases where there are close to zero.
*
* This is likely to be helpful with a table that is continually affected
* by UPDATEs that can mostly apply the HOT optimization, but occasionally
* have small aberrations that lead to just a few heap pages retaining
* only one or two LP_DEAD items. This is pretty common; even when the
* DBA goes out of their way to make UPDATEs use HOT, it is practically
* impossible to predict whether HOT will be applied in 100% of cases.
* It's far easier to ensure that 99%+ of all UPDATEs against a table use
* HOT through careful tuning.
*/
bypass = false;
if (vacrel->consider_bypass_optimization && vacrel->rel_pages > 0)
{
BlockNumber threshold;
/*
* This crossover point at which we'll start to do index vacuuming is
* expressed as a percentage of the total number of heap pages in the
* table that are known to have at least one LP_DEAD item. This is
* much more important than the total number of LP_DEAD items, since
* it's a proxy for the number of heap pages whose visibility map bits
* cannot be set on account of bypassing index and heap vacuuming.
*
* We apply one further precautionary test: the space currently used
* to store the TIDs (TIDs that now all point to LP_DEAD items) must
* not exceed 32MB. This limits the risk that we will bypass index
* vacuuming again and again until eventually there is a VACUUM whose
* dead_items space is not CPU cache resident.
*
* We don't take any special steps to remember the LP_DEAD items (such
* as counting them in our final update to the stats system) when the
* optimization is applied. Though the accounting used in analyze.c's
* acquire_sample_rows() will recognize the same LP_DEAD items as dead
* rows in its own stats report, that's okay. The discrepancy should
* be negligible. If this optimization is ever expanded to cover more
* cases then this may need to be reconsidered.
*/
threshold = (double) vacrel->rel_pages * BYPASS_THRESHOLD_PAGES;
bypass = (vacrel->lpdead_item_pages < threshold &&
TidStoreMemoryUsage(vacrel->dead_items) < 32 * 1024 * 1024);
}
if (bypass)
{
/*
* There are almost zero TIDs. Behave as if there were precisely
* zero: bypass index vacuuming, but do index cleanup.
*
* We expect that the ongoing VACUUM operation will finish very
* quickly, so there is no point in considering speeding up as a
* failsafe against wraparound failure. (Index cleanup is expected to
* finish very quickly in cases where there were no ambulkdelete()
* calls.)
*/
vacrel->do_index_vacuuming = false;
}
else if (lazy_vacuum_all_indexes(vacrel))
{
/*
* We successfully completed a round of index vacuuming. Do related
* heap vacuuming now.
*/
lazy_vacuum_heap_rel(vacrel);
}
else
{
/*
* Failsafe case.
*
* We attempted index vacuuming, but didn't finish a full round/full
* index scan. This happens when relfrozenxid or relminmxid is too
* far in the past.
*
* From this point on the VACUUM operation will do no further index
* vacuuming or heap vacuuming. This VACUUM operation won't end up
* back here again.
*/
Assert(VacuumFailsafeActive);
}
/*
* Forget the LP_DEAD items that we just vacuumed (or just decided to not
* vacuum)
*/
dead_items_reset(vacrel);
}
/*
* lazy_vacuum_all_indexes() -- Main entry for index vacuuming
*
* Returns true in the common case when all indexes were successfully
* vacuumed. Returns false in rare cases where we determined that the ongoing
* VACUUM operation is at risk of taking too long to finish, leading to
* wraparound failure.
*/
static bool
lazy_vacuum_all_indexes(LVRelState *vacrel)
{
bool allindexes = true;
double old_live_tuples = vacrel->rel->rd_rel->reltuples;
const int progress_start_index[] = {
PROGRESS_VACUUM_PHASE,
PROGRESS_VACUUM_INDEXES_TOTAL
};
const int progress_end_index[] = {
PROGRESS_VACUUM_INDEXES_TOTAL,
PROGRESS_VACUUM_INDEXES_PROCESSED,
PROGRESS_VACUUM_NUM_INDEX_VACUUMS
};
int64 progress_start_val[2];
int64 progress_end_val[3];
/* Precheck for XID wraparound emergencies */
if (lazy_check_wraparound_failsafe(vacrel))
{
/* Wraparound emergency -- don't even start an index scan */
return false;
}
/*
* Report that we are now vacuuming indexes and the number of indexes to
* vacuum.
*/
progress_start_val[0] = PROGRESS_VACUUM_PHASE_VACUUM_INDEX;
progress_start_val[1] = vacrel->nindexes;
pgstat_progress_update_multi_param(2, progress_start_index, progress_start_val);
/* Report the number of indexes vacuumed */
pgstat_progress_update_param(PROGRESS_VACUUM_INDEXES_PROCESSED,
idx + 1);
if (lazy_check_wraparound_failsafe(vacrel))
{
/* Wraparound emergency -- end current index scan */
allindexes = false;
break;
}
}
}
else
{
/* Outsource everything to parallel variant */
parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples,
vacrel->num_index_scans);
/*
* Do a postcheck to consider applying wraparound failsafe now. Note
* that parallel VACUUM only gets the precheck and this postcheck.
*/
if (lazy_check_wraparound_failsafe(vacrel))
allindexes = false;
}
/*
* We delete all LP_DEAD items from the first heap pass in all indexes on
* each call here (except calls where we choose to do the failsafe). This
* makes the next call to lazy_vacuum_heap_rel() safe (except in the event
* of the failsafe triggering, which prevents the next call from taking
* place).
*/
Assert(vacrel->num_index_scans > 0 ||
vacrel->dead_items_info->num_items == vacrel->lpdead_items);
Assert(allindexes || VacuumFailsafeActive);
/*
* Increase and report the number of index scans. Also, we reset
* PROGRESS_VACUUM_INDEXES_TOTAL and PROGRESS_VACUUM_INDEXES_PROCESSED.
*
* We deliberately include the case where we started a round of bulk
* deletes that we weren't able to finish due to the failsafe triggering.
*/
vacrel->num_index_scans++;
progress_end_val[0] = 0;
progress_end_val[1] = 0;
progress_end_val[2] = vacrel->num_index_scans;
pgstat_progress_update_multi_param(3, progress_end_index, progress_end_val);
return allindexes;
}
/*
* Read stream callback for vacuum's third phase (second pass over the heap).
* Gets the next block from the TID store and returns it or InvalidBlockNumber
* if there are no further blocks to vacuum.
*
* NB: Assumed to be safe to use with READ_STREAM_USE_BATCHING.
*/
static BlockNumber
vacuum_reap_lp_read_stream_next(ReadStream *stream,
void *callback_private_data,
void *per_buffer_data)
{
TidStoreIter *iter = callback_private_data;
TidStoreIterResult *iter_result;
iter_result = TidStoreIterateNext(iter);
if (iter_result == NULL)
return InvalidBlockNumber;
/*
* Save the TidStoreIterResult for later, so we can extract the offsets.
* It is safe to copy the result, according to TidStoreIterateNext().
*/
memcpy(per_buffer_data, iter_result, sizeof(*iter_result));
return iter_result->blkno;
}
/*
* lazy_vacuum_heap_rel() -- second pass over the heap for two pass strategy
*
* This routine marks LP_DEAD items in vacrel->dead_items as LP_UNUSED. Pages
* that never had lazy_scan_prune record LP_DEAD items are not visited at all.
*
* We may also be able to truncate the line pointer array of the heap pages we
* visit. If there is a contiguous group of LP_UNUSED items at the end of the
* array, it can be reclaimed as free space. These LP_UNUSED items usually
* start out as LP_DEAD items recorded by lazy_scan_prune (we set items from
* each page to LP_UNUSED, and then consider if it's possible to truncate the
* page's line pointer array).
*
* Note: the reason for doing this as a second pass is we cannot remove the
* tuples until we've removed their index entries, and we want to process
* index entry removal in batches as large as possible.
*/
static void
lazy_vacuum_heap_rel(LVRelState *vacrel)
{
ReadStream *stream;
BlockNumber vacuumed_pages = 0;
Buffer vmbuffer = InvalidBuffer;
LVSavedErrInfo saved_err_info;
TidStoreIter *iter;
/*
* Set up the read stream for vacuum's second pass through the heap.
*
* It is safe to use batchmode, as vacuum_reap_lp_read_stream_next() does
* not need to wait for IO and does not perform locking. Once we support
* parallelism it should still be fine, as presumably the holder of locks
* would never be blocked by IO while holding the lock.
*/
stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
READ_STREAM_USE_BATCHING,
vacrel->bstrategy,
vacrel->rel,
MAIN_FORKNUM,
vacuum_reap_lp_read_stream_next,
iter,
sizeof(TidStoreIterResult));
while (true)
{
BlockNumber blkno;
Buffer buf;
Page page;
TidStoreIterResult *iter_result;
Size freespace;
OffsetNumber offsets[MaxOffsetNumber];
int num_offsets;
/*
* Pin the visibility map page in case we need to mark the page
* all-visible. In most cases this will be very cheap, because we'll
* already have the correct page pinned anyway.
*/
visibilitymap_pin(vacrel->rel, blkno, &vmbuffer);
/* We need a non-cleanup exclusive lock to mark dead_items unused */
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
lazy_vacuum_heap_page(vacrel, blkno, buf, offsets,
num_offsets, vmbuffer);
/* Now that we've vacuumed the page, record its available space */
page = BufferGetPage(buf);
freespace = PageGetHeapFreeSpace(page);
vacrel->blkno = InvalidBlockNumber;
if (BufferIsValid(vmbuffer))
ReleaseBuffer(vmbuffer);
/*
* We set all LP_DEAD items from the first heap pass to LP_UNUSED during
* the second heap pass. No more, no less.
*/
Assert(vacrel->num_index_scans > 1 ||
(vacrel->dead_items_info->num_items == vacrel->lpdead_items &&
vacuumed_pages == vacrel->lpdead_item_pages));
ereport(DEBUG2,
(errmsg("table \"%s\": removed %" PRId64 " dead item identifiers in %u pages",
vacrel->relname, vacrel->dead_items_info->num_items,
vacuumed_pages)));
/* Revert to the previous phase information for error traceback */
restore_vacuum_error_info(vacrel, &saved_err_info);
}
/*
* lazy_vacuum_heap_page() -- free page's LP_DEAD items listed in the
* vacrel->dead_items store.
*
* Caller must have an exclusive buffer lock on the buffer (though a full
* cleanup lock is also acceptable). vmbuffer must be valid and already have
* a pin on blkno's visibility map page.
*/
static void
lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer,
OffsetNumber *deadoffsets, int num_offsets,
Buffer vmbuffer)
{
Page page = BufferGetPage(buffer);
OffsetNumber unused[MaxHeapTuplesPerPage];
int nunused = 0;
TransactionId visibility_cutoff_xid;
bool all_frozen;
LVSavedErrInfo saved_err_info;
/*
* End critical section, so we safely can do visibility tests (which
* possibly need to perform IO and allocate memory!). If we crash now the
* page (including the corresponding vm bit) might not be marked all
* visible, but that's fine. A later vacuum will fix that.
*/
END_CRIT_SECTION();
/*
* Now that we have removed the LP_DEAD items from the page, once again
* check if the page has become all-visible. The page is already marked
* dirty, exclusively locked, and, if needed, a full page image has been
* emitted.
*/
Assert(!PageIsAllVisible(page));
if (heap_page_is_all_visible(vacrel, buffer, &visibility_cutoff_xid,
&all_frozen))
{
uint8 flags = VISIBILITYMAP_ALL_VISIBLE;
if (all_frozen)
{
Assert(!TransactionIdIsValid(visibility_cutoff_xid));
flags |= VISIBILITYMAP_ALL_FROZEN;
}
/* Count the newly set VM page for logging */
vacrel->vm_new_visible_pages++;
if (all_frozen)
vacrel->vm_new_visible_frozen_pages++;
}
/* Revert to the previous phase information for error traceback */
restore_vacuum_error_info(vacrel, &saved_err_info);
}
/*
* Trigger the failsafe to avoid wraparound failure when vacrel table has a
* relfrozenxid and/or relminmxid that is dangerously far in the past.
* Triggering the failsafe makes the ongoing VACUUM bypass any further index
* vacuuming and heap vacuuming. Truncating the heap is also bypassed.
*
* Any remaining work (work that VACUUM cannot just bypass) is typically sped
* up when the failsafe triggers. VACUUM stops applying any cost-based delay
* that it started out with.
*
* Returns true when failsafe has been triggered.
*/
static bool
lazy_check_wraparound_failsafe(LVRelState *vacrel)
{
/* Don't warn more than once per VACUUM */
if (VacuumFailsafeActive)
return true;
if (unlikely(vacuum_xid_failsafe_check(&vacrel->cutoffs)))
{
const int progress_index[] = {
PROGRESS_VACUUM_INDEXES_TOTAL,
PROGRESS_VACUUM_INDEXES_PROCESSED
};
int64 progress_val[2] = {0, 0};
VacuumFailsafeActive = true;
/*
* Abandon use of a buffer access strategy to allow use of all of
* shared buffers. We assume the caller who allocated the memory for
* the BufferAccessStrategy will free it.
*/
vacrel->bstrategy = NULL;
/* Disable index vacuuming, index cleanup, and heap rel truncation */
vacrel->do_index_vacuuming = false;
vacrel->do_index_cleanup = false;
vacrel->do_rel_truncate = false;
/* Reset the progress counters */
pgstat_progress_update_multi_param(2, progress_index, progress_val);
ereport(WARNING,
(errmsg("bypassing nonessential maintenance of table \"%s.%s.%s\" as a failsafe after %d index scans",
vacrel->dbname, vacrel->relnamespace, vacrel->relname,
vacrel->num_index_scans),
errdetail("The table's relfrozenxid or relminmxid is too far in the past."),
errhint("Consider increasing configuration parameter \"maintenance_work_mem\" or \"autovacuum_work_mem\".\n"
"You might also need to consider other ways for VACUUM to keep up with the allocation of transaction IDs.")));
/* Stop applying cost limits from this point on */
VacuumCostActive = false;
VacuumCostBalance = 0;
/*
* Report that we are now cleaning up indexes and the number of indexes to
* cleanup.
*/
progress_start_val[0] = PROGRESS_VACUUM_PHASE_INDEX_CLEANUP;
progress_start_val[1] = vacrel->nindexes;
pgstat_progress_update_multi_param(2, progress_start_index, progress_start_val);
/* Report the number of indexes cleaned up */
pgstat_progress_update_param(PROGRESS_VACUUM_INDEXES_PROCESSED,
idx + 1);
}
}
else
{
/* Outsource everything to parallel variant */
parallel_vacuum_cleanup_all_indexes(vacrel->pvs, reltuples,
vacrel->num_index_scans,
estimated_count);
}
/* Reset the progress counters */
pgstat_progress_update_multi_param(2, progress_end_index, progress_end_val);
}
/*
* lazy_vacuum_one_index() -- vacuum index relation.
*
* Delete all the index tuples containing a TID collected in
* vacrel->dead_items. Also update running statistics. Exact
* details depend on index AM's ambulkdelete routine.
*
* reltuples is the number of heap tuples to be passed to the
* bulkdelete callback. It's always assumed to be estimated.
* See indexam.sgml for more info.
*
* Returns bulk delete stats derived from input stats
*/
static IndexBulkDeleteResult *
lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
double reltuples, LVRelState *vacrel)
{
IndexVacuumInfo ivinfo;
LVSavedErrInfo saved_err_info;
/*
* Update error traceback information.
*
* The index name is saved during this phase and restored immediately
* after this phase. See vacuum_error_callback.
*/
Assert(vacrel->indname == NULL);
vacrel->indname = pstrdup(RelationGetRelationName(indrel));
update_vacuum_error_info(vacrel, &saved_err_info,
VACUUM_ERRCB_PHASE_VACUUM_INDEX,
InvalidBlockNumber, InvalidOffsetNumber);
/* Revert to the previous phase information for error traceback */
restore_vacuum_error_info(vacrel, &saved_err_info);
pfree(vacrel->indname);
vacrel->indname = NULL;
return istat;
}
/*
* lazy_cleanup_one_index() -- do post-vacuum cleanup for index relation.
*
* Calls index AM's amvacuumcleanup routine. reltuples is the number
* of heap tuples and estimated_count is true if reltuples is an
* estimated value. See indexam.sgml for more info.
*
* Returns bulk delete stats derived from input stats
*/
static IndexBulkDeleteResult *
lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
double reltuples, bool estimated_count,
LVRelState *vacrel)
{
IndexVacuumInfo ivinfo;
LVSavedErrInfo saved_err_info;
/*
* Update error traceback information.
*
* The index name is saved during this phase and restored immediately
* after this phase. See vacuum_error_callback.
*/
Assert(vacrel->indname == NULL);
vacrel->indname = pstrdup(RelationGetRelationName(indrel));
update_vacuum_error_info(vacrel, &saved_err_info,
VACUUM_ERRCB_PHASE_INDEX_CLEANUP,
InvalidBlockNumber, InvalidOffsetNumber);
istat = vac_cleanup_one_index(&ivinfo, istat);
/* Revert to the previous phase information for error traceback */
restore_vacuum_error_info(vacrel, &saved_err_info);
pfree(vacrel->indname);
vacrel->indname = NULL;
return istat;
}
/*
* should_attempt_truncation - should we attempt to truncate the heap?
*
* Don't even think about it unless we have a shot at releasing a goodly
* number of pages. Otherwise, the time taken isn't worth it, mainly because
* an AccessExclusive lock must be replayed on any hot standby, where it can
* be particularly disruptive.
*
* Also don't attempt it if wraparound failsafe is in effect. The entire
* system might be refusing to allocate new XIDs at this point. The system
* definitely won't return to normal unless and until VACUUM actually advances
* the oldest relfrozenxid -- which hasn't happened for target rel just yet.
* If lazy_truncate_heap attempted to acquire an AccessExclusiveLock to
* truncate the table under these circumstances, an XID exhaustion error might
* make it impossible for VACUUM to fix the underlying XID exhaustion problem.
* There is very little chance of truncation working out when the failsafe is
* in effect in any case. lazy_scan_prune makes the optimistic assumption
* that any LP_DEAD items it encounters will always be LP_UNUSED by the time
* we're called.
*/
static bool
should_attempt_truncation(LVRelState *vacrel)
{
BlockNumber possibly_freeable;
if (!vacrel->do_rel_truncate || VacuumFailsafeActive)
return false;
/*
* lazy_truncate_heap - try to truncate off any empty pages at the end
*/
static void
lazy_truncate_heap(LVRelState *vacrel)
{
BlockNumber orig_rel_pages = vacrel->rel_pages;
BlockNumber new_rel_pages;
bool lock_waiter_detected;
int lock_retry;
/* Report that we are now truncating */
pgstat_progress_update_param(PROGRESS_VACUUM_PHASE,
PROGRESS_VACUUM_PHASE_TRUNCATE);
/* Update error traceback information one last time */
update_vacuum_error_info(vacrel, NULL, VACUUM_ERRCB_PHASE_TRUNCATE,
vacrel->nonempty_pages, InvalidOffsetNumber);
/*
* Loop until no more truncating can be done.
*/
do
{
/*
* We need full exclusive lock on the relation in order to do
* truncation. If we can't get it, give up rather than waiting --- we
* don't want to block other backends, and we don't want to deadlock
* (which is quite possible considering we already hold a lower-grade
* lock).
*/
lock_waiter_detected = false;
lock_retry = 0;
while (true)
{
if (ConditionalLockRelation(vacrel->rel, AccessExclusiveLock))
break;
/*
* Check for interrupts while trying to (re-)acquire the exclusive
* lock.
*/
CHECK_FOR_INTERRUPTS();
if (++lock_retry > (VACUUM_TRUNCATE_LOCK_TIMEOUT /
VACUUM_TRUNCATE_LOCK_WAIT_INTERVAL))
{
/*
* We failed to establish the lock in the specified number of
* retries. This means we give up truncating.
*/
ereport(vacrel->verbose ? INFO : DEBUG2,
(errmsg("\"%s\": stopping truncate due to conflicting lock request",
vacrel->relname)));
return;
}
/*
* Now that we have exclusive lock, look to see if the rel has grown
* whilst we were vacuuming with non-exclusive lock. If so, give up;
* the newly added pages presumably contain non-deletable tuples.
*/
new_rel_pages = RelationGetNumberOfBlocks(vacrel->rel);
if (new_rel_pages != orig_rel_pages)
{
/*
* Note: we intentionally don't update vacrel->rel_pages with the
* new rel size here. If we did, it would amount to assuming that
* the new pages are empty, which is unlikely. Leaving the numbers
* alone amounts to assuming that the new pages have the same
* tuple density as existing ones, which is less unlikely.
*/
UnlockRelation(vacrel->rel, AccessExclusiveLock);
return;
}
/*
* Scan backwards from the end to verify that the end pages actually
* contain no tuples. This is *necessary*, not optional, because
* other backends could have added tuples to these pages whilst we
* were vacuuming.
*/
new_rel_pages = count_nondeletable_pages(vacrel, &lock_waiter_detected);
vacrel->blkno = new_rel_pages;
if (new_rel_pages >= orig_rel_pages)
{
/* can't do anything after all */
UnlockRelation(vacrel->rel, AccessExclusiveLock);
return;
}
/*
* Okay to truncate.
*/
RelationTruncate(vacrel->rel, new_rel_pages);
/*
* We can release the exclusive lock as soon as we have truncated.
* Other backends can't safely access the relation until they have
* processed the smgr invalidation that smgrtruncate sent out ... but
* that should happen as part of standard invalidation processing once
* they acquire lock on the relation.
*/
UnlockRelation(vacrel->rel, AccessExclusiveLock);
/*
* Update statistics. Here, it *is* correct to adjust rel_pages
* without also touching reltuples, since the tuple count wasn't
* changed by the truncation.
*/
vacrel->removed_pages += orig_rel_pages - new_rel_pages;
vacrel->rel_pages = new_rel_pages;
ereport(vacrel->verbose ? INFO : DEBUG2,
(errmsg("table \"%s\": truncated %u to %u pages",
vacrel->relname,
orig_rel_pages, new_rel_pages)));
orig_rel_pages = new_rel_pages;
} while (new_rel_pages > vacrel->nonempty_pages && lock_waiter_detected);
}
/*
* Rescan end pages to verify that they are (still) empty of tuples.
*
* Returns number of nondeletable pages (last nonempty page + 1).
*/
static BlockNumber
count_nondeletable_pages(LVRelState *vacrel, bool *lock_waiter_detected)
{
BlockNumber blkno;
BlockNumber prefetchedUntil;
instr_time starttime;
/* Initialize the starttime if we check for conflicting lock requests */
INSTR_TIME_SET_CURRENT(starttime);
/*
* Start checking blocks at what we believe relation end to be and move
* backwards. (Strange coding of loop control is needed because blkno is
* unsigned.) To make the scan faster, we prefetch a few blocks at a time
* in forward direction, so that OS-level readahead can kick in.
*/
blkno = vacrel->rel_pages;
StaticAssertStmt((PREFETCH_SIZE & (PREFETCH_SIZE - 1)) == 0,
"prefetch size must be power of 2");
prefetchedUntil = InvalidBlockNumber;
while (blkno > vacrel->nonempty_pages)
{
Buffer buf;
Page page;
OffsetNumber offnum,
maxoff;
bool hastup;
/*
* Check if another process requests a lock on our relation. We are
* holding an AccessExclusiveLock here, so they will be waiting. We
* only do this once per VACUUM_TRUNCATE_LOCK_CHECK_INTERVAL, and we
* only check if that interval has elapsed once every 32 blocks to
* keep the number of system calls and actual shared lock table
* lookups to a minimum.
*/
if ((blkno % 32) == 0)
{
instr_time currenttime;
instr_time elapsed;
INSTR_TIME_SET_CURRENT(currenttime);
elapsed = currenttime;
INSTR_TIME_SUBTRACT(elapsed, starttime);
if ((INSTR_TIME_GET_MICROSEC(elapsed) / 1000)
>= VACUUM_TRUNCATE_LOCK_CHECK_INTERVAL)
{
if (LockHasWaitersRelation(vacrel->rel, AccessExclusiveLock))
{
ereport(vacrel->verbose ? INFO : DEBUG2,
(errmsg("table \"%s\": suspending truncate due to conflicting lock request",
vacrel->relname)));
/*
* We don't insert a vacuum delay point here, because we have an
* exclusive lock on the table which we want to hold for as short a
* time as possible. We still need to check for interrupts however.
*/
CHECK_FOR_INTERRUPTS();
blkno--;
/* If we haven't prefetched this lot yet, do so now. */
if (prefetchedUntil > blkno)
{
BlockNumber prefetchStart;
BlockNumber pblkno;
/*
* Note: any non-unused item should be taken as a reason to keep
* this page. Even an LP_DEAD item makes truncation unsafe, since
* we must not have cleaned out its index entries.
*/
if (ItemIdIsUsed(itemid))
{
hastup = true;
break; /* can stop scanning */
}
} /* scan along page */
UnlockReleaseBuffer(buf);
/* Done scanning if we found a tuple here */
if (hastup)
return blkno + 1;
}
/*
* If we fall out of the loop, all the previously-thought-to-be-empty
* pages still are; we need not bother to look at the last known-nonempty
* page.
*/
return vacrel->nonempty_pages;
}
/*
* Allocate dead_items and dead_items_info (either using palloc, or in dynamic
* shared memory). Sets both in vacrel for caller.
*
* Also handles parallel initialization as part of allocating dead_items in
* DSM when required.
*/
static void
dead_items_alloc(LVRelState *vacrel, int nworkers)
{
VacDeadItemsInfo *dead_items_info;
int vac_work_mem = AmAutoVacuumWorkerProcess() &&
autovacuum_work_mem != -1 ?
autovacuum_work_mem : maintenance_work_mem;
/*
* Initialize state for a parallel vacuum. As of now, only one worker can
* be used for an index, so we invoke parallelism only if there are at
* least two indexes on a table.
*/
if (nworkers >= 0 && vacrel->nindexes > 1 && vacrel->do_index_vacuuming)
{
/*
* Since parallel workers cannot access data in temporary tables, we
* can't perform parallel vacuum on them.
*/
if (RelationUsesLocalBuffers(vacrel->rel))
{
/*
* Give warning only if the user explicitly tries to perform a
* parallel vacuum on the temporary table.
*/
if (nworkers > 0)
ereport(WARNING,
(errmsg("disabling parallel option of vacuum on \"%s\" --- cannot vacuum temporary tables in parallel",
vacrel->relname)));
}
else
vacrel->pvs = parallel_vacuum_init(vacrel->rel, vacrel->indrels,
vacrel->nindexes, nworkers,
vac_work_mem,
vacrel->verbose ? INFO : DEBUG2,
vacrel->bstrategy);
/*
* If parallel mode started, dead_items and dead_items_info spaces are
* allocated in DSM.
*/
if (ParallelVacuumIsActive(vacrel))
{
vacrel->dead_items = parallel_vacuum_get_dead_items(vacrel->pvs,
&vacrel->dead_items_info);
return;
}
}
/*
* Serial VACUUM case. Allocate both dead_items and dead_items_info
* locally.
*/
/*
* Add the given block number and offset numbers to dead_items.
*/
static void
dead_items_add(LVRelState *vacrel, BlockNumber blkno, OffsetNumber *offsets,
int num_offsets)
{
const int prog_index[2] = {
PROGRESS_VACUUM_NUM_DEAD_ITEM_IDS,
PROGRESS_VACUUM_DEAD_TUPLE_BYTES
};
int64 prog_val[2];
/* update the progress information */
prog_val[0] = vacrel->dead_items_info->num_items;
prog_val[1] = TidStoreMemoryUsage(vacrel->dead_items);
pgstat_progress_update_multi_param(2, prog_index, prog_val);
}
/*
* Forget all collected dead items.
*/
static void
dead_items_reset(LVRelState *vacrel)
{
if (ParallelVacuumIsActive(vacrel))
{
parallel_vacuum_reset_dead_items(vacrel->pvs);
vacrel->dead_items = parallel_vacuum_get_dead_items(vacrel->pvs,
&vacrel->dead_items_info);
return;
}
/* Recreate the tidstore with the same max_bytes limitation */
TidStoreDestroy(vacrel->dead_items);
vacrel->dead_items = TidStoreCreateLocal(vacrel->dead_items_info->max_bytes, true);
/* Reset the counter */
vacrel->dead_items_info->num_items = 0;
}
/*
* Perform cleanup for resources allocated in dead_items_alloc
*/
static void
dead_items_cleanup(LVRelState *vacrel)
{
if (!ParallelVacuumIsActive(vacrel))
{
/* Don't bother with pfree here */
return;
}
/* Track newest xmin on page. */ if (TransactionIdFollows(xmin, *visibility_cutoff_xid) &&
TransactionIdIsNormal(xmin))
*visibility_cutoff_xid = xmin;
/* Check whether this tuple is already frozen or not */ if (all_visible && *all_frozen &&
heap_tuple_needs_eventual_freeze(tuple.t_data))
*all_frozen = false;
} break;
case HEAPTUPLE_DEAD: case HEAPTUPLE_RECENTLY_DEAD: case HEAPTUPLE_INSERT_IN_PROGRESS: case HEAPTUPLE_DELETE_IN_PROGRESS:
{
all_visible = false;
*all_frozen = false; break;
} default:
elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); break;
}
} /* scan along page */
/* Clear the offset information once we have processed the given page. */
vacrel->offnum = InvalidOffsetNumber;
switch (errinfo->phase)
{ case VACUUM_ERRCB_PHASE_SCAN_HEAP: if (BlockNumberIsValid(errinfo->blkno))
{ if (OffsetNumberIsValid(errinfo->offnum))
errcontext("while scanning block %u offset %u of relation \"%s.%s\"",
errinfo->blkno, errinfo->offnum, errinfo->relnamespace, errinfo->relname); else
errcontext("while scanning block %u of relation \"%s.%s\"",
errinfo->blkno, errinfo->relnamespace, errinfo->relname);
} else
errcontext("while scanning relation \"%s.%s\"",
errinfo->relnamespace, errinfo->relname); break;
case VACUUM_ERRCB_PHASE_VACUUM_HEAP: if (BlockNumberIsValid(errinfo->blkno))
{ if (OffsetNumberIsValid(errinfo->offnum))
errcontext("while vacuuming block %u offset %u of relation \"%s.%s\"",
errinfo->blkno, errinfo->offnum, errinfo->relnamespace, errinfo->relname); else
errcontext("while vacuuming block %u of relation \"%s.%s\"",
errinfo->blkno, errinfo->relnamespace, errinfo->relname);
} else
errcontext("while vacuuming relation \"%s.%s\"",
errinfo->relnamespace, errinfo->relname); break;
case VACUUM_ERRCB_PHASE_VACUUM_INDEX:
errcontext("while vacuuming index \"%s\" of relation \"%s.%s\"",
errinfo->indname, errinfo->relnamespace, errinfo->relname); break;
case VACUUM_ERRCB_PHASE_INDEX_CLEANUP:
errcontext("while cleaning up index \"%s\" of relation \"%s.%s\"",
errinfo->indname, errinfo->relnamespace, errinfo->relname); break;
case VACUUM_ERRCB_PHASE_TRUNCATE: if (BlockNumberIsValid(errinfo->blkno))
errcontext("while truncating relation \"%s.%s\" to %u blocks",
errinfo->relnamespace, errinfo->relname, errinfo->blkno); break;
case VACUUM_ERRCB_PHASE_UNKNOWN: default: return; /* do nothing; the errinfo may not be
* initialized */
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.