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