1 /*-------------------------------------------------------------------------
2 *
3 * ginvacuum.c
4 * delete & vacuum routines for the postgres GIN
5 *
6 *
7 * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
8 * Portions Copyright (c) 1994, Regents of the University of California
9 *
10 * IDENTIFICATION
11 * src/backend/access/gin/ginvacuum.c
12 *-------------------------------------------------------------------------
13 */
14
15 #include "postgres.h"
16
17 #include "access/gin_private.h"
18 #include "access/ginxlog.h"
19 #include "access/xloginsert.h"
20 #include "commands/vacuum.h"
21 #include "miscadmin.h"
22 #include "postmaster/autovacuum.h"
23 #include "storage/indexfsm.h"
24 #include "storage/lmgr.h"
25 #include "storage/predicate.h"
26 #include "utils/memutils.h"
27
28 struct GinVacuumState
29 {
30 Relation index;
31 IndexBulkDeleteResult *result;
32 IndexBulkDeleteCallback callback;
33 void *callback_state;
34 GinState ginstate;
35 BufferAccessStrategy strategy;
36 MemoryContext tmpCxt;
37 };
38
39 /*
40 * Vacuums an uncompressed posting list. The size of the must can be specified
41 * in number of items (nitems).
42 *
43 * If none of the items need to be removed, returns NULL. Otherwise returns
44 * a new palloc'd array with the remaining items. The number of remaining
45 * items is returned in *nremaining.
46 */
47 ItemPointer
ginVacuumItemPointers(GinVacuumState * gvs,ItemPointerData * items,int nitem,int * nremaining)48 ginVacuumItemPointers(GinVacuumState *gvs, ItemPointerData *items,
49 int nitem, int *nremaining)
50 {
51 int i,
52 remaining = 0;
53 ItemPointer tmpitems = NULL;
54
55 /*
56 * Iterate over TIDs array
57 */
58 for (i = 0; i < nitem; i++)
59 {
60 if (gvs->callback(items + i, gvs->callback_state))
61 {
62 gvs->result->tuples_removed += 1;
63 if (!tmpitems)
64 {
65 /*
66 * First TID to be deleted: allocate memory to hold the
67 * remaining items.
68 */
69 tmpitems = palloc(sizeof(ItemPointerData) * nitem);
70 memcpy(tmpitems, items, sizeof(ItemPointerData) * i);
71 }
72 }
73 else
74 {
75 gvs->result->num_index_tuples += 1;
76 if (tmpitems)
77 tmpitems[remaining] = items[i];
78 remaining++;
79 }
80 }
81
82 *nremaining = remaining;
83 return tmpitems;
84 }
85
86 /*
87 * Create a WAL record for vacuuming entry tree leaf page.
88 */
89 static void
xlogVacuumPage(Relation index,Buffer buffer)90 xlogVacuumPage(Relation index, Buffer buffer)
91 {
92 Page page = BufferGetPage(buffer);
93 XLogRecPtr recptr;
94
95 /* This is only used for entry tree leaf pages. */
96 Assert(!GinPageIsData(page));
97 Assert(GinPageIsLeaf(page));
98
99 if (!RelationNeedsWAL(index))
100 return;
101
102 /*
103 * Always create a full image, we don't track the changes on the page at
104 * any more fine-grained level. This could obviously be improved...
105 */
106 XLogBeginInsert();
107 XLogRegisterBuffer(0, buffer, REGBUF_FORCE_IMAGE | REGBUF_STANDARD);
108
109 recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_VACUUM_PAGE);
110 PageSetLSN(page, recptr);
111 }
112
113
114 typedef struct DataPageDeleteStack
115 {
116 struct DataPageDeleteStack *child;
117 struct DataPageDeleteStack *parent;
118
119 BlockNumber blkno; /* current block number */
120 Buffer leftBuffer; /* pinned and locked rightest non-deleted page
121 * on left */
122 bool isRoot;
123 } DataPageDeleteStack;
124
125
126 /*
127 * Delete a posting tree page.
128 */
129 static void
ginDeletePage(GinVacuumState * gvs,BlockNumber deleteBlkno,BlockNumber leftBlkno,BlockNumber parentBlkno,OffsetNumber myoff,bool isParentRoot)130 ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkno,
131 BlockNumber parentBlkno, OffsetNumber myoff, bool isParentRoot)
132 {
133 Buffer dBuffer;
134 Buffer lBuffer;
135 Buffer pBuffer;
136 Page page,
137 parentPage;
138 BlockNumber rightlink;
139
140 /*
141 * This function MUST be called only if someone of parent pages hold
142 * exclusive cleanup lock. This guarantees that no insertions currently
143 * happen in this subtree. Caller also acquires Exclusive locks on
144 * deletable, parent and left pages.
145 */
146 lBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, leftBlkno,
147 RBM_NORMAL, gvs->strategy);
148 dBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, deleteBlkno,
149 RBM_NORMAL, gvs->strategy);
150 pBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, parentBlkno,
151 RBM_NORMAL, gvs->strategy);
152
153 page = BufferGetPage(dBuffer);
154 rightlink = GinPageGetOpaque(page)->rightlink;
155
156 /*
157 * Any insert which would have gone on the leaf block will now go to its
158 * right sibling.
159 */
160 PredicateLockPageCombine(gvs->index, deleteBlkno, rightlink);
161
162 START_CRIT_SECTION();
163
164 /* Unlink the page by changing left sibling's rightlink */
165 page = BufferGetPage(lBuffer);
166 GinPageGetOpaque(page)->rightlink = rightlink;
167
168 /* Delete downlink from parent */
169 parentPage = BufferGetPage(pBuffer);
170 #ifdef USE_ASSERT_CHECKING
171 do
172 {
173 PostingItem *tod = GinDataPageGetPostingItem(parentPage, myoff);
174
175 Assert(PostingItemGetBlockNumber(tod) == deleteBlkno);
176 } while (0);
177 #endif
178 GinPageDeletePostingItem(parentPage, myoff);
179
180 page = BufferGetPage(dBuffer);
181
182 /*
183 * we shouldn't change rightlink field to save workability of running
184 * search scan
185 */
186
187 /*
188 * Mark page as deleted, and remember last xid which could know its
189 * address.
190 */
191 GinPageSetDeleted(page);
192 GinPageSetDeleteXid(page, ReadNextTransactionId());
193
194 MarkBufferDirty(pBuffer);
195 MarkBufferDirty(lBuffer);
196 MarkBufferDirty(dBuffer);
197
198 if (RelationNeedsWAL(gvs->index))
199 {
200 XLogRecPtr recptr;
201 ginxlogDeletePage data;
202
203 /*
204 * We can't pass REGBUF_STANDARD for the deleted page, because we
205 * didn't set pd_lower on pre-9.4 versions. The page might've been
206 * binary-upgraded from an older version, and hence not have pd_lower
207 * set correctly. Ditto for the left page, but removing the item from
208 * the parent updated its pd_lower, so we know that's OK at this
209 * point.
210 */
211 XLogBeginInsert();
212 XLogRegisterBuffer(0, dBuffer, 0);
213 XLogRegisterBuffer(1, pBuffer, REGBUF_STANDARD);
214 XLogRegisterBuffer(2, lBuffer, 0);
215
216 data.parentOffset = myoff;
217 data.rightLink = GinPageGetOpaque(page)->rightlink;
218 data.deleteXid = GinPageGetDeleteXid(page);
219
220 XLogRegisterData((char *) &data, sizeof(ginxlogDeletePage));
221
222 recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_DELETE_PAGE);
223 PageSetLSN(page, recptr);
224 PageSetLSN(parentPage, recptr);
225 PageSetLSN(BufferGetPage(lBuffer), recptr);
226 }
227
228 ReleaseBuffer(pBuffer);
229 ReleaseBuffer(lBuffer);
230 ReleaseBuffer(dBuffer);
231
232 END_CRIT_SECTION();
233
234 gvs->result->pages_newly_deleted++;
235 gvs->result->pages_deleted++;
236 }
237
238
239 /*
240 * Scans posting tree and deletes empty pages. Caller must lock root page for
241 * cleanup. During scan path from root to current page is kept exclusively
242 * locked. Also keep left page exclusively locked, because ginDeletePage()
243 * needs it. If we try to relock left page later, it could deadlock with
244 * ginStepRight().
245 */
246 static bool
ginScanToDelete(GinVacuumState * gvs,BlockNumber blkno,bool isRoot,DataPageDeleteStack * parent,OffsetNumber myoff)247 ginScanToDelete(GinVacuumState *gvs, BlockNumber blkno, bool isRoot,
248 DataPageDeleteStack *parent, OffsetNumber myoff)
249 {
250 DataPageDeleteStack *me;
251 Buffer buffer;
252 Page page;
253 bool meDelete = false;
254 bool isempty;
255
256 if (isRoot)
257 {
258 me = parent;
259 }
260 else
261 {
262 if (!parent->child)
263 {
264 me = (DataPageDeleteStack *) palloc0(sizeof(DataPageDeleteStack));
265 me->parent = parent;
266 parent->child = me;
267 me->leftBuffer = InvalidBuffer;
268 }
269 else
270 me = parent->child;
271 }
272
273 buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
274 RBM_NORMAL, gvs->strategy);
275
276 if (!isRoot)
277 LockBuffer(buffer, GIN_EXCLUSIVE);
278
279 page = BufferGetPage(buffer);
280
281 Assert(GinPageIsData(page));
282
283 if (!GinPageIsLeaf(page))
284 {
285 OffsetNumber i;
286
287 me->blkno = blkno;
288 for (i = FirstOffsetNumber; i <= GinPageGetOpaque(page)->maxoff; i++)
289 {
290 PostingItem *pitem = GinDataPageGetPostingItem(page, i);
291
292 if (ginScanToDelete(gvs, PostingItemGetBlockNumber(pitem), false, me, i))
293 i--;
294 }
295
296 if (GinPageRightMost(page) && BufferIsValid(me->child->leftBuffer))
297 {
298 UnlockReleaseBuffer(me->child->leftBuffer);
299 me->child->leftBuffer = InvalidBuffer;
300 }
301 }
302
303 if (GinPageIsLeaf(page))
304 isempty = GinDataLeafPageIsEmpty(page);
305 else
306 isempty = GinPageGetOpaque(page)->maxoff < FirstOffsetNumber;
307
308 if (isempty)
309 {
310 /* we never delete the left- or rightmost branch */
311 if (BufferIsValid(me->leftBuffer) && !GinPageRightMost(page))
312 {
313 Assert(!isRoot);
314 ginDeletePage(gvs, blkno, BufferGetBlockNumber(me->leftBuffer),
315 me->parent->blkno, myoff, me->parent->isRoot);
316 meDelete = true;
317 }
318 }
319
320 if (!meDelete)
321 {
322 if (BufferIsValid(me->leftBuffer))
323 UnlockReleaseBuffer(me->leftBuffer);
324 me->leftBuffer = buffer;
325 }
326 else
327 {
328 if (!isRoot)
329 LockBuffer(buffer, GIN_UNLOCK);
330
331 ReleaseBuffer(buffer);
332 }
333
334 if (isRoot)
335 ReleaseBuffer(buffer);
336
337 return meDelete;
338 }
339
340
341 /*
342 * Scan through posting tree leafs, delete empty tuples. Returns true if there
343 * is at least one empty page.
344 */
345 static bool
ginVacuumPostingTreeLeaves(GinVacuumState * gvs,BlockNumber blkno)346 ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
347 {
348 Buffer buffer;
349 Page page;
350 bool hasVoidPage = false;
351 MemoryContext oldCxt;
352
353 /* Find leftmost leaf page of posting tree and lock it in exclusive mode */
354 while (true)
355 {
356 PostingItem *pitem;
357
358 buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
359 RBM_NORMAL, gvs->strategy);
360 LockBuffer(buffer, GIN_SHARE);
361 page = BufferGetPage(buffer);
362
363 Assert(GinPageIsData(page));
364
365 if (GinPageIsLeaf(page))
366 {
367 LockBuffer(buffer, GIN_UNLOCK);
368 LockBuffer(buffer, GIN_EXCLUSIVE);
369 break;
370 }
371
372 Assert(PageGetMaxOffsetNumber(page) >= FirstOffsetNumber);
373
374 pitem = GinDataPageGetPostingItem(page, FirstOffsetNumber);
375 blkno = PostingItemGetBlockNumber(pitem);
376 Assert(blkno != InvalidBlockNumber);
377
378 UnlockReleaseBuffer(buffer);
379 }
380
381 /* Iterate all posting tree leaves using rightlinks and vacuum them */
382 while (true)
383 {
384 oldCxt = MemoryContextSwitchTo(gvs->tmpCxt);
385 ginVacuumPostingTreeLeaf(gvs->index, buffer, gvs);
386 MemoryContextSwitchTo(oldCxt);
387 MemoryContextReset(gvs->tmpCxt);
388
389 if (GinDataLeafPageIsEmpty(page))
390 hasVoidPage = true;
391
392 blkno = GinPageGetOpaque(page)->rightlink;
393
394 UnlockReleaseBuffer(buffer);
395
396 if (blkno == InvalidBlockNumber)
397 break;
398
399 buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
400 RBM_NORMAL, gvs->strategy);
401 LockBuffer(buffer, GIN_EXCLUSIVE);
402 page = BufferGetPage(buffer);
403 }
404
405 return hasVoidPage;
406 }
407
408 static void
ginVacuumPostingTree(GinVacuumState * gvs,BlockNumber rootBlkno)409 ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno)
410 {
411 if (ginVacuumPostingTreeLeaves(gvs, rootBlkno))
412 {
413 /*
414 * There is at least one empty page. So we have to rescan the tree
415 * deleting empty pages.
416 */
417 Buffer buffer;
418 DataPageDeleteStack root,
419 *ptr,
420 *tmp;
421
422 buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, rootBlkno,
423 RBM_NORMAL, gvs->strategy);
424
425 /*
426 * Lock posting tree root for cleanup to ensure there are no
427 * concurrent inserts.
428 */
429 LockBufferForCleanup(buffer);
430
431 memset(&root, 0, sizeof(DataPageDeleteStack));
432 root.leftBuffer = InvalidBuffer;
433 root.isRoot = true;
434
435 ginScanToDelete(gvs, rootBlkno, true, &root, InvalidOffsetNumber);
436
437 ptr = root.child;
438
439 while (ptr)
440 {
441 tmp = ptr->child;
442 pfree(ptr);
443 ptr = tmp;
444 }
445
446 UnlockReleaseBuffer(buffer);
447 }
448 }
449
450 /*
451 * returns modified page or NULL if page isn't modified.
452 * Function works with original page until first change is occurred,
453 * then page is copied into temporary one.
454 */
455 static Page
ginVacuumEntryPage(GinVacuumState * gvs,Buffer buffer,BlockNumber * roots,uint32 * nroot)456 ginVacuumEntryPage(GinVacuumState *gvs, Buffer buffer, BlockNumber *roots, uint32 *nroot)
457 {
458 Page origpage = BufferGetPage(buffer),
459 tmppage;
460 OffsetNumber i,
461 maxoff = PageGetMaxOffsetNumber(origpage);
462
463 tmppage = origpage;
464
465 *nroot = 0;
466
467 for (i = FirstOffsetNumber; i <= maxoff; i++)
468 {
469 IndexTuple itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i));
470
471 if (GinIsPostingTree(itup))
472 {
473 /*
474 * store posting tree's roots for further processing, we can't
475 * vacuum it just now due to risk of deadlocks with scans/inserts
476 */
477 roots[*nroot] = GinGetDownlink(itup);
478 (*nroot)++;
479 }
480 else if (GinGetNPosting(itup) > 0)
481 {
482 int nitems;
483 ItemPointer items_orig;
484 bool free_items_orig;
485 ItemPointer items;
486
487 /* Get list of item pointers from the tuple. */
488 if (GinItupIsCompressed(itup))
489 {
490 items_orig = ginPostingListDecode((GinPostingList *) GinGetPosting(itup), &nitems);
491 free_items_orig = true;
492 }
493 else
494 {
495 items_orig = (ItemPointer) GinGetPosting(itup);
496 nitems = GinGetNPosting(itup);
497 free_items_orig = false;
498 }
499
500 /* Remove any items from the list that need to be vacuumed. */
501 items = ginVacuumItemPointers(gvs, items_orig, nitems, &nitems);
502
503 if (free_items_orig)
504 pfree(items_orig);
505
506 /* If any item pointers were removed, recreate the tuple. */
507 if (items)
508 {
509 OffsetNumber attnum;
510 Datum key;
511 GinNullCategory category;
512 GinPostingList *plist;
513 int plistsize;
514
515 if (nitems > 0)
516 {
517 plist = ginCompressPostingList(items, nitems, GinMaxItemSize, NULL);
518 plistsize = SizeOfGinPostingList(plist);
519 }
520 else
521 {
522 plist = NULL;
523 plistsize = 0;
524 }
525
526 /*
527 * if we already created a temporary page, make changes in
528 * place
529 */
530 if (tmppage == origpage)
531 {
532 /*
533 * On first difference, create a temporary copy of the
534 * page and copy the tuple's posting list to it.
535 */
536 tmppage = PageGetTempPageCopy(origpage);
537
538 /* set itup pointer to new page */
539 itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i));
540 }
541
542 attnum = gintuple_get_attrnum(&gvs->ginstate, itup);
543 key = gintuple_get_key(&gvs->ginstate, itup, &category);
544 itup = GinFormTuple(&gvs->ginstate, attnum, key, category,
545 (char *) plist, plistsize,
546 nitems, true);
547 if (plist)
548 pfree(plist);
549 PageIndexTupleDelete(tmppage, i);
550
551 if (PageAddItem(tmppage, (Item) itup, IndexTupleSize(itup), i, false, false) != i)
552 elog(ERROR, "failed to add item to index page in \"%s\"",
553 RelationGetRelationName(gvs->index));
554
555 pfree(itup);
556 pfree(items);
557 }
558 }
559 }
560
561 return (tmppage == origpage) ? NULL : tmppage;
562 }
563
564 IndexBulkDeleteResult *
ginbulkdelete(IndexVacuumInfo * info,IndexBulkDeleteResult * stats,IndexBulkDeleteCallback callback,void * callback_state)565 ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
566 IndexBulkDeleteCallback callback, void *callback_state)
567 {
568 Relation index = info->index;
569 BlockNumber blkno = GIN_ROOT_BLKNO;
570 GinVacuumState gvs;
571 Buffer buffer;
572 BlockNumber rootOfPostingTree[BLCKSZ / (sizeof(IndexTupleData) + sizeof(ItemId))];
573 uint32 nRoot;
574
575 gvs.tmpCxt = AllocSetContextCreate(CurrentMemoryContext,
576 "Gin vacuum temporary context",
577 ALLOCSET_DEFAULT_SIZES);
578 gvs.index = index;
579 gvs.callback = callback;
580 gvs.callback_state = callback_state;
581 gvs.strategy = info->strategy;
582 initGinState(&gvs.ginstate, index);
583
584 /* first time through? */
585 if (stats == NULL)
586 {
587 /* Yes, so initialize stats to zeroes */
588 stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
589
590 /*
591 * and cleanup any pending inserts
592 */
593 ginInsertCleanup(&gvs.ginstate, !IsAutoVacuumWorkerProcess(),
594 false, true, stats);
595 }
596
597 /* we'll re-count the tuples each time */
598 stats->num_index_tuples = 0;
599 gvs.result = stats;
600
601 buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
602 RBM_NORMAL, info->strategy);
603
604 /* find leaf page */
605 for (;;)
606 {
607 Page page = BufferGetPage(buffer);
608 IndexTuple itup;
609
610 LockBuffer(buffer, GIN_SHARE);
611
612 Assert(!GinPageIsData(page));
613
614 if (GinPageIsLeaf(page))
615 {
616 LockBuffer(buffer, GIN_UNLOCK);
617 LockBuffer(buffer, GIN_EXCLUSIVE);
618
619 if (blkno == GIN_ROOT_BLKNO && !GinPageIsLeaf(page))
620 {
621 LockBuffer(buffer, GIN_UNLOCK);
622 continue; /* check it one more */
623 }
624 break;
625 }
626
627 Assert(PageGetMaxOffsetNumber(page) >= FirstOffsetNumber);
628
629 itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, FirstOffsetNumber));
630 blkno = GinGetDownlink(itup);
631 Assert(blkno != InvalidBlockNumber);
632
633 UnlockReleaseBuffer(buffer);
634 buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
635 RBM_NORMAL, info->strategy);
636 }
637
638 /* right now we found leftmost page in entry's BTree */
639
640 for (;;)
641 {
642 Page page = BufferGetPage(buffer);
643 Page resPage;
644 uint32 i;
645
646 Assert(!GinPageIsData(page));
647
648 resPage = ginVacuumEntryPage(&gvs, buffer, rootOfPostingTree, &nRoot);
649
650 blkno = GinPageGetOpaque(page)->rightlink;
651
652 if (resPage)
653 {
654 START_CRIT_SECTION();
655 PageRestoreTempPage(resPage, page);
656 MarkBufferDirty(buffer);
657 xlogVacuumPage(gvs.index, buffer);
658 UnlockReleaseBuffer(buffer);
659 END_CRIT_SECTION();
660 }
661 else
662 {
663 UnlockReleaseBuffer(buffer);
664 }
665
666 vacuum_delay_point();
667
668 for (i = 0; i < nRoot; i++)
669 {
670 ginVacuumPostingTree(&gvs, rootOfPostingTree[i]);
671 vacuum_delay_point();
672 }
673
674 if (blkno == InvalidBlockNumber) /* rightmost page */
675 break;
676
677 buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
678 RBM_NORMAL, info->strategy);
679 LockBuffer(buffer, GIN_EXCLUSIVE);
680 }
681
682 MemoryContextDelete(gvs.tmpCxt);
683
684 return gvs.result;
685 }
686
687 IndexBulkDeleteResult *
ginvacuumcleanup(IndexVacuumInfo * info,IndexBulkDeleteResult * stats)688 ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
689 {
690 Relation index = info->index;
691 bool needLock;
692 BlockNumber npages,
693 blkno;
694 BlockNumber totFreePages;
695 GinState ginstate;
696 GinStatsData idxStat;
697
698 /*
699 * In an autovacuum analyze, we want to clean up pending insertions.
700 * Otherwise, an ANALYZE-only call is a no-op.
701 */
702 if (info->analyze_only)
703 {
704 if (IsAutoVacuumWorkerProcess())
705 {
706 initGinState(&ginstate, index);
707 ginInsertCleanup(&ginstate, false, true, true, stats);
708 }
709 return stats;
710 }
711
712 /*
713 * Set up all-zero stats and cleanup pending inserts if ginbulkdelete
714 * wasn't called
715 */
716 if (stats == NULL)
717 {
718 stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
719 initGinState(&ginstate, index);
720 ginInsertCleanup(&ginstate, !IsAutoVacuumWorkerProcess(),
721 false, true, stats);
722 }
723
724 memset(&idxStat, 0, sizeof(idxStat));
725
726 /*
727 * XXX we always report the heap tuple count as the number of index
728 * entries. This is bogus if the index is partial, but it's real hard to
729 * tell how many distinct heap entries are referenced by a GIN index.
730 */
731 stats->num_index_tuples = Max(info->num_heap_tuples, 0);
732 stats->estimated_count = info->estimated_count;
733
734 /*
735 * Need lock unless it's local to this backend.
736 */
737 needLock = !RELATION_IS_LOCAL(index);
738
739 if (needLock)
740 LockRelationForExtension(index, ExclusiveLock);
741 npages = RelationGetNumberOfBlocks(index);
742 if (needLock)
743 UnlockRelationForExtension(index, ExclusiveLock);
744
745 totFreePages = 0;
746
747 for (blkno = GIN_ROOT_BLKNO; blkno < npages; blkno++)
748 {
749 Buffer buffer;
750 Page page;
751
752 vacuum_delay_point();
753
754 buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
755 RBM_NORMAL, info->strategy);
756 LockBuffer(buffer, GIN_SHARE);
757 page = (Page) BufferGetPage(buffer);
758
759 if (GinPageIsRecyclable(page))
760 {
761 Assert(blkno != GIN_ROOT_BLKNO);
762 RecordFreeIndexPage(index, blkno);
763 totFreePages++;
764 }
765 else if (GinPageIsData(page))
766 {
767 idxStat.nDataPages++;
768 }
769 else if (!GinPageIsList(page))
770 {
771 idxStat.nEntryPages++;
772
773 if (GinPageIsLeaf(page))
774 idxStat.nEntries += PageGetMaxOffsetNumber(page);
775 }
776
777 UnlockReleaseBuffer(buffer);
778 }
779
780 /* Update the metapage with accurate page and entry counts */
781 idxStat.nTotalPages = npages;
782 ginUpdateStats(info->index, &idxStat, false);
783
784 /* Finally, vacuum the FSM */
785 IndexFreeSpaceMapVacuum(info->index);
786
787 stats->pages_free = totFreePages;
788
789 if (needLock)
790 LockRelationForExtension(index, ExclusiveLock);
791 stats->num_pages = RelationGetNumberOfBlocks(index);
792 if (needLock)
793 UnlockRelationForExtension(index, ExclusiveLock);
794
795 return stats;
796 }
797
798 /*
799 * Return whether Page can safely be recycled.
800 */
801 bool
GinPageIsRecyclable(Page page)802 GinPageIsRecyclable(Page page)
803 {
804 TransactionId delete_xid;
805
806 if (PageIsNew(page))
807 return true;
808
809 if (!GinPageIsDeleted(page))
810 return false;
811
812 delete_xid = GinPageGetDeleteXid(page);
813
814 if (!TransactionIdIsValid(delete_xid))
815 return true;
816
817 /*
818 * If no backend still could view delete_xid as in running, all scans
819 * concurrent with ginDeletePage() must have finished.
820 */
821 return GlobalVisCheckRemovableXid(NULL, delete_xid);
822 }
823