1 /*-------------------------------------------------------------------------
2 *
3 * gistvacuum.c
4 * vacuuming routines for the postgres GiST index access method.
5 *
6 *
7 * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
8 * Portions Copyright (c) 1994, Regents of the University of California
9 *
10 * IDENTIFICATION
11 * src/backend/access/gist/gistvacuum.c
12 *
13 *-------------------------------------------------------------------------
14 */
15 #include "postgres.h"
16
17 #include "access/genam.h"
18 #include "access/gist_private.h"
19 #include "access/transam.h"
20 #include "commands/vacuum.h"
21 #include "lib/integerset.h"
22 #include "miscadmin.h"
23 #include "storage/indexfsm.h"
brin_xlog_createidx(XLogReaderState * record)24 #include "storage/lmgr.h"
25 #include "utils/memutils.h"
26
27 /*
28 * State kept across vacuum stages.
29 */
30 typedef struct
31 {
32 IndexBulkDeleteResult stats; /* must be first */
33
34 /*
35 * These are used to memorize all internal and empty leaf pages in the 1st
36 * vacuum stage. They are used in the 2nd stage, to delete all the empty
37 * pages.
38 */
39 IntegerSet *internal_page_set;
40 IntegerSet *empty_leaf_set;
41 MemoryContext page_set_context;
42 } GistBulkDeleteResult;
43
44 /* Working state needed by gistbulkdelete */
45 typedef struct
brin_xlog_insert_update(XLogReaderState * record,xl_brin_insert * xlrec)46 {
47 IndexVacuumInfo *info;
48 GistBulkDeleteResult *stats;
49 IndexBulkDeleteCallback callback;
50 void *callback_state;
51 GistNSN startNSN;
52 } GistVacState;
53
54 static void gistvacuumscan(IndexVacuumInfo *info, GistBulkDeleteResult *stats,
55 IndexBulkDeleteCallback callback, void *callback_state);
56 static void gistvacuumpage(GistVacState *vstate, BlockNumber blkno,
57 BlockNumber orig_blkno);
58 static void gistvacuum_delete_empty_pages(IndexVacuumInfo *info,
59 GistBulkDeleteResult *stats);
60 static bool gistdeletepage(IndexVacuumInfo *info, GistBulkDeleteResult *stats,
61 Buffer buffer, OffsetNumber downlink,
62 Buffer leafBuffer);
63
64 /* allocate the 'stats' struct that's kept over vacuum stages */
65 static GistBulkDeleteResult *
66 create_GistBulkDeleteResult(void)
67 {
68 GistBulkDeleteResult *gist_stats;
69
70 gist_stats = (GistBulkDeleteResult *) palloc0(sizeof(GistBulkDeleteResult));
71 gist_stats->page_set_context =
72 GenerationContextCreate(CurrentMemoryContext,
73 "GiST VACUUM page set context",
74 16 * 1024);
75
76 return gist_stats;
77 }
78
79 /*
80 * VACUUM bulkdelete stage: remove index entries.
81 */
82 IndexBulkDeleteResult *
83 gistbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
84 IndexBulkDeleteCallback callback, void *callback_state)
85 {
86 GistBulkDeleteResult *gist_stats = (GistBulkDeleteResult *) stats;
87
88 /* allocate stats if first time through, else re-use existing struct */
89 if (gist_stats == NULL)
90 gist_stats = create_GistBulkDeleteResult();
91
92 gistvacuumscan(info, gist_stats, callback, callback_state);
93
94 return (IndexBulkDeleteResult *) gist_stats;
95 }
96
97 /*
98 * VACUUM cleanup stage: delete empty pages, and update index statistics.
99 */
100 IndexBulkDeleteResult *
101 gistvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
102 {
103 GistBulkDeleteResult *gist_stats = (GistBulkDeleteResult *) stats;
104
105 /* No-op in ANALYZE ONLY mode */
106 if (info->analyze_only)
107 return stats;
108
109 /*
110 * If gistbulkdelete was called, we need not do anything, just return the
111 * stats from the latest gistbulkdelete call. If it wasn't called, we
112 * still need to do a pass over the index, to obtain index statistics.
113 */
114 if (gist_stats == NULL)
115 {
116 gist_stats = create_GistBulkDeleteResult();
117 gistvacuumscan(info, gist_stats, NULL, NULL);
118 }
119
120 /*
121 * If we saw any empty pages, try to unlink them from the tree so that
122 * they can be reused.
123 */
brin_xlog_insert(XLogReaderState * record)124 gistvacuum_delete_empty_pages(info, gist_stats);
125
126 /* we don't need the internal and empty page sets anymore */
127 MemoryContextDelete(gist_stats->page_set_context);
128 gist_stats->page_set_context = NULL;
129 gist_stats->internal_page_set = NULL;
130 gist_stats->empty_leaf_set = NULL;
131
132 /*
133 * It's quite possible for us to be fooled by concurrent page splits into
134 * double-counting some index tuples, so disbelieve any total that exceeds
brin_xlog_update(XLogReaderState * record)135 * the underlying heap's count ... if we know that accurately. Otherwise
136 * this might just make matters worse.
137 */
138 if (!info->estimated_count)
139 {
140 if (gist_stats->stats.num_index_tuples > info->num_heap_tuples)
141 gist_stats->stats.num_index_tuples = info->num_heap_tuples;
142 }
143
144 return (IndexBulkDeleteResult *) gist_stats;
145 }
146
147 /*
148 * gistvacuumscan --- scan the index for VACUUMing purposes
149 *
150 * This scans the index for leaf tuples that are deletable according to the
151 * vacuum callback, and updates the stats. Both btbulkdelete and
152 * btvacuumcleanup invoke this (the latter only if no btbulkdelete call
153 * occurred).
154 *
155 * This also makes note of any empty leaf pages, as well as all internal
156 * pages. The second stage, gistvacuum_delete_empty_pages(), needs that
157 * information. Any deleted pages are added directly to the free space map.
158 * (They should've been added there when they were originally deleted, already,
159 * but it's possible that the FSM was lost at a crash, for example.)
160 *
161 * The caller is responsible for initially allocating/zeroing a stats struct.
162 */
163 static void
164 gistvacuumscan(IndexVacuumInfo *info, GistBulkDeleteResult *stats,
165 IndexBulkDeleteCallback callback, void *callback_state)
166 {
167 Relation rel = info->index;
168 GistVacState vstate;
169 BlockNumber num_pages;
brin_xlog_samepage_update(XLogReaderState * record)170 bool needLock;
171 BlockNumber blkno;
172 MemoryContext oldctx;
173
174 /*
175 * Reset counts that will be incremented during the scan; needed in case
176 * of multiple scans during a single VACUUM command.
177 */
178 stats->stats.estimated_count = false;
179 stats->stats.num_index_tuples = 0;
180 stats->stats.pages_deleted = 0;
181 stats->stats.pages_free = 0;
182 MemoryContextReset(stats->page_set_context);
183
184 /*
185 * Create the integer sets to remember all the internal and the empty leaf
186 * pages in page_set_context. Internally, the integer set will remember
187 * this context so that the subsequent allocations for these integer sets
188 * will be done from the same context.
189 */
190 oldctx = MemoryContextSwitchTo(stats->page_set_context);
191 stats->internal_page_set = intset_create();
192 stats->empty_leaf_set = intset_create();
193 MemoryContextSwitchTo(oldctx);
194
195 /* Set up info to pass down to gistvacuumpage */
196 vstate.info = info;
197 vstate.stats = stats;
198 vstate.callback = callback;
199 vstate.callback_state = callback_state;
200 if (RelationNeedsWAL(rel))
201 vstate.startNSN = GetInsertRecPtr();
202 else
203 vstate.startNSN = gistGetFakeLSN(rel);
204
205 /*
206 * The outer loop iterates over all index pages, in physical order (we
207 * hope the kernel will cooperate in providing read-ahead for speed). It
brin_xlog_revmap_extend(XLogReaderState * record)208 * is critical that we visit all leaf pages, including ones added after we
209 * start the scan, else we might fail to delete some deletable tuples.
210 * Hence, we must repeatedly check the relation length. We must acquire
211 * the relation-extension lock while doing so to avoid a race condition:
212 * if someone else is extending the relation, there is a window where
213 * bufmgr/smgr have created a new all-zero page but it hasn't yet been
214 * write-locked by gistNewBuffer(). If we manage to scan such a page
215 * here, we'll improperly assume it can be recycled. Taking the lock
216 * synchronizes things enough to prevent a problem: either num_pages won't
217 * include the new page, or gistNewBuffer already has write lock on the
218 * buffer and it will be fully initialized before we can examine it. (See
219 * also vacuumlazy.c, which has the same issue.) Also, we need not worry
220 * if a page is added immediately after we look; the page splitting code
221 * already has write-lock on the left page before it adds a right page, so
222 * we must already have processed any tuples due to be moved into such a
223 * page.
224 *
225 * We can skip locking for new or temp relations, however, since no one
226 * else could be accessing them.
227 */
228 needLock = !RELATION_IS_LOCAL(rel);
229
230 blkno = GIST_ROOT_BLKNO;
231 for (;;)
232 {
233 /* Get the current relation length */
234 if (needLock)
235 LockRelationForExtension(rel, ExclusiveLock);
236 num_pages = RelationGetNumberOfBlocks(rel);
237 if (needLock)
238 UnlockRelationForExtension(rel, ExclusiveLock);
239
240 /* Quit if we've scanned the whole relation */
241 if (blkno >= num_pages)
242 break;
243 /* Iterate over pages, then loop back to recheck length */
244 for (; blkno < num_pages; blkno++)
245 gistvacuumpage(&vstate, blkno, blkno);
246 }
247
248 /*
249 * If we found any recyclable pages (and recorded them in the FSM), then
250 * forcibly update the upper-level FSM pages to ensure that searchers can
251 * find them. It's possible that the pages were also found during
252 * previous scans and so this is a waste of time, but it's cheap enough
253 * relative to scanning the index that it shouldn't matter much, and
254 * making sure that free pages are available sooner not later seems
255 * worthwhile.
256 *
257 * Note that if no recyclable pages exist, we don't bother vacuuming the
258 * FSM at all.
259 */
260 if (stats->stats.pages_free > 0)
261 IndexFreeSpaceMapVacuum(rel);
262
263 /* update statistics */
264 stats->stats.num_pages = num_pages;
265 }
266
267 /*
268 * gistvacuumpage --- VACUUM one page
brin_xlog_desummarize_page(XLogReaderState * record)269 *
270 * This processes a single page for gistbulkdelete(). In some cases we
271 * must go back and re-examine previously-scanned pages; this routine
272 * recurses when necessary to handle that case.
273 *
274 * blkno is the page to process. orig_blkno is the highest block number
275 * reached by the outer gistvacuumscan loop (the same as blkno, unless we
276 * are recursing to re-examine a previous page).
277 */
278 static void
279 gistvacuumpage(GistVacState *vstate, BlockNumber blkno, BlockNumber orig_blkno)
280 {
281 GistBulkDeleteResult *stats = vstate->stats;
282 IndexVacuumInfo *info = vstate->info;
283 IndexBulkDeleteCallback callback = vstate->callback;
284 void *callback_state = vstate->callback_state;
285 Relation rel = info->index;
286 Buffer buffer;
287 Page page;
288 BlockNumber recurse_to;
289
290 restart:
291 recurse_to = InvalidBlockNumber;
292
293 /* call vacuum_delay_point while not holding any buffer lock */
294 vacuum_delay_point();
295
296 buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
297 info->strategy);
298
299 /*
300 * We are not going to stay here for a long time, aggressively grab an
301 * exclusive lock.
302 */
303 LockBuffer(buffer, GIST_EXCLUSIVE);
304 page = (Page) BufferGetPage(buffer);
305
306 if (gistPageRecyclable(page))
307 {
308 /* Okay to recycle this page */
309 RecordFreeIndexPage(rel, blkno);
310 stats->stats.pages_free++;
311 stats->stats.pages_deleted++;
312 }
313 else if (GistPageIsDeleted(page))
314 {
315 /* Already deleted, but can't recycle yet */
316 stats->stats.pages_deleted++;
317 }
318 else if (GistPageIsLeaf(page))
319 {
320 OffsetNumber todelete[MaxOffsetNumber];
321 int ntodelete = 0;
322 int nremain;
323 GISTPageOpaque opaque = GistPageGetOpaque(page);
324 OffsetNumber maxoff = PageGetMaxOffsetNumber(page);
325
326 /*
327 * Check whether we need to recurse back to earlier pages. What we
328 * are concerned about is a page split that happened since we started
329 * the vacuum scan. If the split moved some tuples to a lower page
330 * then we might have missed 'em. If so, set up for tail recursion.
331 *
332 * This is similar to the checks we do during searches, when following
333 * a downlink, but we don't need to jump to higher-numbered pages,
334 * because we will process them later, anyway.
335 */
336 if ((GistFollowRight(page) ||
337 vstate->startNSN < GistPageGetNSN(page)) &&
338 (opaque->rightlink != InvalidBlockNumber) &&
339 (opaque->rightlink < orig_blkno))
340 {
341 recurse_to = opaque->rightlink;
342 }
343
344 /*
345 * Scan over all items to see which ones need to be deleted according
346 * to the callback function.
347 */
348 if (callback)
349 {
350 OffsetNumber off;
351
352 for (off = FirstOffsetNumber;
353 off <= maxoff;
354 off = OffsetNumberNext(off))
355 {
356 ItemId iid = PageGetItemId(page, off);
357 IndexTuple idxtuple = (IndexTuple) PageGetItem(page, iid);
358
359 if (callback(&(idxtuple->t_tid), callback_state))
360 todelete[ntodelete++] = off;
361 }
362 }
363
364 /*
365 * Apply any needed deletes. We issue just one WAL record per page,
366 * so as to minimize WAL traffic.
367 */
368 if (ntodelete > 0)
369 {
370 START_CRIT_SECTION();
371
372 MarkBufferDirty(buffer);
373
374 PageIndexMultiDelete(page, todelete, ntodelete);
375 GistMarkTuplesDeleted(page);
376
377 if (RelationNeedsWAL(rel))
378 {
379 XLogRecPtr recptr;
380
381 recptr = gistXLogUpdate(buffer,
382 todelete, ntodelete,
383 NULL, 0, InvalidBuffer);
384 PageSetLSN(page, recptr);
385 }
386 else
387 PageSetLSN(page, gistGetFakeLSN(rel));
388
389 END_CRIT_SECTION();
390
391 stats->stats.tuples_removed += ntodelete;
392 /* must recompute maxoff */
393 maxoff = PageGetMaxOffsetNumber(page);
394 }
395
396 nremain = maxoff - FirstOffsetNumber + 1;
397 if (nremain == 0)
398 {
399 /*
400 * The page is now completely empty. Remember its block number,
401 * so that we will try to delete the page in the second stage.
402 *
403 * Skip this when recursing, because IntegerSet requires that the
404 * values are added in ascending order. The next VACUUM will pick
405 * it up.
406 */
407 if (blkno == orig_blkno)
408 intset_add_member(stats->empty_leaf_set, blkno);
409 }
410 else
411 stats->stats.num_index_tuples += nremain;
412 }
413 else
414 {
415 /*
416 * On an internal page, check for "invalid tuples", left behind by an
417 * incomplete page split on PostgreSQL 9.0 or below. These are not
418 * created by newer PostgreSQL versions, but unfortunately, there is
419 * no version number anywhere in a GiST index, so we don't know
420 * whether this index might still contain invalid tuples or not.
421 */
422 OffsetNumber maxoff = PageGetMaxOffsetNumber(page);
423 OffsetNumber off;
424
425 for (off = FirstOffsetNumber;
426 off <= maxoff;
427 off = OffsetNumberNext(off))
428 {
429 ItemId iid = PageGetItemId(page, off);
430 IndexTuple idxtuple = (IndexTuple) PageGetItem(page, iid);
431
432 if (GistTupleIsInvalid(idxtuple))
433 ereport(LOG,
434 (errmsg("index \"%s\" contains an inner tuple marked as invalid",
435 RelationGetRelationName(rel)),
436 errdetail("This is caused by an incomplete page split at crash recovery before upgrading to PostgreSQL 9.1."),
437 errhint("Please REINDEX it.")));
438 }
439
440 /*
441 * Remember the block number of this page, so that we can revisit it
442 * later in gistvacuum_delete_empty_pages(), when we search for
443 * parents of empty leaf pages.
444 */
445 if (blkno == orig_blkno)
446 intset_add_member(stats->internal_page_set, blkno);
447 }
448
449 UnlockReleaseBuffer(buffer);
450
451 /*
452 * This is really tail recursion, but if the compiler is too stupid to
453 * optimize it as such, we'd eat an uncomfortably large amount of stack
454 * space per recursion level (due to the deletable[] array). A failure is
455 * improbable since the number of levels isn't likely to be large ... but
456 * just in case, let's hand-optimize into a loop.
457 */
458 if (recurse_to != InvalidBlockNumber)
459 {
460 blkno = recurse_to;
461 goto restart;
462 }
463 }
464
465 /*
466 * Scan all internal pages, and try to delete their empty child pages.
467 */
468 static void
469 gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistBulkDeleteResult *stats)
470 {
471 Relation rel = info->index;
472 BlockNumber empty_pages_remaining;
473 uint64 blkno;
474
475 /*
476 * Rescan all inner pages to find those that have empty child pages.
477 */
478 empty_pages_remaining = intset_num_entries(stats->empty_leaf_set);
479 intset_begin_iterate(stats->internal_page_set);
480 while (empty_pages_remaining > 0 &&
481 intset_iterate_next(stats->internal_page_set, &blkno))
482 {
483 Buffer buffer;
484 Page page;
485 OffsetNumber off,
486 maxoff;
487 OffsetNumber todelete[MaxOffsetNumber];
488 BlockNumber leafs_to_delete[MaxOffsetNumber];
489 int ntodelete;
490 int deleted;
491
492 buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno,
493 RBM_NORMAL, info->strategy);
494
495 LockBuffer(buffer, GIST_SHARE);
496 page = (Page) BufferGetPage(buffer);
497
498 if (PageIsNew(page) || GistPageIsDeleted(page) || GistPageIsLeaf(page))
499 {
500 /*
501 * This page was an internal page earlier, but now it's something
502 * else. Shouldn't happen...
503 */
504 Assert(false);
505 UnlockReleaseBuffer(buffer);
506 continue;
507 }
508
509 /*
510 * Scan all the downlinks, and see if any of them point to empty leaf
511 * pages.
512 */
513 maxoff = PageGetMaxOffsetNumber(page);
514 ntodelete = 0;
515 for (off = FirstOffsetNumber;
516 off <= maxoff && ntodelete < maxoff - 1;
517 off = OffsetNumberNext(off))
518 {
519 ItemId iid = PageGetItemId(page, off);
520 IndexTuple idxtuple = (IndexTuple) PageGetItem(page, iid);
521 BlockNumber leafblk;
522
523 leafblk = ItemPointerGetBlockNumber(&(idxtuple->t_tid));
524 if (intset_is_member(stats->empty_leaf_set, leafblk))
525 {
526 leafs_to_delete[ntodelete] = leafblk;
527 todelete[ntodelete++] = off;
528 }
529 }
530
531 /*
532 * In order to avoid deadlock, child page must be locked before
533 * parent, so we must release the lock on the parent, lock the child,
534 * and then re-acquire the lock the parent. (And we wouldn't want to
535 * do I/O, while holding a lock, anyway.)
536 *
537 * At the instant that we're not holding a lock on the parent, the
538 * downlink might get moved by a concurrent insert, so we must
539 * re-check that it still points to the same child page after we have
540 * acquired both locks. Also, another backend might have inserted a
541 * tuple to the page, so that it is no longer empty. gistdeletepage()
542 * re-checks all these conditions.
543 */
544 LockBuffer(buffer, GIST_UNLOCK);
545
546 deleted = 0;
547 for (int i = 0; i < ntodelete; i++)
548 {
549 Buffer leafbuf;
550
551 /*
552 * Don't remove the last downlink from the parent. That would
553 * confuse the insertion code.
554 */
555 if (PageGetMaxOffsetNumber(page) == FirstOffsetNumber)
556 break;
557
558 leafbuf = ReadBufferExtended(rel, MAIN_FORKNUM, leafs_to_delete[i],
559 RBM_NORMAL, info->strategy);
560 LockBuffer(leafbuf, GIST_EXCLUSIVE);
561 gistcheckpage(rel, leafbuf);
562
563 LockBuffer(buffer, GIST_EXCLUSIVE);
564 if (gistdeletepage(info, stats,
565 buffer, todelete[i] - deleted,
566 leafbuf))
567 deleted++;
568 LockBuffer(buffer, GIST_UNLOCK);
569
570 UnlockReleaseBuffer(leafbuf);
571 }
572
573 ReleaseBuffer(buffer);
574
575 /* update stats */
576 stats->stats.pages_removed += deleted;
577
578 /*
579 * We can stop the scan as soon as we have seen the downlinks, even if
580 * we were not able to remove them all.
581 */
582 empty_pages_remaining -= ntodelete;
583 }
584 }
585
586 /*
587 * gistdeletepage takes a leaf page, and its parent, and tries to delete the
588 * leaf. Both pages must be locked.
589 *
590 * Even if the page was empty when we first saw it, a concurrent inserter might
591 * have added a tuple to it since. Similarly, the downlink might have moved.
592 * We re-check all the conditions, to make sure the page is still deletable,
593 * before modifying anything.
594 *
595 * Returns true, if the page was deleted, and false if a concurrent update
596 * prevented it.
597 */
598 static bool
599 gistdeletepage(IndexVacuumInfo *info, GistBulkDeleteResult *stats,
600 Buffer parentBuffer, OffsetNumber downlink,
601 Buffer leafBuffer)
602 {
603 Page parentPage = BufferGetPage(parentBuffer);
604 Page leafPage = BufferGetPage(leafBuffer);
605 ItemId iid;
606 IndexTuple idxtuple;
607 XLogRecPtr recptr;
608 FullTransactionId txid;
609
610 /*
611 * Check that the leaf is still empty and deletable.
612 */
613 if (!GistPageIsLeaf(leafPage))
614 {
615 /* a leaf page should never become a non-leaf page */
616 Assert(false);
617 return false;
618 }
619
620 if (GistFollowRight(leafPage))
621 return false; /* don't mess with a concurrent page split */
622
623 if (PageGetMaxOffsetNumber(leafPage) != InvalidOffsetNumber)
624 return false; /* not empty anymore */
625
626 /*
627 * Ok, the leaf is deletable. Is the downlink in the parent page still
628 * valid? It might have been moved by a concurrent insert. We could try
629 * to re-find it by scanning the page again, possibly moving right if the
630 * was split. But for now, let's keep it simple and just give up. The
631 * next VACUUM will pick it up.
632 */
633 if (PageIsNew(parentPage) || GistPageIsDeleted(parentPage) ||
634 GistPageIsLeaf(parentPage))
635 {
636 /* shouldn't happen, internal pages are never deleted */
637 Assert(false);
638 return false;
639 }
640
641 if (PageGetMaxOffsetNumber(parentPage) < downlink
642 || PageGetMaxOffsetNumber(parentPage) <= FirstOffsetNumber)
643 return false;
644
645 iid = PageGetItemId(parentPage, downlink);
646 idxtuple = (IndexTuple) PageGetItem(parentPage, iid);
647 if (BufferGetBlockNumber(leafBuffer) !=
648 ItemPointerGetBlockNumber(&(idxtuple->t_tid)))
649 return false;
650
651 /*
652 * All good, proceed with the deletion.
653 *
654 * The page cannot be immediately recycled, because in-progress scans that
655 * saw the downlink might still visit it. Mark the page with the current
656 * next-XID counter, so that we know when it can be recycled. Once that
657 * XID becomes older than GlobalXmin, we know that all scans that are
658 * currently in progress must have ended. (That's much more conservative
659 * than needed, but let's keep it safe and simple.)
660 */
661 txid = ReadNextFullTransactionId();
662
663 START_CRIT_SECTION();
664
665 /* mark the page as deleted */
666 MarkBufferDirty(leafBuffer);
667 GistPageSetDeleted(leafPage, txid);
668 stats->stats.pages_deleted++;
669
670 /* remove the downlink from the parent */
671 MarkBufferDirty(parentBuffer);
672 PageIndexTupleDelete(parentPage, downlink);
673
674 if (RelationNeedsWAL(info->index))
675 recptr = gistXLogPageDelete(leafBuffer, txid, parentBuffer, downlink);
676 else
677 recptr = gistGetFakeLSN(info->index);
678 PageSetLSN(parentPage, recptr);
679 PageSetLSN(leafPage, recptr);
680
681 END_CRIT_SECTION();
682
683 return true;
684 }
685