diff options
Diffstat (limited to 'src/backend/commands/vacuumlazy.c')
-rw-r--r-- | src/backend/commands/vacuumlazy.c | 230 |
1 files changed, 166 insertions, 64 deletions
diff --git a/src/backend/commands/vacuumlazy.c b/src/backend/commands/vacuumlazy.c index 503684936b3..fc18b27427b 100644 --- a/src/backend/commands/vacuumlazy.c +++ b/src/backend/commands/vacuumlazy.c @@ -48,6 +48,7 @@ #include "commands/vacuum.h" #include "miscadmin.h" #include "pgstat.h" +#include "portability/instr_time.h" #include "postmaster/autovacuum.h" #include "storage/bufmgr.h" #include "storage/freespace.h" @@ -70,6 +71,17 @@ #define REL_TRUNCATE_FRACTION 16 /* + * Timing parameters for truncate locking heuristics. + * + * These were not exposed as user tunable GUC values because it didn't seem + * that the potential for improvement was great enough to merit the cost of + * supporting them. + */ +#define AUTOVACUUM_TRUNCATE_LOCK_CHECK_INTERVAL 20 /* ms */ +#define AUTOVACUUM_TRUNCATE_LOCK_WAIT_INTERVAL 50 /* ms */ +#define AUTOVACUUM_TRUNCATE_LOCK_TIMEOUT 5000 /* ms */ + +/* * Guesstimation of number of dead tuples per page. This is used to * provide an upper limit to memory allocated when vacuuming small * tables. @@ -103,6 +115,7 @@ typedef struct LVRelStats ItemPointer dead_tuples; /* array of ItemPointerData */ int num_index_scans; TransactionId latestRemovedXid; + bool lock_waiter_detected; } LVRelStats; @@ -193,6 +206,8 @@ lazy_vacuum_rel(Relation onerel, VacuumStmt *vacstmt, vacrelstats->old_rel_pages = onerel->rd_rel->relpages; vacrelstats->old_rel_tuples = onerel->rd_rel->reltuples; vacrelstats->num_index_scans = 0; + vacrelstats->pages_removed = 0; + vacrelstats->lock_waiter_detected = false; /* Open all indexes of the relation */ vac_open_indexes(onerel, RowExclusiveLock, &nindexes, &Irel); @@ -259,10 +274,17 @@ lazy_vacuum_rel(Relation onerel, VacuumStmt *vacstmt, vacrelstats->hasindex, new_frozen_xid); - /* report results to the stats collector, too */ - pgstat_report_vacuum(RelationGetRelid(onerel), - onerel->rd_rel->relisshared, - new_rel_tuples); + /* + * Report results to the stats collector, too. An early terminated + * lazy_truncate_heap attempt suppresses the message and also cancels the + * execution of ANALYZE, if that was ordered. + */ + if (!vacrelstats->lock_waiter_detected) + pgstat_report_vacuum(RelationGetRelid(onerel), + onerel->rd_rel->relisshared, + new_rel_tuples); + else + vacstmt->options &= ~VACOPT_ANALYZE; /* and log the action if appropriate */ if (IsAutoVacuumWorkerProcess() && Log_autovacuum_min_duration >= 0) @@ -1257,80 +1279,124 @@ lazy_truncate_heap(Relation onerel, LVRelStats *vacrelstats) BlockNumber old_rel_pages = vacrelstats->rel_pages; BlockNumber new_rel_pages; PGRUsage ru0; + int lock_retry; pg_rusage_init(&ru0); /* - * 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). - */ - if (!ConditionalLockRelation(onerel, AccessExclusiveLock)) - 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. + * Loop until no more truncating can be done. */ - new_rel_pages = RelationGetNumberOfBlocks(onerel); - if (new_rel_pages != old_rel_pages) + do { /* - * Note: we intentionally don't update vacrelstats->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. + * 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). */ - UnlockRelation(onerel, AccessExclusiveLock); - return; - } + vacrelstats->lock_waiter_detected = false; + lock_retry = 0; + while (true) + { + if (ConditionalLockRelation(onerel, AccessExclusiveLock)) + break; - /* - * 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(onerel, vacrelstats); + /* + * Check for interrupts while trying to (re-)acquire the exclusive + * lock. + */ + CHECK_FOR_INTERRUPTS(); - if (new_rel_pages >= old_rel_pages) - { - /* can't do anything after all */ - UnlockRelation(onerel, AccessExclusiveLock); - return; - } + if (++lock_retry > (AUTOVACUUM_TRUNCATE_LOCK_TIMEOUT / + AUTOVACUUM_TRUNCATE_LOCK_WAIT_INTERVAL)) + { + /* + * We failed to establish the lock in the specified number of + * retries. This means we give up truncating. Suppress the + * ANALYZE step. Doing an ANALYZE at this point will reset the + * dead_tuple_count in the stats collector, so we will not get + * called by the autovacuum launcher again to do the truncate. + */ + vacrelstats->lock_waiter_detected = true; + ereport(LOG, + (errmsg("automatic vacuum of table \"%s.%s.%s\": " + "cannot (re)acquire exclusive " + "lock for truncate scan", + get_database_name(MyDatabaseId), + get_namespace_name(RelationGetNamespace(onerel)), + RelationGetRelationName(onerel)))); + return; + } - /* - * Okay to truncate. - */ - RelationTruncate(onerel, new_rel_pages); + pg_usleep(AUTOVACUUM_TRUNCATE_LOCK_WAIT_INTERVAL); + } - /* - * 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(onerel, AccessExclusiveLock); + /* + * 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(onerel); + if (new_rel_pages != old_rel_pages) + { + /* + * Note: we intentionally don't update vacrelstats->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(onerel, AccessExclusiveLock); + return; + } - /* - * Update statistics. Here, it *is* correct to adjust rel_pages without - * also touching reltuples, since the tuple count wasn't changed by the - * truncation. - */ - vacrelstats->rel_pages = new_rel_pages; - vacrelstats->pages_removed = old_rel_pages - new_rel_pages; + /* + * 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(onerel, vacrelstats); - ereport(elevel, - (errmsg("\"%s\": truncated %u to %u pages", - RelationGetRelationName(onerel), - old_rel_pages, new_rel_pages), - errdetail("%s.", - pg_rusage_show(&ru0)))); + if (new_rel_pages >= old_rel_pages) + { + /* can't do anything after all */ + UnlockRelation(onerel, AccessExclusiveLock); + return; + } + + /* + * Okay to truncate. + */ + RelationTruncate(onerel, 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(onerel, 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. + */ + vacrelstats->pages_removed += old_rel_pages - new_rel_pages; + vacrelstats->rel_pages = new_rel_pages; + + ereport(elevel, + (errmsg("\"%s\": truncated %u to %u pages", + RelationGetRelationName(onerel), + old_rel_pages, new_rel_pages), + errdetail("%s.", + pg_rusage_show(&ru0)))); + old_rel_pages = new_rel_pages; + } while (new_rel_pages > vacrelstats->nonempty_pages && + vacrelstats->lock_waiter_detected); } /* @@ -1342,6 +1408,12 @@ static BlockNumber count_nondeletable_pages(Relation onerel, LVRelStats *vacrelstats) { BlockNumber blkno; + instr_time starttime; + instr_time currenttime; + instr_time elapsed; + + /* Initialize the starttime if we check for conflicting lock requests */ + INSTR_TIME_SET_CURRENT(starttime); /* Strange coding of loop control is needed because blkno is unsigned */ blkno = vacrelstats->rel_pages; @@ -1354,6 +1426,36 @@ count_nondeletable_pages(Relation onerel, LVRelStats *vacrelstats) 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 in autovacuum_truncate_lock_check millisecond + * intervals, 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_SET_CURRENT(currenttime); + elapsed = currenttime; + INSTR_TIME_SUBTRACT(elapsed, starttime); + if ((INSTR_TIME_GET_MICROSEC(elapsed) / 1000) + >= AUTOVACUUM_TRUNCATE_LOCK_CHECK_INTERVAL) + { + if (LockHasWaitersRelation(onerel, AccessExclusiveLock)) + { + ereport(elevel, + (errmsg("\"%s\": suspending truncate " + "due to conflicting lock request", + RelationGetRelationName(onerel)))); + + vacrelstats->lock_waiter_detected = true; + return blkno; + } + starttime = currenttime; + } + } + + /* * 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. |