| /*------------------------------------------------------------------------- |
| * |
| * ginvacuum.c |
| * delete & vacuum routines for the postgres GIN |
| * |
| * |
| * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group |
| * Portions Copyright (c) 1994, Regents of the University of California |
| * |
| * IDENTIFICATION |
| * src/backend/access/gin/ginvacuum.c |
| *------------------------------------------------------------------------- |
| */ |
| |
| #include "postgres.h" |
| |
| #include "access/gin_private.h" |
| #include "access/ginxlog.h" |
| #include "access/xloginsert.h" |
| #include "commands/vacuum.h" |
| #include "miscadmin.h" |
| #include "postmaster/autovacuum.h" |
| #include "storage/indexfsm.h" |
| #include "storage/lmgr.h" |
| #include "storage/predicate.h" |
| #include "utils/memutils.h" |
| |
| struct GinVacuumState |
| { |
| Relation index; |
| IndexBulkDeleteResult *result; |
| IndexBulkDeleteCallback callback; |
| void *callback_state; |
| GinState ginstate; |
| BufferAccessStrategy strategy; |
| MemoryContext tmpCxt; |
| }; |
| |
| /* |
| * Vacuums an uncompressed posting list. The size of the must can be specified |
| * in number of items (nitems). |
| * |
| * If none of the items need to be removed, returns NULL. Otherwise returns |
| * a new palloc'd array with the remaining items. The number of remaining |
| * items is returned in *nremaining. |
| */ |
| ItemPointer |
| ginVacuumItemPointers(GinVacuumState *gvs, ItemPointerData *items, |
| int nitem, int *nremaining) |
| { |
| int i, |
| remaining = 0; |
| ItemPointer tmpitems = NULL; |
| |
| /* |
| * Iterate over TIDs array |
| */ |
| for (i = 0; i < nitem; i++) |
| { |
| if (gvs->callback(items + i, gvs->callback_state)) |
| { |
| gvs->result->tuples_removed += 1; |
| if (!tmpitems) |
| { |
| /* |
| * First TID to be deleted: allocate memory to hold the |
| * remaining items. |
| */ |
| tmpitems = palloc(sizeof(ItemPointerData) * nitem); |
| memcpy(tmpitems, items, sizeof(ItemPointerData) * i); |
| } |
| } |
| else |
| { |
| gvs->result->num_index_tuples += 1; |
| if (tmpitems) |
| tmpitems[remaining] = items[i]; |
| remaining++; |
| } |
| } |
| |
| *nremaining = remaining; |
| return tmpitems; |
| } |
| |
| /* |
| * Create a WAL record for vacuuming entry tree leaf page. |
| */ |
| static void |
| xlogVacuumPage(Relation index, Buffer buffer) |
| { |
| Page page = BufferGetPage(buffer); |
| XLogRecPtr recptr; |
| |
| /* This is only used for entry tree leaf pages. */ |
| Assert(!GinPageIsData(page)); |
| Assert(GinPageIsLeaf(page)); |
| |
| if (!RelationNeedsWAL(index)) |
| return; |
| |
| /* |
| * Always create a full image, we don't track the changes on the page at |
| * any more fine-grained level. This could obviously be improved... |
| */ |
| XLogBeginInsert(); |
| XLogRegisterBuffer(0, buffer, REGBUF_FORCE_IMAGE | REGBUF_STANDARD); |
| |
| recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_VACUUM_PAGE); |
| PageSetLSN(page, recptr); |
| } |
| |
| |
| typedef struct DataPageDeleteStack |
| { |
| struct DataPageDeleteStack *child; |
| struct DataPageDeleteStack *parent; |
| |
| BlockNumber blkno; /* current block number */ |
| Buffer leftBuffer; /* pinned and locked rightest non-deleted page |
| * on left */ |
| bool isRoot; |
| } DataPageDeleteStack; |
| |
| |
| /* |
| * Delete a posting tree page. |
| */ |
| static void |
| ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkno, |
| BlockNumber parentBlkno, OffsetNumber myoff, bool isParentRoot) |
| { |
| Buffer dBuffer; |
| Buffer lBuffer; |
| Buffer pBuffer; |
| Page page, |
| parentPage; |
| BlockNumber rightlink; |
| |
| /* |
| * This function MUST be called only if someone of parent pages hold |
| * exclusive cleanup lock. This guarantees that no insertions currently |
| * happen in this subtree. Caller also acquires Exclusive locks on |
| * deletable, parent and left pages. |
| */ |
| lBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, leftBlkno, |
| RBM_NORMAL, gvs->strategy); |
| dBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, deleteBlkno, |
| RBM_NORMAL, gvs->strategy); |
| pBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, parentBlkno, |
| RBM_NORMAL, gvs->strategy); |
| |
| page = BufferGetPage(dBuffer); |
| rightlink = GinPageGetOpaque(page)->rightlink; |
| |
| /* |
| * Any insert which would have gone on the leaf block will now go to its |
| * right sibling. |
| */ |
| PredicateLockPageCombine(gvs->index, deleteBlkno, rightlink); |
| |
| START_CRIT_SECTION(); |
| |
| /* Unlink the page by changing left sibling's rightlink */ |
| page = BufferGetPage(lBuffer); |
| GinPageGetOpaque(page)->rightlink = rightlink; |
| |
| /* Delete downlink from parent */ |
| parentPage = BufferGetPage(pBuffer); |
| #ifdef USE_ASSERT_CHECKING |
| do |
| { |
| PostingItem *tod = GinDataPageGetPostingItem(parentPage, myoff); |
| |
| Assert(PostingItemGetBlockNumber(tod) == deleteBlkno); |
| } while (0); |
| #endif |
| GinPageDeletePostingItem(parentPage, myoff); |
| |
| page = BufferGetPage(dBuffer); |
| |
| /* |
| * we shouldn't change rightlink field to save workability of running |
| * search scan |
| */ |
| |
| /* |
| * Mark page as deleted, and remember last xid which could know its |
| * address. |
| */ |
| GinPageSetDeleted(page); |
| GinPageSetDeleteXid(page, ReadNextTransactionId()); |
| |
| MarkBufferDirty(pBuffer); |
| MarkBufferDirty(lBuffer); |
| MarkBufferDirty(dBuffer); |
| |
| if (RelationNeedsWAL(gvs->index)) |
| { |
| XLogRecPtr recptr; |
| ginxlogDeletePage data; |
| |
| /* |
| * We can't pass REGBUF_STANDARD for the deleted page, because we |
| * didn't set pd_lower on pre-9.4 versions. The page might've been |
| * binary-upgraded from an older version, and hence not have pd_lower |
| * set correctly. Ditto for the left page, but removing the item from |
| * the parent updated its pd_lower, so we know that's OK at this |
| * point. |
| */ |
| XLogBeginInsert(); |
| XLogRegisterBuffer(0, dBuffer, 0); |
| XLogRegisterBuffer(1, pBuffer, REGBUF_STANDARD); |
| XLogRegisterBuffer(2, lBuffer, 0); |
| |
| data.parentOffset = myoff; |
| data.rightLink = GinPageGetOpaque(page)->rightlink; |
| data.deleteXid = GinPageGetDeleteXid(page); |
| |
| XLogRegisterData((char *) &data, sizeof(ginxlogDeletePage)); |
| |
| recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_DELETE_PAGE); |
| PageSetLSN(page, recptr); |
| PageSetLSN(parentPage, recptr); |
| PageSetLSN(BufferGetPage(lBuffer), recptr); |
| } |
| |
| ReleaseBuffer(pBuffer); |
| ReleaseBuffer(lBuffer); |
| ReleaseBuffer(dBuffer); |
| |
| END_CRIT_SECTION(); |
| |
| gvs->result->pages_newly_deleted++; |
| gvs->result->pages_deleted++; |
| } |
| |
| |
| /* |
| * Scans posting tree and deletes empty pages. Caller must lock root page for |
| * cleanup. During scan path from root to current page is kept exclusively |
| * locked. Also keep left page exclusively locked, because ginDeletePage() |
| * needs it. If we try to relock left page later, it could deadlock with |
| * ginStepRight(). |
| */ |
| static bool |
| ginScanToDelete(GinVacuumState *gvs, BlockNumber blkno, bool isRoot, |
| DataPageDeleteStack *parent, OffsetNumber myoff) |
| { |
| DataPageDeleteStack *me; |
| Buffer buffer; |
| Page page; |
| bool meDelete = false; |
| bool isempty; |
| |
| if (isRoot) |
| { |
| me = parent; |
| } |
| else |
| { |
| if (!parent->child) |
| { |
| me = (DataPageDeleteStack *) palloc0(sizeof(DataPageDeleteStack)); |
| me->parent = parent; |
| parent->child = me; |
| me->leftBuffer = InvalidBuffer; |
| } |
| else |
| me = parent->child; |
| } |
| |
| buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno, |
| RBM_NORMAL, gvs->strategy); |
| |
| if (!isRoot) |
| LockBuffer(buffer, GIN_EXCLUSIVE); |
| |
| page = BufferGetPage(buffer); |
| |
| Assert(GinPageIsData(page)); |
| |
| if (!GinPageIsLeaf(page)) |
| { |
| OffsetNumber i; |
| |
| me->blkno = blkno; |
| for (i = FirstOffsetNumber; i <= GinPageGetOpaque(page)->maxoff; i++) |
| { |
| PostingItem *pitem = GinDataPageGetPostingItem(page, i); |
| |
| if (ginScanToDelete(gvs, PostingItemGetBlockNumber(pitem), false, me, i)) |
| i--; |
| } |
| |
| if (GinPageRightMost(page) && BufferIsValid(me->child->leftBuffer)) |
| { |
| UnlockReleaseBuffer(me->child->leftBuffer); |
| me->child->leftBuffer = InvalidBuffer; |
| } |
| } |
| |
| if (GinPageIsLeaf(page)) |
| isempty = GinDataLeafPageIsEmpty(page); |
| else |
| isempty = GinPageGetOpaque(page)->maxoff < FirstOffsetNumber; |
| |
| if (isempty) |
| { |
| /* we never delete the left- or rightmost branch */ |
| if (BufferIsValid(me->leftBuffer) && !GinPageRightMost(page)) |
| { |
| Assert(!isRoot); |
| ginDeletePage(gvs, blkno, BufferGetBlockNumber(me->leftBuffer), |
| me->parent->blkno, myoff, me->parent->isRoot); |
| meDelete = true; |
| } |
| } |
| |
| if (!meDelete) |
| { |
| if (BufferIsValid(me->leftBuffer)) |
| UnlockReleaseBuffer(me->leftBuffer); |
| me->leftBuffer = buffer; |
| } |
| else |
| { |
| if (!isRoot) |
| LockBuffer(buffer, GIN_UNLOCK); |
| |
| ReleaseBuffer(buffer); |
| } |
| |
| if (isRoot) |
| ReleaseBuffer(buffer); |
| |
| return meDelete; |
| } |
| |
| |
| /* |
| * Scan through posting tree leafs, delete empty tuples. Returns true if there |
| * is at least one empty page. |
| */ |
| static bool |
| ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno) |
| { |
| Buffer buffer; |
| Page page; |
| bool hasVoidPage = false; |
| MemoryContext oldCxt; |
| |
| /* Find leftmost leaf page of posting tree and lock it in exclusive mode */ |
| while (true) |
| { |
| PostingItem *pitem; |
| |
| buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno, |
| RBM_NORMAL, gvs->strategy); |
| LockBuffer(buffer, GIN_SHARE); |
| page = BufferGetPage(buffer); |
| |
| Assert(GinPageIsData(page)); |
| |
| if (GinPageIsLeaf(page)) |
| { |
| LockBuffer(buffer, GIN_UNLOCK); |
| LockBuffer(buffer, GIN_EXCLUSIVE); |
| break; |
| } |
| |
| Assert(PageGetMaxOffsetNumber(page) >= FirstOffsetNumber); |
| |
| pitem = GinDataPageGetPostingItem(page, FirstOffsetNumber); |
| blkno = PostingItemGetBlockNumber(pitem); |
| Assert(blkno != InvalidBlockNumber); |
| |
| UnlockReleaseBuffer(buffer); |
| } |
| |
| /* Iterate all posting tree leaves using rightlinks and vacuum them */ |
| while (true) |
| { |
| oldCxt = MemoryContextSwitchTo(gvs->tmpCxt); |
| ginVacuumPostingTreeLeaf(gvs->index, buffer, gvs); |
| MemoryContextSwitchTo(oldCxt); |
| MemoryContextReset(gvs->tmpCxt); |
| |
| if (GinDataLeafPageIsEmpty(page)) |
| hasVoidPage = true; |
| |
| blkno = GinPageGetOpaque(page)->rightlink; |
| |
| UnlockReleaseBuffer(buffer); |
| |
| if (blkno == InvalidBlockNumber) |
| break; |
| |
| buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno, |
| RBM_NORMAL, gvs->strategy); |
| LockBuffer(buffer, GIN_EXCLUSIVE); |
| page = BufferGetPage(buffer); |
| } |
| |
| return hasVoidPage; |
| } |
| |
| static void |
| ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno) |
| { |
| if (ginVacuumPostingTreeLeaves(gvs, rootBlkno)) |
| { |
| /* |
| * There is at least one empty page. So we have to rescan the tree |
| * deleting empty pages. |
| */ |
| Buffer buffer; |
| DataPageDeleteStack root, |
| *ptr, |
| *tmp; |
| |
| buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, rootBlkno, |
| RBM_NORMAL, gvs->strategy); |
| |
| /* |
| * Lock posting tree root for cleanup to ensure there are no |
| * concurrent inserts. |
| */ |
| LockBufferForCleanup(buffer); |
| |
| memset(&root, 0, sizeof(DataPageDeleteStack)); |
| root.leftBuffer = InvalidBuffer; |
| root.isRoot = true; |
| |
| ginScanToDelete(gvs, rootBlkno, true, &root, InvalidOffsetNumber); |
| |
| ptr = root.child; |
| |
| while (ptr) |
| { |
| tmp = ptr->child; |
| pfree(ptr); |
| ptr = tmp; |
| } |
| |
| UnlockReleaseBuffer(buffer); |
| } |
| } |
| |
| /* |
| * returns modified page or NULL if page isn't modified. |
| * Function works with original page until first change is occurred, |
| * then page is copied into temporary one. |
| */ |
| static Page |
| ginVacuumEntryPage(GinVacuumState *gvs, Buffer buffer, BlockNumber *roots, uint32 *nroot) |
| { |
| Page origpage = BufferGetPage(buffer), |
| tmppage; |
| OffsetNumber i, |
| maxoff = PageGetMaxOffsetNumber(origpage); |
| |
| tmppage = origpage; |
| |
| *nroot = 0; |
| |
| for (i = FirstOffsetNumber; i <= maxoff; i++) |
| { |
| IndexTuple itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i)); |
| |
| if (GinIsPostingTree(itup)) |
| { |
| /* |
| * store posting tree's roots for further processing, we can't |
| * vacuum it just now due to risk of deadlocks with scans/inserts |
| */ |
| roots[*nroot] = GinGetDownlink(itup); |
| (*nroot)++; |
| } |
| else if (GinGetNPosting(itup) > 0) |
| { |
| int nitems; |
| ItemPointer items_orig; |
| bool free_items_orig; |
| ItemPointer items; |
| |
| /* Get list of item pointers from the tuple. */ |
| if (GinItupIsCompressed(itup)) |
| { |
| items_orig = ginPostingListDecode((GinPostingList *) GinGetPosting(itup), &nitems); |
| free_items_orig = true; |
| } |
| else |
| { |
| items_orig = (ItemPointer) GinGetPosting(itup); |
| nitems = GinGetNPosting(itup); |
| free_items_orig = false; |
| } |
| |
| /* Remove any items from the list that need to be vacuumed. */ |
| items = ginVacuumItemPointers(gvs, items_orig, nitems, &nitems); |
| |
| if (free_items_orig) |
| pfree(items_orig); |
| |
| /* If any item pointers were removed, recreate the tuple. */ |
| if (items) |
| { |
| OffsetNumber attnum; |
| Datum key; |
| GinNullCategory category; |
| GinPostingList *plist; |
| int plistsize; |
| |
| if (nitems > 0) |
| { |
| plist = ginCompressPostingList(items, nitems, GinMaxItemSize, NULL); |
| plistsize = SizeOfGinPostingList(plist); |
| } |
| else |
| { |
| plist = NULL; |
| plistsize = 0; |
| } |
| |
| /* |
| * if we already created a temporary page, make changes in |
| * place |
| */ |
| if (tmppage == origpage) |
| { |
| /* |
| * On first difference, create a temporary copy of the |
| * page and copy the tuple's posting list to it. |
| */ |
| tmppage = PageGetTempPageCopy(origpage); |
| |
| /* set itup pointer to new page */ |
| itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i)); |
| } |
| |
| attnum = gintuple_get_attrnum(&gvs->ginstate, itup); |
| key = gintuple_get_key(&gvs->ginstate, itup, &category); |
| itup = GinFormTuple(&gvs->ginstate, attnum, key, category, |
| (char *) plist, plistsize, |
| nitems, true); |
| if (plist) |
| pfree(plist); |
| PageIndexTupleDelete(tmppage, i); |
| |
| if (PageAddItem(tmppage, (Item) itup, IndexTupleSize(itup), i, false, false) != i) |
| elog(ERROR, "failed to add item to index page in \"%s\"", |
| RelationGetRelationName(gvs->index)); |
| |
| pfree(itup); |
| pfree(items); |
| } |
| } |
| } |
| |
| return (tmppage == origpage) ? NULL : tmppage; |
| } |
| |
| IndexBulkDeleteResult * |
| ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, |
| IndexBulkDeleteCallback callback, void *callback_state) |
| { |
| Relation index = info->index; |
| BlockNumber blkno = GIN_ROOT_BLKNO; |
| GinVacuumState gvs; |
| Buffer buffer; |
| BlockNumber rootOfPostingTree[BLCKSZ / (sizeof(IndexTupleData) + sizeof(ItemId))]; |
| uint32 nRoot; |
| |
| gvs.tmpCxt = AllocSetContextCreate(CurrentMemoryContext, |
| "Gin vacuum temporary context", |
| ALLOCSET_DEFAULT_SIZES); |
| gvs.index = index; |
| gvs.callback = callback; |
| gvs.callback_state = callback_state; |
| gvs.strategy = info->strategy; |
| initGinState(&gvs.ginstate, index); |
| |
| /* first time through? */ |
| if (stats == NULL) |
| { |
| /* Yes, so initialize stats to zeroes */ |
| stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); |
| |
| /* |
| * and cleanup any pending inserts |
| */ |
| ginInsertCleanup(&gvs.ginstate, !IsAutoVacuumWorkerProcess(), |
| false, true, stats); |
| } |
| |
| /* we'll re-count the tuples each time */ |
| stats->num_index_tuples = 0; |
| gvs.result = stats; |
| |
| buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, |
| RBM_NORMAL, info->strategy); |
| |
| /* find leaf page */ |
| for (;;) |
| { |
| Page page = BufferGetPage(buffer); |
| IndexTuple itup; |
| |
| LockBuffer(buffer, GIN_SHARE); |
| |
| Assert(!GinPageIsData(page)); |
| |
| if (GinPageIsLeaf(page)) |
| { |
| LockBuffer(buffer, GIN_UNLOCK); |
| LockBuffer(buffer, GIN_EXCLUSIVE); |
| |
| if (blkno == GIN_ROOT_BLKNO && !GinPageIsLeaf(page)) |
| { |
| LockBuffer(buffer, GIN_UNLOCK); |
| continue; /* check it one more */ |
| } |
| break; |
| } |
| |
| Assert(PageGetMaxOffsetNumber(page) >= FirstOffsetNumber); |
| |
| itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, FirstOffsetNumber)); |
| blkno = GinGetDownlink(itup); |
| Assert(blkno != InvalidBlockNumber); |
| |
| UnlockReleaseBuffer(buffer); |
| buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, |
| RBM_NORMAL, info->strategy); |
| } |
| |
| /* right now we found leftmost page in entry's BTree */ |
| |
| for (;;) |
| { |
| Page page = BufferGetPage(buffer); |
| Page resPage; |
| uint32 i; |
| |
| Assert(!GinPageIsData(page)); |
| |
| resPage = ginVacuumEntryPage(&gvs, buffer, rootOfPostingTree, &nRoot); |
| |
| blkno = GinPageGetOpaque(page)->rightlink; |
| |
| if (resPage) |
| { |
| START_CRIT_SECTION(); |
| PageRestoreTempPage(resPage, page); |
| MarkBufferDirty(buffer); |
| xlogVacuumPage(gvs.index, buffer); |
| UnlockReleaseBuffer(buffer); |
| END_CRIT_SECTION(); |
| } |
| else |
| { |
| UnlockReleaseBuffer(buffer); |
| } |
| |
| vacuum_delay_point(); |
| |
| for (i = 0; i < nRoot; i++) |
| { |
| ginVacuumPostingTree(&gvs, rootOfPostingTree[i]); |
| vacuum_delay_point(); |
| } |
| |
| if (blkno == InvalidBlockNumber) /* rightmost page */ |
| break; |
| |
| buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, |
| RBM_NORMAL, info->strategy); |
| LockBuffer(buffer, GIN_EXCLUSIVE); |
| } |
| |
| MemoryContextDelete(gvs.tmpCxt); |
| |
| return gvs.result; |
| } |
| |
| IndexBulkDeleteResult * |
| ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) |
| { |
| Relation index = info->index; |
| bool needLock; |
| BlockNumber npages, |
| blkno; |
| BlockNumber totFreePages; |
| GinState ginstate; |
| GinStatsData idxStat; |
| |
| /* |
| * In an autovacuum analyze, we want to clean up pending insertions. |
| * Otherwise, an ANALYZE-only call is a no-op. |
| */ |
| if (info->analyze_only) |
| { |
| if (IsAutoVacuumWorkerProcess()) |
| { |
| initGinState(&ginstate, index); |
| ginInsertCleanup(&ginstate, false, true, true, stats); |
| } |
| return stats; |
| } |
| |
| /* |
| * Set up all-zero stats and cleanup pending inserts if ginbulkdelete |
| * wasn't called |
| */ |
| if (stats == NULL) |
| { |
| stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); |
| initGinState(&ginstate, index); |
| ginInsertCleanup(&ginstate, !IsAutoVacuumWorkerProcess(), |
| false, true, stats); |
| } |
| |
| memset(&idxStat, 0, sizeof(idxStat)); |
| |
| /* |
| * XXX we always report the heap tuple count as the number of index |
| * entries. This is bogus if the index is partial, but it's real hard to |
| * tell how many distinct heap entries are referenced by a GIN index. |
| */ |
| stats->num_index_tuples = Max(info->num_heap_tuples, 0); |
| stats->estimated_count = info->estimated_count; |
| |
| /* |
| * Need lock unless it's local to this backend. |
| */ |
| needLock = !RELATION_IS_LOCAL(index); |
| |
| if (needLock) |
| LockRelationForExtension(index, ExclusiveLock); |
| npages = RelationGetNumberOfBlocks(index); |
| if (needLock) |
| UnlockRelationForExtension(index, ExclusiveLock); |
| |
| totFreePages = 0; |
| |
| for (blkno = GIN_ROOT_BLKNO; blkno < npages; blkno++) |
| { |
| Buffer buffer; |
| Page page; |
| |
| vacuum_delay_point(); |
| |
| buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, |
| RBM_NORMAL, info->strategy); |
| LockBuffer(buffer, GIN_SHARE); |
| page = (Page) BufferGetPage(buffer); |
| |
| if (GinPageIsRecyclable(page)) |
| { |
| Assert(blkno != GIN_ROOT_BLKNO); |
| RecordFreeIndexPage(index, blkno); |
| totFreePages++; |
| } |
| else if (GinPageIsData(page)) |
| { |
| idxStat.nDataPages++; |
| } |
| else if (!GinPageIsList(page)) |
| { |
| idxStat.nEntryPages++; |
| |
| if (GinPageIsLeaf(page)) |
| idxStat.nEntries += PageGetMaxOffsetNumber(page); |
| } |
| |
| UnlockReleaseBuffer(buffer); |
| } |
| |
| /* Update the metapage with accurate page and entry counts */ |
| idxStat.nTotalPages = npages; |
| ginUpdateStats(info->index, &idxStat, false); |
| |
| /* Finally, vacuum the FSM */ |
| IndexFreeSpaceMapVacuum(info->index); |
| |
| stats->pages_free = totFreePages; |
| |
| if (needLock) |
| LockRelationForExtension(index, ExclusiveLock); |
| stats->num_pages = RelationGetNumberOfBlocks(index); |
| if (needLock) |
| UnlockRelationForExtension(index, ExclusiveLock); |
| |
| return stats; |
| } |
| |
| /* |
| * Return whether Page can safely be recycled. |
| */ |
| bool |
| GinPageIsRecyclable(Page page) |
| { |
| TransactionId delete_xid; |
| |
| if (PageIsNew(page)) |
| return true; |
| |
| if (!GinPageIsDeleted(page)) |
| return false; |
| |
| delete_xid = GinPageGetDeleteXid(page); |
| |
| if (!TransactionIdIsValid(delete_xid)) |
| return true; |
| |
| /* |
| * If no backend still could view delete_xid as in running, all scans |
| * concurrent with ginDeletePage() must have finished. |
| */ |
| return GlobalVisCheckRemovableXid(NULL, delete_xid); |
| } |