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" 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 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 */ 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 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; 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 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 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