/*------------------------------------------------------------------------- * * nbtpage.c * BTree-specific page management code for the Postgres btree access * method. * * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * * IDENTIFICATION * src/backend/access/nbtree/nbtpage.c * * NOTES * Postgres btree pages look like ordinary relation pages. The opaque * data at high addresses includes pointers to left and right siblings * and flag data describing page state. The first page in a btree, page * zero, is special -- it stores meta-information describing the tree. * Pages one and higher store the actual tree data. * *------------------------------------------------------------------------- */ #include "postgres.h" #include "access/nbtree.h" #include "access/nbtxlog.h" #include "access/tableam.h" #include "access/transam.h" #include "access/xlog.h" #include "access/xloginsert.h" #include "miscadmin.h" #include "storage/indexfsm.h" #include "storage/lmgr.h" #include "storage/predicate.h" #include "storage/procarray.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/snapmgr.h" static BTMetaPageData *_bt_getmeta(Relation rel, Buffer metabuf); static void _bt_log_reuse_page(Relation rel, Relation heaprel, BlockNumber blkno, FullTransactionId safexid); static void _bt_delitems_delete(Relation rel, Relation heaprel, Buffer buf, TransactionId snapshotConflictHorizon, OffsetNumber *deletable, int ndeletable, BTVacuumPosting *updatable, int nupdatable); static char *_bt_delitems_update(BTVacuumPosting *updatable, int nupdatable, OffsetNumber *updatedoffsets, Size *updatedbuflen, bool needswal); static bool _bt_mark_page_halfdead(Relation rel, Relation heaprel, Buffer leafbuf, BTStack stack); static bool _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno, bool *rightsib_empty, BTVacState *vstate); static bool _bt_lock_subtree_parent(Relation rel, Relation heaprel, BlockNumber child, BTStack stack, Buffer *subtreeparent, OffsetNumber *poffset, BlockNumber *topparent, BlockNumber *topparentrightsib); static void _bt_pendingfsm_add(BTVacState *vstate, BlockNumber target, FullTransactionId safexid); /* * _bt_initmetapage() -- Fill a page buffer with a correct metapage image */ void _bt_initmetapage(Page page, BlockNumber rootbknum, uint32 level, bool allequalimage) { BTMetaPageData *metad; BTPageOpaque metaopaque; _bt_pageinit(page, BLCKSZ); metad = BTPageGetMeta(page); metad->btm_magic = BTREE_MAGIC; metad->btm_version = BTREE_VERSION; metad->btm_root = rootbknum; metad->btm_level = level; metad->btm_fastroot = rootbknum; metad->btm_fastlevel = level; metad->btm_last_cleanup_num_delpages = 0; metad->btm_last_cleanup_num_heap_tuples = -1.0; metad->btm_allequalimage = allequalimage; metaopaque = BTPageGetOpaque(page); metaopaque->btpo_flags = BTP_META; /* * Set pd_lower just past the end of the metadata. This is essential, * because without doing so, metadata will be lost if xlog.c compresses * the page. */ ((PageHeader) page)->pd_lower = ((char *) metad + sizeof(BTMetaPageData)) - (char *) page; } /* * _bt_upgrademetapage() -- Upgrade a meta-page from an old format to version * 3, the last version that can be updated without broadly affecting * on-disk compatibility. (A REINDEX is required to upgrade to v4.) * * This routine does purely in-memory image upgrade. Caller is * responsible for locking, WAL-logging etc. */ void _bt_upgrademetapage(Page page) { BTMetaPageData *metad; BTPageOpaque metaopaque PG_USED_FOR_ASSERTS_ONLY; metad = BTPageGetMeta(page); metaopaque = BTPageGetOpaque(page); /* It must be really a meta page of upgradable version */ Assert(metaopaque->btpo_flags & BTP_META); Assert(metad->btm_version < BTREE_NOVAC_VERSION); Assert(metad->btm_version >= BTREE_MIN_VERSION); /* Set version number and fill extra fields added into version 3 */ metad->btm_version = BTREE_NOVAC_VERSION; metad->btm_last_cleanup_num_delpages = 0; metad->btm_last_cleanup_num_heap_tuples = -1.0; /* Only a REINDEX can set this field */ Assert(!metad->btm_allequalimage); metad->btm_allequalimage = false; /* Adjust pd_lower (see _bt_initmetapage() for details) */ ((PageHeader) page)->pd_lower = ((char *) metad + sizeof(BTMetaPageData)) - (char *) page; } /* * Get metadata from share-locked buffer containing metapage, while performing * standard sanity checks. * * Callers that cache data returned here in local cache should note that an * on-the-fly upgrade using _bt_upgrademetapage() can change the version field * and BTREE_NOVAC_VERSION specific fields without invalidating local cache. */ static BTMetaPageData * _bt_getmeta(Relation rel, Buffer metabuf) { Page metapg; BTPageOpaque metaopaque; BTMetaPageData *metad; metapg = BufferGetPage(metabuf); metaopaque = BTPageGetOpaque(metapg); metad = BTPageGetMeta(metapg); /* sanity-check the metapage */ if (!P_ISMETA(metaopaque) || metad->btm_magic != BTREE_MAGIC) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index \"%s\" is not a btree", RelationGetRelationName(rel)))); if (metad->btm_version < BTREE_MIN_VERSION || metad->btm_version > BTREE_VERSION) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("version mismatch in index \"%s\": file version %d, " "current version %d, minimal supported version %d", RelationGetRelationName(rel), metad->btm_version, BTREE_VERSION, BTREE_MIN_VERSION))); return metad; } /* * _bt_vacuum_needs_cleanup() -- Checks if index needs cleanup * * Called by btvacuumcleanup when btbulkdelete was never called because no * index tuples needed to be deleted. */ bool _bt_vacuum_needs_cleanup(Relation rel, Relation heaprel) { Buffer metabuf; Page metapg; BTMetaPageData *metad; uint32 btm_version; BlockNumber prev_num_delpages; /* * Copy details from metapage to local variables quickly. * * Note that we deliberately avoid using cached version of metapage here. */ metabuf = _bt_getbuf(rel, heaprel, BTREE_METAPAGE, BT_READ); metapg = BufferGetPage(metabuf); metad = BTPageGetMeta(metapg); btm_version = metad->btm_version; if (btm_version < BTREE_NOVAC_VERSION) { /* * Metapage needs to be dynamically upgraded to store fields that are * only present when btm_version >= BTREE_NOVAC_VERSION */ _bt_relbuf(rel, metabuf); return true; } prev_num_delpages = metad->btm_last_cleanup_num_delpages; _bt_relbuf(rel, metabuf); /* * Trigger cleanup in rare cases where prev_num_delpages exceeds 5% of the * total size of the index. We can reasonably expect (though are not * guaranteed) to be able to recycle this many pages if we decide to do a * btvacuumscan call during the ongoing btvacuumcleanup. For further * details see the nbtree/README section on placing deleted pages in the * FSM. */ if (prev_num_delpages > 0 && prev_num_delpages > RelationGetNumberOfBlocks(rel) / 20) return true; return false; } /* * _bt_set_cleanup_info() -- Update metapage for btvacuumcleanup. * * Called at the end of btvacuumcleanup, when num_delpages value has been * finalized. */ void _bt_set_cleanup_info(Relation rel, Relation heaprel, BlockNumber num_delpages) { Buffer metabuf; Page metapg; BTMetaPageData *metad; /* * On-disk compatibility note: The btm_last_cleanup_num_delpages metapage * field started out as a TransactionId field called btm_oldest_btpo_xact. * Both "versions" are just uint32 fields. It was convenient to repurpose * the field when we began to use 64-bit XIDs in deleted pages. * * It's possible that a pg_upgrade'd database will contain an XID value in * what is now recognized as the metapage's btm_last_cleanup_num_delpages * field. _bt_vacuum_needs_cleanup() may even believe that this value * indicates that there are lots of pages that it needs to recycle, when * in reality there are only one or two. The worst that can happen is * that there will be a call to btvacuumscan a little earlier, which will * set btm_last_cleanup_num_delpages to a sane value when we're called. * * Note also that the metapage's btm_last_cleanup_num_heap_tuples field is * no longer used as of PostgreSQL 14. We set it to -1.0 on rewrite, just * to be consistent. */ metabuf = _bt_getbuf(rel, heaprel, BTREE_METAPAGE, BT_READ); metapg = BufferGetPage(metabuf); metad = BTPageGetMeta(metapg); /* Don't miss chance to upgrade index/metapage when BTREE_MIN_VERSION */ if (metad->btm_version >= BTREE_NOVAC_VERSION && metad->btm_last_cleanup_num_delpages == num_delpages) { /* Usually means index continues to have num_delpages of 0 */ _bt_relbuf(rel, metabuf); return; } /* trade in our read lock for a write lock */ _bt_unlockbuf(rel, metabuf); _bt_lockbuf(rel, metabuf, BT_WRITE); START_CRIT_SECTION(); /* upgrade meta-page if needed */ if (metad->btm_version < BTREE_NOVAC_VERSION) _bt_upgrademetapage(metapg); /* update cleanup-related information */ metad->btm_last_cleanup_num_delpages = num_delpages; metad->btm_last_cleanup_num_heap_tuples = -1.0; MarkBufferDirty(metabuf); /* write wal record if needed */ if (RelationNeedsWAL(rel)) { xl_btree_metadata md; XLogRecPtr recptr; XLogBeginInsert(); XLogRegisterBuffer(0, metabuf, REGBUF_WILL_INIT | REGBUF_STANDARD); Assert(metad->btm_version >= BTREE_NOVAC_VERSION); md.version = metad->btm_version; md.root = metad->btm_root; md.level = metad->btm_level; md.fastroot = metad->btm_fastroot; md.fastlevel = metad->btm_fastlevel; md.last_cleanup_num_delpages = num_delpages; md.allequalimage = metad->btm_allequalimage; XLogRegisterBufData(0, (char *) &md, sizeof(xl_btree_metadata)); recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_META_CLEANUP); PageSetLSN(metapg, recptr); } END_CRIT_SECTION(); _bt_relbuf(rel, metabuf); } /* * _bt_getroot() -- Get the root page of the btree. * * Since the root page can move around the btree file, we have to read * its location from the metadata page, and then read the root page * itself. If no root page exists yet, we have to create one. * * The access type parameter (BT_READ or BT_WRITE) controls whether * a new root page will be created or not. If access = BT_READ, * and no root page exists, we just return InvalidBuffer. For * BT_WRITE, we try to create the root page if it doesn't exist. * NOTE that the returned root page will have only a read lock set * on it even if access = BT_WRITE! * * The returned page is not necessarily the true root --- it could be * a "fast root" (a page that is alone in its level due to deletions). * Also, if the root page is split while we are "in flight" to it, * what we will return is the old root, which is now just the leftmost * page on a probably-not-very-wide level. For most purposes this is * as good as or better than the true root, so we do not bother to * insist on finding the true root. We do, however, guarantee to * return a live (not deleted or half-dead) page. * * On successful return, the root page is pinned and read-locked. * The metadata page is not locked or pinned on exit. */ Buffer _bt_getroot(Relation rel, Relation heaprel, int access) { Buffer metabuf; Buffer rootbuf; Page rootpage; BTPageOpaque rootopaque; BlockNumber rootblkno; uint32 rootlevel; BTMetaPageData *metad; /* * Try to use previously-cached metapage data to find the root. This * normally saves one buffer access per index search, which is a very * helpful savings in bufmgr traffic and hence contention. */ if (rel->rd_amcache != NULL) { metad = (BTMetaPageData *) rel->rd_amcache; /* We shouldn't have cached it if any of these fail */ Assert(metad->btm_magic == BTREE_MAGIC); Assert(metad->btm_version >= BTREE_MIN_VERSION); Assert(metad->btm_version <= BTREE_VERSION); Assert(!metad->btm_allequalimage || metad->btm_version > BTREE_NOVAC_VERSION); Assert(metad->btm_root != P_NONE); rootblkno = metad->btm_fastroot; Assert(rootblkno != P_NONE); rootlevel = metad->btm_fastlevel; rootbuf = _bt_getbuf(rel, heaprel, rootblkno, BT_READ); rootpage = BufferGetPage(rootbuf); rootopaque = BTPageGetOpaque(rootpage); /* * Since the cache might be stale, we check the page more carefully * here than normal. We *must* check that it's not deleted. If it's * not alone on its level, then we reject too --- this may be overly * paranoid but better safe than sorry. Note we don't check P_ISROOT, * because that's not set in a "fast root". */ if (!P_IGNORE(rootopaque) && rootopaque->btpo_level == rootlevel && P_LEFTMOST(rootopaque) && P_RIGHTMOST(rootopaque)) { /* OK, accept cached page as the root */ return rootbuf; } _bt_relbuf(rel, rootbuf); /* Cache is stale, throw it away */ if (rel->rd_amcache) pfree(rel->rd_amcache); rel->rd_amcache = NULL; } metabuf = _bt_getbuf(rel, heaprel, BTREE_METAPAGE, BT_READ); metad = _bt_getmeta(rel, metabuf); /* if no root page initialized yet, do it */ if (metad->btm_root == P_NONE) { Page metapg; /* If access = BT_READ, caller doesn't want us to create root yet */ if (access == BT_READ) { _bt_relbuf(rel, metabuf); return InvalidBuffer; } /* trade in our read lock for a write lock */ _bt_unlockbuf(rel, metabuf); _bt_lockbuf(rel, metabuf, BT_WRITE); /* * Race condition: if someone else initialized the metadata between * the time we released the read lock and acquired the write lock, we * must avoid doing it again. */ if (metad->btm_root != P_NONE) { /* * Metadata initialized by someone else. In order to guarantee no * deadlocks, we have to release the metadata page and start all * over again. (Is that really true? But it's hardly worth trying * to optimize this case.) */ _bt_relbuf(rel, metabuf); return _bt_getroot(rel, heaprel, access); } /* * Get, initialize, write, and leave a lock of the appropriate type on * the new root page. Since this is the first page in the tree, it's * a leaf as well as the root. */ rootbuf = _bt_getbuf(rel, heaprel, P_NEW, BT_WRITE); rootblkno = BufferGetBlockNumber(rootbuf); rootpage = BufferGetPage(rootbuf); rootopaque = BTPageGetOpaque(rootpage); rootopaque->btpo_prev = rootopaque->btpo_next = P_NONE; rootopaque->btpo_flags = (BTP_LEAF | BTP_ROOT); rootopaque->btpo_level = 0; rootopaque->btpo_cycleid = 0; /* Get raw page pointer for metapage */ metapg = BufferGetPage(metabuf); /* NO ELOG(ERROR) till meta is updated */ START_CRIT_SECTION(); /* upgrade metapage if needed */ if (metad->btm_version < BTREE_NOVAC_VERSION) _bt_upgrademetapage(metapg); metad->btm_root = rootblkno; metad->btm_level = 0; metad->btm_fastroot = rootblkno; metad->btm_fastlevel = 0; metad->btm_last_cleanup_num_delpages = 0; metad->btm_last_cleanup_num_heap_tuples = -1.0; MarkBufferDirty(rootbuf); MarkBufferDirty(metabuf); /* XLOG stuff */ if (RelationNeedsWAL(rel)) { xl_btree_newroot xlrec; XLogRecPtr recptr; xl_btree_metadata md; XLogBeginInsert(); XLogRegisterBuffer(0, rootbuf, REGBUF_WILL_INIT); XLogRegisterBuffer(2, metabuf, REGBUF_WILL_INIT | REGBUF_STANDARD); Assert(metad->btm_version >= BTREE_NOVAC_VERSION); md.version = metad->btm_version; md.root = rootblkno; md.level = 0; md.fastroot = rootblkno; md.fastlevel = 0; md.last_cleanup_num_delpages = 0; md.allequalimage = metad->btm_allequalimage; XLogRegisterBufData(2, (char *) &md, sizeof(xl_btree_metadata)); xlrec.rootblk = rootblkno; xlrec.level = 0; XLogRegisterData((char *) &xlrec, SizeOfBtreeNewroot); recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_NEWROOT); PageSetLSN(rootpage, recptr); PageSetLSN(metapg, recptr); } END_CRIT_SECTION(); /* * swap root write lock for read lock. There is no danger of anyone * else accessing the new root page while it's unlocked, since no one * else knows where it is yet. */ _bt_unlockbuf(rel, rootbuf); _bt_lockbuf(rel, rootbuf, BT_READ); /* okay, metadata is correct, release lock on it without caching */ _bt_relbuf(rel, metabuf); } else { rootblkno = metad->btm_fastroot; Assert(rootblkno != P_NONE); rootlevel = metad->btm_fastlevel; /* * Cache the metapage data for next time */ rel->rd_amcache = MemoryContextAlloc(rel->rd_indexcxt, sizeof(BTMetaPageData)); memcpy(rel->rd_amcache, metad, sizeof(BTMetaPageData)); /* * We are done with the metapage; arrange to release it via first * _bt_relandgetbuf call */ rootbuf = metabuf; for (;;) { rootbuf = _bt_relandgetbuf(rel, rootbuf, rootblkno, BT_READ); rootpage = BufferGetPage(rootbuf); rootopaque = BTPageGetOpaque(rootpage); if (!P_IGNORE(rootopaque)) break; /* it's dead, Jim. step right one page */ if (P_RIGHTMOST(rootopaque)) elog(ERROR, "no live root page found in index \"%s\"", RelationGetRelationName(rel)); rootblkno = rootopaque->btpo_next; } if (rootopaque->btpo_level != rootlevel) elog(ERROR, "root page %u of index \"%s\" has level %u, expected %u", rootblkno, RelationGetRelationName(rel), rootopaque->btpo_level, rootlevel); } /* * By here, we have a pin and read lock on the root page, and no lock set * on the metadata page. Return the root page's buffer. */ return rootbuf; } /* * _bt_gettrueroot() -- Get the true root page of the btree. * * This is the same as the BT_READ case of _bt_getroot(), except * we follow the true-root link not the fast-root link. * * By the time we acquire lock on the root page, it might have been split and * not be the true root anymore. This is okay for the present uses of this * routine; we only really need to be able to move up at least one tree level * from whatever non-root page we were at. If we ever do need to lock the * one true root page, we could loop here, re-reading the metapage on each * failure. (Note that it wouldn't do to hold the lock on the metapage while * moving to the root --- that'd deadlock against any concurrent root split.) */ Buffer _bt_gettrueroot(Relation rel, Relation heaprel) { Buffer metabuf; Page metapg; BTPageOpaque metaopaque; Buffer rootbuf; Page rootpage; BTPageOpaque rootopaque; BlockNumber rootblkno; uint32 rootlevel; BTMetaPageData *metad; /* * We don't try to use cached metapage data here, since (a) this path is * not performance-critical, and (b) if we are here it suggests our cache * is out-of-date anyway. In light of point (b), it's probably safest to * actively flush any cached metapage info. */ if (rel->rd_amcache) pfree(rel->rd_amcache); rel->rd_amcache = NULL; metabuf = _bt_getbuf(rel, heaprel, BTREE_METAPAGE, BT_READ); metapg = BufferGetPage(metabuf); metaopaque = BTPageGetOpaque(metapg); metad = BTPageGetMeta(metapg); if (!P_ISMETA(metaopaque) || metad->btm_magic != BTREE_MAGIC) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index \"%s\" is not a btree", RelationGetRelationName(rel)))); if (metad->btm_version < BTREE_MIN_VERSION || metad->btm_version > BTREE_VERSION) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("version mismatch in index \"%s\": file version %d, " "current version %d, minimal supported version %d", RelationGetRelationName(rel), metad->btm_version, BTREE_VERSION, BTREE_MIN_VERSION))); /* if no root page initialized yet, fail */ if (metad->btm_root == P_NONE) { _bt_relbuf(rel, metabuf); return InvalidBuffer; } rootblkno = metad->btm_root; rootlevel = metad->btm_level; /* * We are done with the metapage; arrange to release it via first * _bt_relandgetbuf call */ rootbuf = metabuf; for (;;) { rootbuf = _bt_relandgetbuf(rel, rootbuf, rootblkno, BT_READ); rootpage = BufferGetPage(rootbuf); rootopaque = BTPageGetOpaque(rootpage); if (!P_IGNORE(rootopaque)) break; /* it's dead, Jim. step right one page */ if (P_RIGHTMOST(rootopaque)) elog(ERROR, "no live root page found in index \"%s\"", RelationGetRelationName(rel)); rootblkno = rootopaque->btpo_next; } if (rootopaque->btpo_level != rootlevel) elog(ERROR, "root page %u of index \"%s\" has level %u, expected %u", rootblkno, RelationGetRelationName(rel), rootopaque->btpo_level, rootlevel); return rootbuf; } /* * _bt_getrootheight() -- Get the height of the btree search tree. * * We return the level (counting from zero) of the current fast root. * This represents the number of tree levels we'd have to descend through * to start any btree index search. * * This is used by the planner for cost-estimation purposes. Since it's * only an estimate, slightly-stale data is fine, hence we don't worry * about updating previously cached data. */ int _bt_getrootheight(Relation rel, Relation heaprel) { BTMetaPageData *metad; if (rel->rd_amcache == NULL) { Buffer metabuf; metabuf = _bt_getbuf(rel, heaprel, BTREE_METAPAGE, BT_READ); metad = _bt_getmeta(rel, metabuf); /* * If there's no root page yet, _bt_getroot() doesn't expect a cache * to be made, so just stop here and report the index height is zero. * (XXX perhaps _bt_getroot() should be changed to allow this case.) */ if (metad->btm_root == P_NONE) { _bt_relbuf(rel, metabuf); return 0; } /* * Cache the metapage data for next time */ rel->rd_amcache = MemoryContextAlloc(rel->rd_indexcxt, sizeof(BTMetaPageData)); memcpy(rel->rd_amcache, metad, sizeof(BTMetaPageData)); _bt_relbuf(rel, metabuf); } /* Get cached page */ metad = (BTMetaPageData *) rel->rd_amcache; /* We shouldn't have cached it if any of these fail */ Assert(metad->btm_magic == BTREE_MAGIC); Assert(metad->btm_version >= BTREE_MIN_VERSION); Assert(metad->btm_version <= BTREE_VERSION); Assert(!metad->btm_allequalimage || metad->btm_version > BTREE_NOVAC_VERSION); Assert(metad->btm_fastroot != P_NONE); return metad->btm_fastlevel; } /* * _bt_metaversion() -- Get version/status info from metapage. * * Sets caller's *heapkeyspace and *allequalimage arguments using data * from the B-Tree metapage (could be locally-cached version). This * information needs to be stashed in insertion scankey, so we provide a * single function that fetches both at once. * * This is used to determine the rules that must be used to descend a * btree. Version 4 indexes treat heap TID as a tiebreaker attribute. * pg_upgrade'd version 3 indexes need extra steps to preserve reasonable * performance when inserting a new BTScanInsert-wise duplicate tuple * among many leaf pages already full of such duplicates. * * Also sets allequalimage field, which indicates whether or not it is * safe to apply deduplication. We rely on the assumption that * btm_allequalimage will be zero'ed on heapkeyspace indexes that were * pg_upgrade'd from Postgres 12. */ void _bt_metaversion(Relation rel, Relation heaprel, bool *heapkeyspace, bool *allequalimage) { BTMetaPageData *metad; if (rel->rd_amcache == NULL) { Buffer metabuf; metabuf = _bt_getbuf(rel, heaprel, BTREE_METAPAGE, BT_READ); metad = _bt_getmeta(rel, metabuf); /* * If there's no root page yet, _bt_getroot() doesn't expect a cache * to be made, so just stop here. (XXX perhaps _bt_getroot() should * be changed to allow this case.) */ if (metad->btm_root == P_NONE) { *heapkeyspace = metad->btm_version > BTREE_NOVAC_VERSION; *allequalimage = metad->btm_allequalimage; _bt_relbuf(rel, metabuf); return; } /* * Cache the metapage data for next time * * An on-the-fly version upgrade performed by _bt_upgrademetapage() * can change the nbtree version for an index without invalidating any * local cache. This is okay because it can only happen when moving * from version 2 to version 3, both of which are !heapkeyspace * versions. */ rel->rd_amcache = MemoryContextAlloc(rel->rd_indexcxt, sizeof(BTMetaPageData)); memcpy(rel->rd_amcache, metad, sizeof(BTMetaPageData)); _bt_relbuf(rel, metabuf); } /* Get cached page */ metad = (BTMetaPageData *) rel->rd_amcache; /* We shouldn't have cached it if any of these fail */ Assert(metad->btm_magic == BTREE_MAGIC); Assert(metad->btm_version >= BTREE_MIN_VERSION); Assert(metad->btm_version <= BTREE_VERSION); Assert(!metad->btm_allequalimage || metad->btm_version > BTREE_NOVAC_VERSION); Assert(metad->btm_fastroot != P_NONE); *heapkeyspace = metad->btm_version > BTREE_NOVAC_VERSION; *allequalimage = metad->btm_allequalimage; } /* * _bt_checkpage() -- Verify that a freshly-read page looks sane. */ void _bt_checkpage(Relation rel, Buffer buf) { Page page = BufferGetPage(buf); /* * ReadBuffer verifies that every newly-read page passes * PageHeaderIsValid, which means it either contains a reasonably sane * page header or is all-zero. We have to defend against the all-zero * case, however. */ if (PageIsNew(page)) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index \"%s\" contains unexpected zero page at block %u", RelationGetRelationName(rel), BufferGetBlockNumber(buf)), errhint("Please REINDEX it."))); /* * Additionally check that the special area looks sane. */ if (PageGetSpecialSize(page) != MAXALIGN(sizeof(BTPageOpaqueData))) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index \"%s\" contains corrupted page at block %u", RelationGetRelationName(rel), BufferGetBlockNumber(buf)), errhint("Please REINDEX it."))); } /* * Log the reuse of a page from the FSM. */ static void _bt_log_reuse_page(Relation rel, Relation heaprel, BlockNumber blkno, FullTransactionId safexid) { xl_btree_reuse_page xlrec_reuse; /* * Note that we don't register the buffer with the record, because this * operation doesn't modify the page. This record only exists to provide a * conflict point for Hot Standby. */ /* XLOG stuff */ xlrec_reuse.isCatalogRel = RelationIsAccessibleInLogicalDecoding(heaprel); xlrec_reuse.locator = rel->rd_locator; xlrec_reuse.block = blkno; xlrec_reuse.snapshotConflictHorizon = safexid; XLogBeginInsert(); XLogRegisterData((char *) &xlrec_reuse, SizeOfBtreeReusePage); XLogInsert(RM_BTREE_ID, XLOG_BTREE_REUSE_PAGE); } /* * _bt_getbuf() -- Get a buffer by block number for read or write. * * blkno == P_NEW means to get an unallocated index page. The page * will be initialized before returning it. * * The general rule in nbtree is that it's never okay to access a * page without holding both a buffer pin and a buffer lock on * the page's buffer. * * When this routine returns, the appropriate lock is set on the * requested buffer and its reference count has been incremented * (ie, the buffer is "locked and pinned"). Also, we apply * _bt_checkpage to sanity-check the page (except in P_NEW case), * and perform Valgrind client requests that help Valgrind detect * unsafe page accesses. * * Note: raw LockBuffer() calls are disallowed in nbtree; all * buffer lock requests need to go through wrapper functions such * as _bt_lockbuf(). */ Buffer _bt_getbuf(Relation rel, Relation heaprel, BlockNumber blkno, int access) { Buffer buf; if (blkno != P_NEW) { /* Read an existing block of the relation */ buf = ReadBuffer(rel, blkno); _bt_lockbuf(rel, buf, access); _bt_checkpage(rel, buf); } else { Page page; Assert(access == BT_WRITE); /* * First see if the FSM knows of any free pages. * * We can't trust the FSM's report unreservedly; we have to check that * the page is still free. (For example, an already-free page could * have been re-used between the time the last VACUUM scanned it and * the time the VACUUM made its FSM updates.) * * In fact, it's worse than that: we can't even assume that it's safe * to take a lock on the reported page. If somebody else has a lock * on it, or even worse our own caller does, we could deadlock. (The * own-caller scenario is actually not improbable. Consider an index * on a serial or timestamp column. Nearly all splits will be at the * rightmost page, so it's entirely likely that _bt_split will call us * while holding a lock on the page most recently acquired from FSM. A * VACUUM running concurrently with the previous split could well have * placed that page back in FSM.) * * To get around that, we ask for only a conditional lock on the * reported page. If we fail, then someone else is using the page, * and we may reasonably assume it's not free. (If we happen to be * wrong, the worst consequence is the page will be lost to use till * the next VACUUM, which is no big problem.) */ for (;;) { blkno = GetFreeIndexPage(rel); if (blkno == InvalidBlockNumber) break; buf = ReadBuffer(rel, blkno); if (_bt_conditionallockbuf(rel, buf)) { page = BufferGetPage(buf); /* * It's possible to find an all-zeroes page in an index. For * example, a backend might successfully extend the relation * one page and then crash before it is able to make a WAL * entry for adding the page. If we find a zeroed page then * reclaim it immediately. */ if (PageIsNew(page)) { /* Okay to use page. Initialize and return it. */ _bt_pageinit(page, BufferGetPageSize(buf)); return buf; } if (BTPageIsRecyclable(page, heaprel)) { /* * If we are generating WAL for Hot Standby then create a * WAL record that will allow us to conflict with queries * running on standby, in case they have snapshots older * than safexid value */ if (XLogStandbyInfoActive() && RelationNeedsWAL(rel)) _bt_log_reuse_page(rel, heaprel, blkno, BTPageGetDeleteXid(page)); /* Okay to use page. Re-initialize and return it. */ _bt_pageinit(page, BufferGetPageSize(buf)); return buf; } elog(DEBUG2, "FSM returned nonrecyclable page"); _bt_relbuf(rel, buf); } else { elog(DEBUG2, "FSM returned nonlockable page"); /* couldn't get lock, so just drop pin */ ReleaseBuffer(buf); } } /* * Extend the relation by one page. Need to use RBM_ZERO_AND_LOCK or * we risk a race condition against btvacuumscan --- see comments * therein. This forces us to repeat the valgrind request that * _bt_lockbuf() otherwise would make, as we can't use _bt_lockbuf() * without introducing a race. */ buf = ExtendBufferedRel(EB_REL(rel), MAIN_FORKNUM, NULL, EB_LOCK_FIRST); if (!RelationUsesLocalBuffers(rel)) VALGRIND_MAKE_MEM_DEFINED(BufferGetPage(buf), BLCKSZ); /* Initialize the new page before returning it */ page = BufferGetPage(buf); Assert(PageIsNew(page)); _bt_pageinit(page, BufferGetPageSize(buf)); } /* ref count and lock type are correct */ return buf; } /* * _bt_relandgetbuf() -- release a locked buffer and get another one. * * This is equivalent to _bt_relbuf followed by _bt_getbuf, with the * exception that blkno may not be P_NEW. Also, if obuf is InvalidBuffer * then it reduces to just _bt_getbuf; allowing this case simplifies some * callers. * * The original motivation for using this was to avoid two entries to the * bufmgr when one would do. However, now it's mainly just a notational * convenience. The only case where it saves work over _bt_relbuf/_bt_getbuf * is when the target page is the same one already in the buffer. */ Buffer _bt_relandgetbuf(Relation rel, Buffer obuf, BlockNumber blkno, int access) { Buffer buf; Assert(blkno != P_NEW); if (BufferIsValid(obuf)) _bt_unlockbuf(rel, obuf); buf = ReleaseAndReadBuffer(obuf, rel, blkno); _bt_lockbuf(rel, buf, access); _bt_checkpage(rel, buf); return buf; } /* * _bt_relbuf() -- release a locked buffer. * * Lock and pin (refcount) are both dropped. */ void _bt_relbuf(Relation rel, Buffer buf) { _bt_unlockbuf(rel, buf); ReleaseBuffer(buf); } /* * _bt_lockbuf() -- lock a pinned buffer. * * Lock is acquired without acquiring another pin. This is like a raw * LockBuffer() call, but performs extra steps needed by Valgrind. * * Note: Caller may need to call _bt_checkpage() with buf when pin on buf * wasn't originally acquired in _bt_getbuf() or _bt_relandgetbuf(). */ void _bt_lockbuf(Relation rel, Buffer buf, int access) { /* LockBuffer() asserts that pin is held by this backend */ LockBuffer(buf, access); /* * It doesn't matter that _bt_unlockbuf() won't get called in the event of * an nbtree error (e.g. a unique violation error). That won't cause * Valgrind false positives. * * The nbtree client requests are superimposed on top of the bufmgr.c * buffer pin client requests. In the event of an nbtree error the buffer * will certainly get marked as defined when the backend once again * acquires its first pin on the buffer. (Of course, if the backend never * touches the buffer again then it doesn't matter that it remains * non-accessible to Valgrind.) * * Note: When an IndexTuple C pointer gets computed using an ItemId read * from a page while a lock was held, the C pointer becomes unsafe to * dereference forever as soon as the lock is released. Valgrind can only * detect cases where the pointer gets dereferenced with no _current_ * lock/pin held, though. */ if (!RelationUsesLocalBuffers(rel)) VALGRIND_MAKE_MEM_DEFINED(BufferGetPage(buf), BLCKSZ); } /* * _bt_unlockbuf() -- unlock a pinned buffer. */ void _bt_unlockbuf(Relation rel, Buffer buf) { /* * Buffer is pinned and locked, which means that it is expected to be * defined and addressable. Check that proactively. */ VALGRIND_CHECK_MEM_IS_DEFINED(BufferGetPage(buf), BLCKSZ); /* LockBuffer() asserts that pin is held by this backend */ LockBuffer(buf, BUFFER_LOCK_UNLOCK); if (!RelationUsesLocalBuffers(rel)) VALGRIND_MAKE_MEM_NOACCESS(BufferGetPage(buf), BLCKSZ); } /* * _bt_conditionallockbuf() -- conditionally BT_WRITE lock pinned * buffer. * * Note: Caller may need to call _bt_checkpage() with buf when pin on buf * wasn't originally acquired in _bt_getbuf() or _bt_relandgetbuf(). */ bool _bt_conditionallockbuf(Relation rel, Buffer buf) { /* ConditionalLockBuffer() asserts that pin is held by this backend */ if (!ConditionalLockBuffer(buf)) return false; if (!RelationUsesLocalBuffers(rel)) VALGRIND_MAKE_MEM_DEFINED(BufferGetPage(buf), BLCKSZ); return true; } /* * _bt_upgradelockbufcleanup() -- upgrade lock to a full cleanup lock. */ void _bt_upgradelockbufcleanup(Relation rel, Buffer buf) { /* * Buffer is pinned and locked, which means that it is expected to be * defined and addressable. Check that proactively. */ VALGRIND_CHECK_MEM_IS_DEFINED(BufferGetPage(buf), BLCKSZ); /* LockBuffer() asserts that pin is held by this backend */ LockBuffer(buf, BUFFER_LOCK_UNLOCK); LockBufferForCleanup(buf); } /* * _bt_pageinit() -- Initialize a new page. * * On return, the page header is initialized; data space is empty; * special space is zeroed out. */ void _bt_pageinit(Page page, Size size) { PageInit(page, size, sizeof(BTPageOpaqueData)); } /* * Delete item(s) from a btree leaf page during VACUUM. * * This routine assumes that the caller already has a full cleanup lock on * the buffer. Also, the given deletable and updatable arrays *must* be * sorted in ascending order. * * Routine deals with deleting TIDs when some (but not all) of the heap TIDs * in an existing posting list item are to be removed. This works by * updating/overwriting an existing item with caller's new version of the item * (a version that lacks the TIDs that are to be deleted). * * We record VACUUMs and b-tree deletes differently in WAL. Deletes must * generate their own snapshotConflictHorizon directly from the tableam, * whereas VACUUMs rely on the initial VACUUM table scan performing * WAL-logging that takes care of the issue for the table's indexes * indirectly. Also, we remove the VACUUM cycle ID from pages, which b-tree * deletes don't do. */ void _bt_delitems_vacuum(Relation rel, Buffer buf, OffsetNumber *deletable, int ndeletable, BTVacuumPosting *updatable, int nupdatable) { Page page = BufferGetPage(buf); BTPageOpaque opaque; bool needswal = RelationNeedsWAL(rel); char *updatedbuf = NULL; Size updatedbuflen = 0; OffsetNumber updatedoffsets[MaxIndexTuplesPerPage]; /* Shouldn't be called unless there's something to do */ Assert(ndeletable > 0 || nupdatable > 0); /* Generate new version of posting lists without deleted TIDs */ if (nupdatable > 0) updatedbuf = _bt_delitems_update(updatable, nupdatable, updatedoffsets, &updatedbuflen, needswal); /* No ereport(ERROR) until changes are logged */ START_CRIT_SECTION(); /* * Handle posting tuple updates. * * Deliberately do this before handling simple deletes. If we did it the * other way around (i.e. WAL record order -- simple deletes before * updates) then we'd have to make compensating changes to the 'updatable' * array of offset numbers. * * PageIndexTupleOverwrite() won't unset each item's LP_DEAD bit when it * happens to already be set. It's important that we not interfere with * any future simple index tuple deletion operations. */ for (int i = 0; i < nupdatable; i++) { OffsetNumber updatedoffset = updatedoffsets[i]; IndexTuple itup; Size itemsz; itup = updatable[i]->itup; itemsz = MAXALIGN(IndexTupleSize(itup)); if (!PageIndexTupleOverwrite(page, updatedoffset, (Item) itup, itemsz)) elog(PANIC, "failed to update partially dead item in block %u of index \"%s\"", BufferGetBlockNumber(buf), RelationGetRelationName(rel)); } /* Now handle simple deletes of entire tuples */ if (ndeletable > 0) PageIndexMultiDelete(page, deletable, ndeletable); /* * We can clear the vacuum cycle ID since this page has certainly been * processed by the current vacuum scan. */ opaque = BTPageGetOpaque(page); opaque->btpo_cycleid = 0; /* * Clear the BTP_HAS_GARBAGE page flag. * * This flag indicates the presence of LP_DEAD items on the page (though * not reliably). Note that we only rely on it with pg_upgrade'd * !heapkeyspace indexes. That's why clearing it here won't usually * interfere with simple index tuple deletion. */ opaque->btpo_flags &= ~BTP_HAS_GARBAGE; MarkBufferDirty(buf); /* XLOG stuff */ if (needswal) { XLogRecPtr recptr; xl_btree_vacuum xlrec_vacuum; xlrec_vacuum.ndeleted = ndeletable; xlrec_vacuum.nupdated = nupdatable; XLogBeginInsert(); XLogRegisterBuffer(0, buf, REGBUF_STANDARD); XLogRegisterData((char *) &xlrec_vacuum, SizeOfBtreeVacuum); if (ndeletable > 0) XLogRegisterBufData(0, (char *) deletable, ndeletable * sizeof(OffsetNumber)); if (nupdatable > 0) { XLogRegisterBufData(0, (char *) updatedoffsets, nupdatable * sizeof(OffsetNumber)); XLogRegisterBufData(0, updatedbuf, updatedbuflen); } recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_VACUUM); PageSetLSN(page, recptr); } END_CRIT_SECTION(); /* can't leak memory here */ if (updatedbuf != NULL) pfree(updatedbuf); /* free tuples allocated within _bt_delitems_update() */ for (int i = 0; i < nupdatable; i++) pfree(updatable[i]->itup); } /* * Delete item(s) from a btree leaf page during single-page cleanup. * * This routine assumes that the caller has pinned and write locked the * buffer. Also, the given deletable and updatable arrays *must* be sorted in * ascending order. * * Routine deals with deleting TIDs when some (but not all) of the heap TIDs * in an existing posting list item are to be removed. This works by * updating/overwriting an existing item with caller's new version of the item * (a version that lacks the TIDs that are to be deleted). * * This is nearly the same as _bt_delitems_vacuum as far as what it does to * the page, but it needs its own snapshotConflictHorizon (caller gets this * from tableam). This is used by the REDO routine to generate recovery * conflicts. The other difference is that only _bt_delitems_vacuum will * clear page's VACUUM cycle ID. */ static void _bt_delitems_delete(Relation rel, Relation heaprel, Buffer buf, TransactionId snapshotConflictHorizon, OffsetNumber *deletable, int ndeletable, BTVacuumPosting *updatable, int nupdatable) { Page page = BufferGetPage(buf); BTPageOpaque opaque; bool needswal = RelationNeedsWAL(rel); char *updatedbuf = NULL; Size updatedbuflen = 0; OffsetNumber updatedoffsets[MaxIndexTuplesPerPage]; /* Shouldn't be called unless there's something to do */ Assert(ndeletable > 0 || nupdatable > 0); /* Generate new versions of posting lists without deleted TIDs */ if (nupdatable > 0) updatedbuf = _bt_delitems_update(updatable, nupdatable, updatedoffsets, &updatedbuflen, needswal); /* No ereport(ERROR) until changes are logged */ START_CRIT_SECTION(); /* Handle updates and deletes just like _bt_delitems_vacuum */ for (int i = 0; i < nupdatable; i++) { OffsetNumber updatedoffset = updatedoffsets[i]; IndexTuple itup; Size itemsz; itup = updatable[i]->itup; itemsz = MAXALIGN(IndexTupleSize(itup)); if (!PageIndexTupleOverwrite(page, updatedoffset, (Item) itup, itemsz)) elog(PANIC, "failed to update partially dead item in block %u of index \"%s\"", BufferGetBlockNumber(buf), RelationGetRelationName(rel)); } if (ndeletable > 0) PageIndexMultiDelete(page, deletable, ndeletable); /* * Unlike _bt_delitems_vacuum, we *must not* clear the vacuum cycle ID at * this point. The VACUUM command alone controls vacuum cycle IDs. */ opaque = BTPageGetOpaque(page); /* * Clear the BTP_HAS_GARBAGE page flag. * * This flag indicates the presence of LP_DEAD items on the page (though * not reliably). Note that we only rely on it with pg_upgrade'd * !heapkeyspace indexes. */ opaque->btpo_flags &= ~BTP_HAS_GARBAGE; MarkBufferDirty(buf); /* XLOG stuff */ if (needswal) { XLogRecPtr recptr; xl_btree_delete xlrec_delete; xlrec_delete.isCatalogRel = RelationIsAccessibleInLogicalDecoding(heaprel); xlrec_delete.snapshotConflictHorizon = snapshotConflictHorizon; xlrec_delete.ndeleted = ndeletable; xlrec_delete.nupdated = nupdatable; XLogBeginInsert(); XLogRegisterBuffer(0, buf, REGBUF_STANDARD); XLogRegisterData((char *) &xlrec_delete, SizeOfBtreeDelete); if (ndeletable > 0) XLogRegisterBufData(0, (char *) deletable, ndeletable * sizeof(OffsetNumber)); if (nupdatable > 0) { XLogRegisterBufData(0, (char *) updatedoffsets, nupdatable * sizeof(OffsetNumber)); XLogRegisterBufData(0, updatedbuf, updatedbuflen); } recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_DELETE); PageSetLSN(page, recptr); } END_CRIT_SECTION(); /* can't leak memory here */ if (updatedbuf != NULL) pfree(updatedbuf); /* free tuples allocated within _bt_delitems_update() */ for (int i = 0; i < nupdatable; i++) pfree(updatable[i]->itup); } /* * Set up state needed to delete TIDs from posting list tuples via "updating" * the tuple. Performs steps common to both _bt_delitems_vacuum and * _bt_delitems_delete. These steps must take place before each function's * critical section begins. * * updatable and nupdatable are inputs, though note that we will use * _bt_update_posting() to replace the original itup with a pointer to a final * version in palloc()'d memory. Caller should free the tuples when its done. * * The first nupdatable entries from updatedoffsets are set to the page offset * number for posting list tuples that caller updates. This is mostly useful * because caller may need to WAL-log the page offsets (though we always do * this for caller out of convenience). * * Returns buffer consisting of an array of xl_btree_update structs that * describe the steps we perform here for caller (though only when needswal is * true). Also sets *updatedbuflen to the final size of the buffer. This * buffer is used by caller when WAL logging is required. */ static char * _bt_delitems_update(BTVacuumPosting *updatable, int nupdatable, OffsetNumber *updatedoffsets, Size *updatedbuflen, bool needswal) { char *updatedbuf = NULL; Size buflen = 0; /* Shouldn't be called unless there's something to do */ Assert(nupdatable > 0); for (int i = 0; i < nupdatable; i++) { BTVacuumPosting vacposting = updatable[i]; Size itemsz; /* Replace work area IndexTuple with updated version */ _bt_update_posting(vacposting); /* Keep track of size of xl_btree_update for updatedbuf in passing */ itemsz = SizeOfBtreeUpdate + vacposting->ndeletedtids * sizeof(uint16); buflen += itemsz; /* Build updatedoffsets buffer in passing */ updatedoffsets[i] = vacposting->updatedoffset; } /* XLOG stuff */ if (needswal) { Size offset = 0; /* Allocate, set final size for caller */ updatedbuf = palloc(buflen); *updatedbuflen = buflen; for (int i = 0; i < nupdatable; i++) { BTVacuumPosting vacposting = updatable[i]; Size itemsz; xl_btree_update update; update.ndeletedtids = vacposting->ndeletedtids; memcpy(updatedbuf + offset, &update.ndeletedtids, SizeOfBtreeUpdate); offset += SizeOfBtreeUpdate; itemsz = update.ndeletedtids * sizeof(uint16); memcpy(updatedbuf + offset, vacposting->deletetids, itemsz); offset += itemsz; } } return updatedbuf; } /* * Comparator used by _bt_delitems_delete_check() to restore deltids array * back to its original leaf-page-wise sort order */ static int _bt_delitems_cmp(const void *a, const void *b) { TM_IndexDelete *indexdelete1 = (TM_IndexDelete *) a; TM_IndexDelete *indexdelete2 = (TM_IndexDelete *) b; if (indexdelete1->id > indexdelete2->id) return 1; if (indexdelete1->id < indexdelete2->id) return -1; Assert(false); return 0; } /* * Try to delete item(s) from a btree leaf page during single-page cleanup. * * nbtree interface to table_index_delete_tuples(). Deletes a subset of index * tuples from caller's deltids array: those whose TIDs are found safe to * delete by the tableam (or already marked LP_DEAD in index, and so already * known to be deletable by our simple index deletion caller). We physically * delete index tuples from buf leaf page last of all (for index tuples where * that is known to be safe following our table_index_delete_tuples() call). * * Simple index deletion caller only includes TIDs from index tuples marked * LP_DEAD, as well as extra TIDs it found on the same leaf page that can be * included without increasing the total number of distinct table blocks for * the deletion operation as a whole. This approach often allows us to delete * some extra index tuples that were practically free for tableam to check in * passing (when they actually turn out to be safe to delete). It probably * only makes sense for the tableam to go ahead with these extra checks when * it is block-oriented (otherwise the checks probably won't be practically * free, which we rely on). The tableam interface requires the tableam side * to handle the problem, though, so this is okay (we as an index AM are free * to make the simplifying assumption that all tableams must be block-based). * * Bottom-up index deletion caller provides all the TIDs from the leaf page, * without expecting that tableam will check most of them. The tableam has * considerable discretion around which entries/blocks it checks. Our role in * costing the bottom-up deletion operation is strictly advisory. * * Note: Caller must have added deltids entries (i.e. entries that go in * delstate's main array) in leaf-page-wise order: page offset number order, * TID order among entries taken from the same posting list tuple (tiebreak on * TID). This order is convenient to work with here. * * Note: We also rely on the id field of each deltids element "capturing" this * original leaf-page-wise order. That is, we expect to be able to get back * to the original leaf-page-wise order just by sorting deltids on the id * field (tableam will sort deltids for its own reasons, so we'll need to put * it back in leaf-page-wise order afterwards). */ void _bt_delitems_delete_check(Relation rel, Buffer buf, Relation heapRel, TM_IndexDeleteOp *delstate) { Page page = BufferGetPage(buf); TransactionId snapshotConflictHorizon; OffsetNumber postingidxoffnum = InvalidOffsetNumber; int ndeletable = 0, nupdatable = 0; OffsetNumber deletable[MaxIndexTuplesPerPage]; BTVacuumPosting updatable[MaxIndexTuplesPerPage]; /* Use tableam interface to determine which tuples to delete first */ snapshotConflictHorizon = table_index_delete_tuples(heapRel, delstate); /* Should not WAL-log snapshotConflictHorizon unless it's required */ if (!XLogStandbyInfoActive()) snapshotConflictHorizon = InvalidTransactionId; /* * Construct a leaf-page-wise description of what _bt_delitems_delete() * needs to do to physically delete index tuples from the page. * * Must sort deltids array to restore leaf-page-wise order (original order * before call to tableam). This is the order that the loop expects. * * Note that deltids array might be a lot smaller now. It might even have * no entries at all (with bottom-up deletion caller), in which case there * is nothing left to do. */ qsort(delstate->deltids, delstate->ndeltids, sizeof(TM_IndexDelete), _bt_delitems_cmp); if (delstate->ndeltids == 0) { Assert(delstate->bottomup); return; } /* We definitely have to delete at least one index tuple (or one TID) */ for (int i = 0; i < delstate->ndeltids; i++) { TM_IndexStatus *dstatus = delstate->status + delstate->deltids[i].id; OffsetNumber idxoffnum = dstatus->idxoffnum; ItemId itemid = PageGetItemId(page, idxoffnum); IndexTuple itup = (IndexTuple) PageGetItem(page, itemid); int nestedi, nitem; BTVacuumPosting vacposting; Assert(OffsetNumberIsValid(idxoffnum)); if (idxoffnum == postingidxoffnum) { /* * This deltid entry is a TID from a posting list tuple that has * already been completely processed */ Assert(BTreeTupleIsPosting(itup)); Assert(ItemPointerCompare(BTreeTupleGetHeapTID(itup), &delstate->deltids[i].tid) < 0); Assert(ItemPointerCompare(BTreeTupleGetMaxHeapTID(itup), &delstate->deltids[i].tid) >= 0); continue; } if (!BTreeTupleIsPosting(itup)) { /* Plain non-pivot tuple */ Assert(ItemPointerEquals(&itup->t_tid, &delstate->deltids[i].tid)); if (dstatus->knowndeletable) deletable[ndeletable++] = idxoffnum; continue; } /* * itup is a posting list tuple whose lowest deltids entry (which may * or may not be for the first TID from itup) is considered here now. * We should process all of the deltids entries for the posting list * together now, though (not just the lowest). Remember to skip over * later itup-related entries during later iterations of outermost * loop. */ postingidxoffnum = idxoffnum; /* Remember work in outermost loop */ nestedi = i; /* Initialize for first itup deltids entry */ vacposting = NULL; /* Describes final action for itup */ nitem = BTreeTupleGetNPosting(itup); for (int p = 0; p < nitem; p++) { ItemPointer ptid = BTreeTupleGetPostingN(itup, p); int ptidcmp = -1; /* * This nested loop reuses work across ptid TIDs taken from itup. * We take advantage of the fact that both itup's TIDs and deltids * entries (within a single itup/posting list grouping) must both * be in ascending TID order. */ for (; nestedi < delstate->ndeltids; nestedi++) { TM_IndexDelete *tcdeltid = &delstate->deltids[nestedi]; TM_IndexStatus *tdstatus = (delstate->status + tcdeltid->id); /* Stop once we get past all itup related deltids entries */ Assert(tdstatus->idxoffnum >= idxoffnum); if (tdstatus->idxoffnum != idxoffnum) break; /* Skip past non-deletable itup related entries up front */ if (!tdstatus->knowndeletable) continue; /* Entry is first partial ptid match (or an exact match)? */ ptidcmp = ItemPointerCompare(&tcdeltid->tid, ptid); if (ptidcmp >= 0) { /* Greater than or equal (partial or exact) match... */ break; } } /* ...exact ptid match to a deletable deltids entry? */ if (ptidcmp != 0) continue; /* Exact match for deletable deltids entry -- ptid gets deleted */ if (vacposting == NULL) { vacposting = palloc(offsetof(BTVacuumPostingData, deletetids) + nitem * sizeof(uint16)); vacposting->itup = itup; vacposting->updatedoffset = idxoffnum; vacposting->ndeletedtids = 0; } vacposting->deletetids[vacposting->ndeletedtids++] = p; } /* Final decision on itup, a posting list tuple */ if (vacposting == NULL) { /* No TIDs to delete from itup -- do nothing */ } else if (vacposting->ndeletedtids == nitem) { /* Straight delete of itup (to delete all TIDs) */ deletable[ndeletable++] = idxoffnum; /* Turns out we won't need granular information */ pfree(vacposting); } else { /* Delete some (but not all) TIDs from itup */ Assert(vacposting->ndeletedtids > 0 && vacposting->ndeletedtids < nitem); updatable[nupdatable++] = vacposting; } } /* Physically delete tuples (or TIDs) using deletable (or updatable) */ _bt_delitems_delete(rel, heapRel, buf, snapshotConflictHorizon, deletable, ndeletable, updatable, nupdatable); /* be tidy */ for (int i = 0; i < nupdatable; i++) pfree(updatable[i]); } /* * Check that leftsib page (the btpo_prev of target page) is not marked with * INCOMPLETE_SPLIT flag. Used during page deletion. * * Returning true indicates that page flag is set in leftsib (which is * definitely still the left sibling of target). When that happens, the * target doesn't have a downlink in parent, and the page deletion algorithm * isn't prepared to handle that. Deletion of the target page (or the whole * subtree that contains the target page) cannot take place. * * Caller should not have a lock on the target page itself, since pages on the * same level must always be locked left to right to avoid deadlocks. */ static bool _bt_leftsib_splitflag(Relation rel, Relation heaprel, BlockNumber leftsib, BlockNumber target) { Buffer buf; Page page; BTPageOpaque opaque; bool result; /* Easy case: No left sibling */ if (leftsib == P_NONE) return false; buf = _bt_getbuf(rel, heaprel, leftsib, BT_READ); page = BufferGetPage(buf); opaque = BTPageGetOpaque(page); /* * If the left sibling was concurrently split, so that its next-pointer * doesn't point to the current page anymore, the split that created * target must be completed. Caller can reasonably expect that there will * be a downlink to the target page that it can relocate using its stack. * (We don't allow splitting an incompletely split page again until the * previous split has been completed.) */ result = (opaque->btpo_next == target && P_INCOMPLETE_SPLIT(opaque)); _bt_relbuf(rel, buf); return result; } /* * Check that leafrightsib page (the btpo_next of target leaf page) is not * marked with ISHALFDEAD flag. Used during page deletion. * * Returning true indicates that page flag is set in leafrightsib, so page * deletion cannot go ahead. Our caller is not prepared to deal with the case * where the parent page does not have a pivot tuples whose downlink points to * leafrightsib (due to an earlier interrupted VACUUM operation). It doesn't * seem worth going to the trouble of teaching our caller to deal with it. * The situation will be resolved after VACUUM finishes the deletion of the * half-dead page (when a future VACUUM operation reaches the target page * again). * * _bt_leftsib_splitflag() is called for both leaf pages and internal pages. * _bt_rightsib_halfdeadflag() is only called for leaf pages, though. This is * okay because of the restriction on deleting pages that are the rightmost * page of their parent (i.e. that such deletions can only take place when the * entire subtree must be deleted). The leaf level check made here will apply * to a right "cousin" leaf page rather than a simple right sibling leaf page * in cases where caller actually goes on to attempt deleting pages that are * above the leaf page. The right cousin leaf page is representative of the * left edge of the subtree to the right of the to-be-deleted subtree as a * whole, which is exactly the condition that our caller cares about. * (Besides, internal pages are never marked half-dead, so it isn't even * possible to _directly_ assess if an internal page is part of some other * to-be-deleted subtree.) */ static bool _bt_rightsib_halfdeadflag(Relation rel, Relation heaprel, BlockNumber leafrightsib) { Buffer buf; Page page; BTPageOpaque opaque; bool result; Assert(leafrightsib != P_NONE); buf = _bt_getbuf(rel, heaprel, leafrightsib, BT_READ); page = BufferGetPage(buf); opaque = BTPageGetOpaque(page); Assert(P_ISLEAF(opaque) && !P_ISDELETED(opaque)); result = P_ISHALFDEAD(opaque); _bt_relbuf(rel, buf); return result; } /* * _bt_pagedel() -- Delete a leaf page from the b-tree, if legal to do so. * * This action unlinks the leaf page from the b-tree structure, removing all * pointers leading to it --- but not touching its own left and right links. * The page cannot be physically reclaimed right away, since other processes * may currently be trying to follow links leading to the page; they have to * be allowed to use its right-link to recover. See nbtree/README. * * On entry, the target buffer must be pinned and locked (either read or write * lock is OK). The page must be an empty leaf page, which may be half-dead * already (a half-dead page should only be passed to us when an earlier * VACUUM operation was interrupted, though). Note in particular that caller * should never pass a buffer containing an existing deleted page here. The * lock and pin on caller's buffer will be dropped before we return. * * Maintains bulk delete stats for caller, which are taken from vstate. We * need to cooperate closely with caller here so that whole VACUUM operation * reliably avoids any double counting of subsidiary-to-leafbuf pages that we * delete in passing. If such pages happen to be from a block number that is * ahead of the current scanblkno position, then caller is expected to count * them directly later on. It's simpler for us to understand caller's * requirements than it would be for caller to understand when or how a * deleted page became deleted after the fact. * * NOTE: this leaks memory. Rather than trying to clean up everything * carefully, it's better to run it in a temp context that can be reset * frequently. */ void _bt_pagedel(Relation rel, Buffer leafbuf, BTVacState *vstate) { BlockNumber rightsib; bool rightsib_empty; Page page; BTPageOpaque opaque; /* * Save original leafbuf block number from caller. Only deleted blocks * that are <= scanblkno are added to bulk delete stat's pages_deleted * count. */ BlockNumber scanblkno = BufferGetBlockNumber(leafbuf); /* * "stack" is a search stack leading (approximately) to the target page. * It is initially NULL, but when iterating, we keep it to avoid * duplicated search effort. * * Also, when "stack" is not NULL, we have already checked that the * current page is not the right half of an incomplete split, i.e. the * left sibling does not have its INCOMPLETE_SPLIT flag set, including * when the current target page is to the right of caller's initial page * (the scanblkno page). */ BTStack stack = NULL; for (;;) { page = BufferGetPage(leafbuf); opaque = BTPageGetOpaque(page); /* * Internal pages are never deleted directly, only as part of deleting * the whole subtree all the way down to leaf level. * * Also check for deleted pages here. Caller never passes us a fully * deleted page. Only VACUUM can delete pages, so there can't have * been a concurrent deletion. Assume that we reached any deleted * page encountered here by following a sibling link, and that the * index is corrupt. */ Assert(!P_ISDELETED(opaque)); if (!P_ISLEAF(opaque) || P_ISDELETED(opaque)) { /* * Pre-9.4 page deletion only marked internal pages as half-dead, * but now we only use that flag on leaf pages. The old algorithm * was never supposed to leave half-dead pages in the tree, it was * just a transient state, but it was nevertheless possible in * error scenarios. We don't know how to deal with them here. They * are harmless as far as searches are considered, but inserts * into the deleted keyspace could add out-of-order downlinks in * the upper levels. Log a notice, hopefully the admin will notice * and reindex. */ if (P_ISHALFDEAD(opaque)) ereport(LOG, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index \"%s\" contains a half-dead internal page", RelationGetRelationName(rel)), errhint("This can be caused by an interrupted VACUUM in version 9.3 or older, before upgrade. Please REINDEX it."))); if (P_ISDELETED(opaque)) ereport(LOG, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg_internal("found deleted block %u while following right link from block %u in index \"%s\"", BufferGetBlockNumber(leafbuf), scanblkno, RelationGetRelationName(rel)))); _bt_relbuf(rel, leafbuf); return; } /* * We can never delete rightmost pages nor root pages. While at it, * check that page is empty, since it's possible that the leafbuf page * was empty a moment ago, but has since had some inserts. * * To keep the algorithm simple, we also never delete an incompletely * split page (they should be rare enough that this doesn't make any * meaningful difference to disk usage): * * The INCOMPLETE_SPLIT flag on the page tells us if the page is the * left half of an incomplete split, but ensuring that it's not the * right half is more complicated. For that, we have to check that * the left sibling doesn't have its INCOMPLETE_SPLIT flag set using * _bt_leftsib_splitflag(). On the first iteration, we temporarily * release the lock on scanblkno/leafbuf, check the left sibling, and * construct a search stack to scanblkno. On subsequent iterations, * we know we stepped right from a page that passed these tests, so * it's OK. */ if (P_RIGHTMOST(opaque) || P_ISROOT(opaque) || P_FIRSTDATAKEY(opaque) <= PageGetMaxOffsetNumber(page) || P_INCOMPLETE_SPLIT(opaque)) { /* Should never fail to delete a half-dead page */ Assert(!P_ISHALFDEAD(opaque)); _bt_relbuf(rel, leafbuf); return; } /* * First, remove downlink pointing to the page (or a parent of the * page, if we are going to delete a taller subtree), and mark the * leafbuf page half-dead */ if (!P_ISHALFDEAD(opaque)) { /* * We need an approximate pointer to the page's parent page. We * use a variant of the standard search mechanism to search for * the page's high key; this will give us a link to either the * current parent or someplace to its left (if there are multiple * equal high keys, which is possible with !heapkeyspace indexes). * * Also check if this is the right-half of an incomplete split * (see comment above). */ if (!stack) { BTScanInsert itup_key; ItemId itemid; IndexTuple targetkey; BlockNumber leftsib, leafblkno; Buffer sleafbuf; itemid = PageGetItemId(page, P_HIKEY); targetkey = CopyIndexTuple((IndexTuple) PageGetItem(page, itemid)); leftsib = opaque->btpo_prev; leafblkno = BufferGetBlockNumber(leafbuf); /* * To avoid deadlocks, we'd better drop the leaf page lock * before going further. */ _bt_unlockbuf(rel, leafbuf); /* * Check that the left sibling of leafbuf (if any) is not * marked with INCOMPLETE_SPLIT flag before proceeding */ Assert(leafblkno == scanblkno); if (_bt_leftsib_splitflag(rel, vstate->info->heaprel, leftsib, leafblkno)) { ReleaseBuffer(leafbuf); return; } /* we need an insertion scan key for the search, so build one */ itup_key = _bt_mkscankey(rel, vstate->info->heaprel, targetkey); /* find the leftmost leaf page with matching pivot/high key */ itup_key->pivotsearch = true; stack = _bt_search(rel, vstate->info->heaprel, itup_key, &sleafbuf, BT_READ, NULL); /* won't need a second lock or pin on leafbuf */ _bt_relbuf(rel, sleafbuf); /* * Re-lock the leaf page, and start over to use our stack * within _bt_mark_page_halfdead. We must do it that way * because it's possible that leafbuf can no longer be * deleted. We need to recheck. * * Note: We can't simply hold on to the sleafbuf lock instead, * because it's barely possible that sleafbuf is not the same * page as leafbuf. This happens when leafbuf split after our * original lock was dropped, but before _bt_search finished * its descent. We rely on the assumption that we'll find * leafbuf isn't safe to delete anymore in this scenario. * (Page deletion can cope with the stack being to the left of * leafbuf, but not to the right of leafbuf.) */ _bt_lockbuf(rel, leafbuf, BT_WRITE); continue; } /* * See if it's safe to delete the leaf page, and determine how * many parent/internal pages above the leaf level will be * deleted. If it's safe then _bt_mark_page_halfdead will also * perform the first phase of deletion, which includes marking the * leafbuf page half-dead. */ Assert(P_ISLEAF(opaque) && !P_IGNORE(opaque)); if (!_bt_mark_page_halfdead(rel, vstate->info->heaprel, leafbuf, stack)) { _bt_relbuf(rel, leafbuf); return; } } /* * Then unlink it from its siblings. Each call to * _bt_unlink_halfdead_page unlinks the topmost page from the subtree, * making it shallower. Iterate until the leafbuf page is deleted. */ rightsib_empty = false; Assert(P_ISLEAF(opaque) && P_ISHALFDEAD(opaque)); while (P_ISHALFDEAD(opaque)) { /* Check for interrupts in _bt_unlink_halfdead_page */ if (!_bt_unlink_halfdead_page(rel, leafbuf, scanblkno, &rightsib_empty, vstate)) { /* * _bt_unlink_halfdead_page should never fail, since we * established that deletion is generally safe in * _bt_mark_page_halfdead -- index must be corrupt. * * Note that _bt_unlink_halfdead_page already released the * lock and pin on leafbuf for us. */ Assert(false); return; } } Assert(P_ISLEAF(opaque) && P_ISDELETED(opaque)); rightsib = opaque->btpo_next; _bt_relbuf(rel, leafbuf); /* * Check here, as calling loops will have locks held, preventing * interrupts from being processed. */ CHECK_FOR_INTERRUPTS(); /* * The page has now been deleted. If its right sibling is completely * empty, it's possible that the reason we haven't deleted it earlier * is that it was the rightmost child of the parent. Now that we * removed the downlink for this page, the right sibling might now be * the only child of the parent, and could be removed. It would be * picked up by the next vacuum anyway, but might as well try to * remove it now, so loop back to process the right sibling. * * Note: This relies on the assumption that _bt_getstackbuf() will be * able to reuse our original descent stack with a different child * block (provided that the child block is to the right of the * original leaf page reached by _bt_search()). It will even update * the descent stack each time we loop around, avoiding repeated work. */ if (!rightsib_empty) break; leafbuf = _bt_getbuf(rel, vstate->info->heaprel, rightsib, BT_WRITE); } } /* * First stage of page deletion. * * Establish the height of the to-be-deleted subtree with leafbuf at its * lowest level, remove the downlink to the subtree, and mark leafbuf * half-dead. The final to-be-deleted subtree is usually just leafbuf itself, * but may include additional internal pages (at most one per level of the * tree below the root). * * Returns 'false' if leafbuf is unsafe to delete, usually because leafbuf is * the rightmost child of its parent (and parent has more than one downlink). * Returns 'true' when the first stage of page deletion completed * successfully. */ static bool _bt_mark_page_halfdead(Relation rel, Relation heaprel, Buffer leafbuf, BTStack stack) { BlockNumber leafblkno; BlockNumber leafrightsib; BlockNumber topparent; BlockNumber topparentrightsib; ItemId itemid; Page page; BTPageOpaque opaque; Buffer subtreeparent; OffsetNumber poffset; OffsetNumber nextoffset; IndexTuple itup; IndexTupleData trunctuple; page = BufferGetPage(leafbuf); opaque = BTPageGetOpaque(page); Assert(!P_RIGHTMOST(opaque) && !P_ISROOT(opaque) && P_ISLEAF(opaque) && !P_IGNORE(opaque) && P_FIRSTDATAKEY(opaque) > PageGetMaxOffsetNumber(page)); /* * Save info about the leaf page. */ leafblkno = BufferGetBlockNumber(leafbuf); leafrightsib = opaque->btpo_next; /* * Before attempting to lock the parent page, check that the right sibling * is not in half-dead state. A half-dead right sibling would have no * downlink in the parent, which would be highly confusing later when we * delete the downlink. It would fail the "right sibling of target page * is also the next child in parent page" cross-check below. */ if (_bt_rightsib_halfdeadflag(rel, heaprel, leafrightsib)) { elog(DEBUG1, "could not delete page %u because its right sibling %u is half-dead", leafblkno, leafrightsib); return false; } /* * We cannot delete a page that is the rightmost child of its immediate * parent, unless it is the only child --- in which case the parent has to * be deleted too, and the same condition applies recursively to it. We * have to check this condition all the way up before trying to delete, * and lock the parent of the root of the to-be-deleted subtree (the * "subtree parent"). _bt_lock_subtree_parent() locks the subtree parent * for us. We remove the downlink to the "top parent" page (subtree root * page) from the subtree parent page below. * * Initialize topparent to be leafbuf page now. The final to-be-deleted * subtree is often a degenerate one page subtree consisting only of the * leafbuf page. When that happens, the leafbuf page is the final subtree * root page/top parent page. */ topparent = leafblkno; topparentrightsib = leafrightsib; if (!_bt_lock_subtree_parent(rel, heaprel, leafblkno, stack, &subtreeparent, &poffset, &topparent, &topparentrightsib)) return false; /* * Check that the parent-page index items we're about to delete/overwrite * in subtree parent page contain what we expect. This can fail if the * index has become corrupt for some reason. We want to throw any error * before entering the critical section --- otherwise it'd be a PANIC. */ page = BufferGetPage(subtreeparent); opaque = BTPageGetOpaque(page); #ifdef USE_ASSERT_CHECKING /* * This is just an assertion because _bt_lock_subtree_parent should have * guaranteed tuple has the expected contents */ itemid = PageGetItemId(page, poffset); itup = (IndexTuple) PageGetItem(page, itemid); Assert(BTreeTupleGetDownLink(itup) == topparent); #endif nextoffset = OffsetNumberNext(poffset); itemid = PageGetItemId(page, nextoffset); itup = (IndexTuple) PageGetItem(page, itemid); if (BTreeTupleGetDownLink(itup) != topparentrightsib) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg_internal("right sibling %u of block %u is not next child %u of block %u in index \"%s\"", topparentrightsib, topparent, BTreeTupleGetDownLink(itup), BufferGetBlockNumber(subtreeparent), RelationGetRelationName(rel)))); /* * Any insert which would have gone on the leaf block will now go to its * right sibling. In other words, the key space moves right. */ PredicateLockPageCombine(rel, leafblkno, leafrightsib); /* No ereport(ERROR) until changes are logged */ START_CRIT_SECTION(); /* * Update parent of subtree. We want to delete the downlink to the top * parent page/root of the subtree, and the *following* key. Easiest way * is to copy the right sibling's downlink over the downlink that points * to top parent page, and then delete the right sibling's original pivot * tuple. * * Lanin and Shasha make the key space move left when deleting a page, * whereas the key space moves right here. That's why we cannot simply * delete the pivot tuple with the downlink to the top parent page. See * nbtree/README. */ page = BufferGetPage(subtreeparent); opaque = BTPageGetOpaque(page); itemid = PageGetItemId(page, poffset); itup = (IndexTuple) PageGetItem(page, itemid); BTreeTupleSetDownLink(itup, topparentrightsib); nextoffset = OffsetNumberNext(poffset); PageIndexTupleDelete(page, nextoffset); /* * Mark the leaf page as half-dead, and stamp it with a link to the top * parent page. When the leaf page is also the top parent page, the link * is set to InvalidBlockNumber. */ page = BufferGetPage(leafbuf); opaque = BTPageGetOpaque(page); opaque->btpo_flags |= BTP_HALF_DEAD; Assert(PageGetMaxOffsetNumber(page) == P_HIKEY); MemSet(&trunctuple, 0, sizeof(IndexTupleData)); trunctuple.t_info = sizeof(IndexTupleData); if (topparent != leafblkno) BTreeTupleSetTopParent(&trunctuple, topparent); else BTreeTupleSetTopParent(&trunctuple, InvalidBlockNumber); if (!PageIndexTupleOverwrite(page, P_HIKEY, (Item) &trunctuple, IndexTupleSize(&trunctuple))) elog(ERROR, "could not overwrite high key in half-dead page"); /* Must mark buffers dirty before XLogInsert */ MarkBufferDirty(subtreeparent); MarkBufferDirty(leafbuf); /* XLOG stuff */ if (RelationNeedsWAL(rel)) { xl_btree_mark_page_halfdead xlrec; XLogRecPtr recptr; xlrec.poffset = poffset; xlrec.leafblk = leafblkno; if (topparent != leafblkno) xlrec.topparent = topparent; else xlrec.topparent = InvalidBlockNumber; XLogBeginInsert(); XLogRegisterBuffer(0, leafbuf, REGBUF_WILL_INIT); XLogRegisterBuffer(1, subtreeparent, REGBUF_STANDARD); page = BufferGetPage(leafbuf); opaque = BTPageGetOpaque(page); xlrec.leftblk = opaque->btpo_prev; xlrec.rightblk = opaque->btpo_next; XLogRegisterData((char *) &xlrec, SizeOfBtreeMarkPageHalfDead); recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_MARK_PAGE_HALFDEAD); page = BufferGetPage(subtreeparent); PageSetLSN(page, recptr); page = BufferGetPage(leafbuf); PageSetLSN(page, recptr); } END_CRIT_SECTION(); _bt_relbuf(rel, subtreeparent); return true; } /* * Second stage of page deletion. * * Unlinks a single page (in the subtree undergoing deletion) from its * siblings. Also marks the page deleted. * * To get rid of the whole subtree, including the leaf page itself, call here * until the leaf page is deleted. The original "top parent" established in * the first stage of deletion is deleted in the first call here, while the * leaf page is deleted in the last call here. Note that the leaf page itself * is often the initial top parent page. * * Returns 'false' if the page could not be unlinked (shouldn't happen). If * the right sibling of the current target page is empty, *rightsib_empty is * set to true, allowing caller to delete the target's right sibling page in * passing. Note that *rightsib_empty is only actually used by caller when * target page is leafbuf, following last call here for leafbuf/the subtree * containing leafbuf. (We always set *rightsib_empty for caller, just to be * consistent.) * * Must hold pin and lock on leafbuf at entry (read or write doesn't matter). * On success exit, we'll be holding pin and write lock. On failure exit, * we'll release both pin and lock before returning (we define it that way * to avoid having to reacquire a lock we already released). */ static bool _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno, bool *rightsib_empty, BTVacState *vstate) { BlockNumber leafblkno = BufferGetBlockNumber(leafbuf); IndexBulkDeleteResult *stats = vstate->stats; BlockNumber leafleftsib; BlockNumber leafrightsib; BlockNumber target; BlockNumber leftsib; BlockNumber rightsib; Buffer lbuf = InvalidBuffer; Buffer buf; Buffer rbuf; Buffer metabuf = InvalidBuffer; Page metapg = NULL; BTMetaPageData *metad = NULL; ItemId itemid; Page page; BTPageOpaque opaque; FullTransactionId safexid; bool rightsib_is_rightmost; uint32 targetlevel; IndexTuple leafhikey; BlockNumber leaftopparent; page = BufferGetPage(leafbuf); opaque = BTPageGetOpaque(page); Assert(P_ISLEAF(opaque) && !P_ISDELETED(opaque) && P_ISHALFDEAD(opaque)); /* * Remember some information about the leaf page. */ itemid = PageGetItemId(page, P_HIKEY); leafhikey = (IndexTuple) PageGetItem(page, itemid); target = BTreeTupleGetTopParent(leafhikey); leafleftsib = opaque->btpo_prev; leafrightsib = opaque->btpo_next; _bt_unlockbuf(rel, leafbuf); /* * Check here, as calling loops will have locks held, preventing * interrupts from being processed. */ CHECK_FOR_INTERRUPTS(); /* Unlink the current top parent of the subtree */ if (!BlockNumberIsValid(target)) { /* Target is leaf page (or leaf page is top parent, if you prefer) */ target = leafblkno; buf = leafbuf; leftsib = leafleftsib; targetlevel = 0; } else { /* Target is the internal page taken from leaf's top parent link */ Assert(target != leafblkno); /* Fetch the block number of the target's left sibling */ buf = _bt_getbuf(rel, vstate->info->heaprel, target, BT_READ); page = BufferGetPage(buf); opaque = BTPageGetOpaque(page); leftsib = opaque->btpo_prev; targetlevel = opaque->btpo_level; Assert(targetlevel > 0); /* * To avoid deadlocks, we'd better drop the target page lock before * going further. */ _bt_unlockbuf(rel, buf); } /* * We have to lock the pages we need to modify in the standard order: * moving right, then up. Else we will deadlock against other writers. * * So, first lock the leaf page, if it's not the target. Then find and * write-lock the current left sibling of the target page. The sibling * that was current a moment ago could have split, so we may have to move * right. */ if (target != leafblkno) _bt_lockbuf(rel, leafbuf, BT_WRITE); if (leftsib != P_NONE) { lbuf = _bt_getbuf(rel, vstate->info->heaprel, leftsib, BT_WRITE); page = BufferGetPage(lbuf); opaque = BTPageGetOpaque(page); while (P_ISDELETED(opaque) || opaque->btpo_next != target) { bool leftsibvalid = true; /* * Before we follow the link from the page that was the left * sibling mere moments ago, validate its right link. This * reduces the opportunities for loop to fail to ever make any * progress in the presence of index corruption. * * Note: we rely on the assumption that there can only be one * vacuum process running at a time (against the same index). */ if (P_RIGHTMOST(opaque) || P_ISDELETED(opaque) || leftsib == opaque->btpo_next) leftsibvalid = false; leftsib = opaque->btpo_next; _bt_relbuf(rel, lbuf); if (!leftsibvalid) { if (target != leafblkno) { /* we have only a pin on target, but pin+lock on leafbuf */ ReleaseBuffer(buf); _bt_relbuf(rel, leafbuf); } else { /* we have only a pin on leafbuf */ ReleaseBuffer(leafbuf); } ereport(LOG, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg_internal("valid left sibling for deletion target could not be located: " "left sibling %u of target %u with leafblkno %u and scanblkno %u in index \"%s\"", leftsib, target, leafblkno, scanblkno, RelationGetRelationName(rel)))); return false; } CHECK_FOR_INTERRUPTS(); /* step right one page */ lbuf = _bt_getbuf(rel, vstate->info->heaprel, leftsib, BT_WRITE); page = BufferGetPage(lbuf); opaque = BTPageGetOpaque(page); } } else lbuf = InvalidBuffer; /* Next write-lock the target page itself */ _bt_lockbuf(rel, buf, BT_WRITE); page = BufferGetPage(buf); opaque = BTPageGetOpaque(page); /* * Check page is still empty etc, else abandon deletion. This is just for * paranoia's sake; a half-dead page cannot resurrect because there can be * only one vacuum process running at a time. */ if (P_RIGHTMOST(opaque) || P_ISROOT(opaque) || P_ISDELETED(opaque)) elog(ERROR, "target page changed status unexpectedly in block %u of index \"%s\"", target, RelationGetRelationName(rel)); if (opaque->btpo_prev != leftsib) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg_internal("target page left link unexpectedly changed from %u to %u in block %u of index \"%s\"", leftsib, opaque->btpo_prev, target, RelationGetRelationName(rel)))); if (target == leafblkno) { if (P_FIRSTDATAKEY(opaque) <= PageGetMaxOffsetNumber(page) || !P_ISLEAF(opaque) || !P_ISHALFDEAD(opaque)) elog(ERROR, "target leaf page changed status unexpectedly in block %u of index \"%s\"", target, RelationGetRelationName(rel)); /* Leaf page is also target page: don't set leaftopparent */ leaftopparent = InvalidBlockNumber; } else { IndexTuple finaldataitem; if (P_FIRSTDATAKEY(opaque) != PageGetMaxOffsetNumber(page) || P_ISLEAF(opaque)) elog(ERROR, "target internal page on level %u changed status unexpectedly in block %u of index \"%s\"", targetlevel, target, RelationGetRelationName(rel)); /* Target is internal: set leaftopparent for next call here... */ itemid = PageGetItemId(page, P_FIRSTDATAKEY(opaque)); finaldataitem = (IndexTuple) PageGetItem(page, itemid); leaftopparent = BTreeTupleGetDownLink(finaldataitem); /* ...except when it would be a redundant pointer-to-self */ if (leaftopparent == leafblkno) leaftopparent = InvalidBlockNumber; } /* No leaftopparent for level 0 (leaf page) or level 1 target */ Assert(!BlockNumberIsValid(leaftopparent) || targetlevel > 1); /* * And next write-lock the (current) right sibling. */ rightsib = opaque->btpo_next; rbuf = _bt_getbuf(rel, vstate->info->heaprel, rightsib, BT_WRITE); page = BufferGetPage(rbuf); opaque = BTPageGetOpaque(page); if (opaque->btpo_prev != target) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg_internal("right sibling's left-link doesn't match: " "block %u links to %u instead of expected %u in index \"%s\"", rightsib, opaque->btpo_prev, target, RelationGetRelationName(rel)))); rightsib_is_rightmost = P_RIGHTMOST(opaque); *rightsib_empty = (P_FIRSTDATAKEY(opaque) > PageGetMaxOffsetNumber(page)); /* * If we are deleting the next-to-last page on the target's level, then * the rightsib is a candidate to become the new fast root. (In theory, it * might be possible to push the fast root even further down, but the odds * of doing so are slim, and the locking considerations daunting.) * * We can safely acquire a lock on the metapage here --- see comments for * _bt_newroot(). */ if (leftsib == P_NONE && rightsib_is_rightmost) { page = BufferGetPage(rbuf); opaque = BTPageGetOpaque(page); if (P_RIGHTMOST(opaque)) { /* rightsib will be the only one left on the level */ metabuf = _bt_getbuf(rel, vstate->info->heaprel, BTREE_METAPAGE, BT_WRITE); metapg = BufferGetPage(metabuf); metad = BTPageGetMeta(metapg); /* * The expected case here is btm_fastlevel == targetlevel+1; if * the fastlevel is <= targetlevel, something is wrong, and we * choose to overwrite it to fix it. */ if (metad->btm_fastlevel > targetlevel + 1) { /* no update wanted */ _bt_relbuf(rel, metabuf); metabuf = InvalidBuffer; } } } /* * Here we begin doing the deletion. */ /* No ereport(ERROR) until changes are logged */ START_CRIT_SECTION(); /* * Update siblings' side-links. Note the target page's side-links will * continue to point to the siblings. Asserts here are just rechecking * things we already verified above. */ if (BufferIsValid(lbuf)) { page = BufferGetPage(lbuf); opaque = BTPageGetOpaque(page); Assert(opaque->btpo_next == target); opaque->btpo_next = rightsib; } page = BufferGetPage(rbuf); opaque = BTPageGetOpaque(page); Assert(opaque->btpo_prev == target); opaque->btpo_prev = leftsib; /* * If we deleted a parent of the targeted leaf page, instead of the leaf * itself, update the leaf to point to the next remaining child in the * subtree. * * Note: We rely on the fact that a buffer pin on the leaf page has been * held since leafhikey was initialized. This is safe, though only * because the page was already half-dead at that point. The leaf page * cannot have been modified by any other backend during the period when * no lock was held. */ if (target != leafblkno) BTreeTupleSetTopParent(leafhikey, leaftopparent); /* * Mark the page itself deleted. It can be recycled when all current * transactions are gone. Storing GetTopTransactionId() would work, but * we're in VACUUM and would not otherwise have an XID. Having already * updated links to the target, ReadNextFullTransactionId() suffices as an * upper bound. Any scan having retained a now-stale link is advertising * in its PGPROC an xmin less than or equal to the value we read here. It * will continue to do so, holding back the xmin horizon, for the duration * of that scan. */ page = BufferGetPage(buf); opaque = BTPageGetOpaque(page); Assert(P_ISHALFDEAD(opaque) || !P_ISLEAF(opaque)); /* * Store upper bound XID that's used to determine when deleted page is no * longer needed as a tombstone */ safexid = ReadNextFullTransactionId(); BTPageSetDeleted(page, safexid); opaque->btpo_cycleid = 0; /* And update the metapage, if needed */ if (BufferIsValid(metabuf)) { /* upgrade metapage if needed */ if (metad->btm_version < BTREE_NOVAC_VERSION) _bt_upgrademetapage(metapg); metad->btm_fastroot = rightsib; metad->btm_fastlevel = targetlevel; MarkBufferDirty(metabuf); } /* Must mark buffers dirty before XLogInsert */ MarkBufferDirty(rbuf); MarkBufferDirty(buf); if (BufferIsValid(lbuf)) MarkBufferDirty(lbuf); if (target != leafblkno) MarkBufferDirty(leafbuf); /* XLOG stuff */ if (RelationNeedsWAL(rel)) { xl_btree_unlink_page xlrec; xl_btree_metadata xlmeta; uint8 xlinfo; XLogRecPtr recptr; XLogBeginInsert(); XLogRegisterBuffer(0, buf, REGBUF_WILL_INIT); if (BufferIsValid(lbuf)) XLogRegisterBuffer(1, lbuf, REGBUF_STANDARD); XLogRegisterBuffer(2, rbuf, REGBUF_STANDARD); if (target != leafblkno) XLogRegisterBuffer(3, leafbuf, REGBUF_WILL_INIT); /* information stored on the target/to-be-unlinked block */ xlrec.leftsib = leftsib; xlrec.rightsib = rightsib; xlrec.level = targetlevel; xlrec.safexid = safexid; /* information needed to recreate the leaf block (if not the target) */ xlrec.leafleftsib = leafleftsib; xlrec.leafrightsib = leafrightsib; xlrec.leaftopparent = leaftopparent; XLogRegisterData((char *) &xlrec, SizeOfBtreeUnlinkPage); if (BufferIsValid(metabuf)) { XLogRegisterBuffer(4, metabuf, REGBUF_WILL_INIT | REGBUF_STANDARD); Assert(metad->btm_version >= BTREE_NOVAC_VERSION); xlmeta.version = metad->btm_version; xlmeta.root = metad->btm_root; xlmeta.level = metad->btm_level; xlmeta.fastroot = metad->btm_fastroot; xlmeta.fastlevel = metad->btm_fastlevel; xlmeta.last_cleanup_num_delpages = metad->btm_last_cleanup_num_delpages; xlmeta.allequalimage = metad->btm_allequalimage; XLogRegisterBufData(4, (char *) &xlmeta, sizeof(xl_btree_metadata)); xlinfo = XLOG_BTREE_UNLINK_PAGE_META; } else xlinfo = XLOG_BTREE_UNLINK_PAGE; recptr = XLogInsert(RM_BTREE_ID, xlinfo); if (BufferIsValid(metabuf)) { PageSetLSN(metapg, recptr); } page = BufferGetPage(rbuf); PageSetLSN(page, recptr); page = BufferGetPage(buf); PageSetLSN(page, recptr); if (BufferIsValid(lbuf)) { page = BufferGetPage(lbuf); PageSetLSN(page, recptr); } if (target != leafblkno) { page = BufferGetPage(leafbuf); PageSetLSN(page, recptr); } } END_CRIT_SECTION(); /* release metapage */ if (BufferIsValid(metabuf)) _bt_relbuf(rel, metabuf); /* release siblings */ if (BufferIsValid(lbuf)) _bt_relbuf(rel, lbuf); _bt_relbuf(rel, rbuf); /* If the target is not leafbuf, we're done with it now -- release it */ if (target != leafblkno) _bt_relbuf(rel, buf); /* * Maintain pages_newly_deleted, which is simply the number of pages * deleted by the ongoing VACUUM operation. * * Maintain pages_deleted in a way that takes into account how * btvacuumpage() will count deleted pages that have yet to become * scanblkno -- only count page when it's not going to get that treatment * later on. */ stats->pages_newly_deleted++; if (target <= scanblkno) stats->pages_deleted++; /* * Remember information about the target page (now a newly deleted page) * in dedicated vstate space for later. The page will be considered as a * candidate to place in the FSM at the end of the current btvacuumscan() * call. */ _bt_pendingfsm_add(vstate, target, safexid); return true; } /* * Establish how tall the to-be-deleted subtree will be during the first stage * of page deletion. * * Caller's child argument is the block number of the page caller wants to * delete (this is leafbuf's block number, except when we're called * recursively). stack is a search stack leading to it. Note that we will * update the stack entry(s) to reflect current downlink positions --- this is * similar to the corresponding point in page split handling. * * If "first stage" caller cannot go ahead with deleting _any_ pages, returns * false. Returns true on success, in which case caller can use certain * details established here to perform the first stage of deletion. This * function is the last point at which page deletion may be deemed unsafe * (barring index corruption, or unexpected concurrent page deletions). * * We write lock the parent of the root of the to-be-deleted subtree for * caller on success (i.e. we leave our lock on the *subtreeparent buffer for * caller). Caller will have to remove a downlink from *subtreeparent. We * also set a *subtreeparent offset number in *poffset, to indicate the * location of the pivot tuple that contains the relevant downlink. * * The root of the to-be-deleted subtree is called the "top parent". Note * that the leafbuf page is often the final "top parent" page (you can think * of the leafbuf page as a degenerate single page subtree when that happens). * Caller should initialize *topparent to the target leafbuf page block number * (while *topparentrightsib should be set to leafbuf's right sibling block * number). We will update *topparent (and *topparentrightsib) for caller * here, though only when it turns out that caller will delete at least one * internal page (i.e. only when caller needs to store a valid link to the top * parent block in the leafbuf page using BTreeTupleSetTopParent()). */ static bool _bt_lock_subtree_parent(Relation rel, Relation heaprel, BlockNumber child, BTStack stack, Buffer *subtreeparent, OffsetNumber *poffset, BlockNumber *topparent, BlockNumber *topparentrightsib) { BlockNumber parent, leftsibparent; OffsetNumber parentoffset, maxoff; Buffer pbuf; Page page; BTPageOpaque opaque; /* * Locate the pivot tuple whose downlink points to "child". Write lock * the parent page itself. */ pbuf = _bt_getstackbuf(rel, heaprel, stack, child); if (pbuf == InvalidBuffer) { /* * Failed to "re-find" a pivot tuple whose downlink matched our child * block number on the parent level -- the index must be corrupt. * Don't even try to delete the leafbuf subtree. Just report the * issue and press on with vacuuming the index. * * Note: _bt_getstackbuf() recovers from concurrent page splits that * take place on the parent level. Its approach is a near-exhaustive * linear search. This also gives it a surprisingly good chance of * recovering in the event of a buggy or inconsistent opclass. But we * don't rely on that here. */ ereport(LOG, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg_internal("failed to re-find parent key in index \"%s\" for deletion target page %u", RelationGetRelationName(rel), child))); return false; } parent = stack->bts_blkno; parentoffset = stack->bts_offset; page = BufferGetPage(pbuf); opaque = BTPageGetOpaque(page); maxoff = PageGetMaxOffsetNumber(page); leftsibparent = opaque->btpo_prev; /* * _bt_getstackbuf() completes page splits on returned parent buffer when * required. * * In general it's a bad idea for VACUUM to use up more disk space, which * is why page deletion does not finish incomplete page splits most of the * time. We allow this limited exception because the risk is much lower, * and the potential downside of not proceeding is much higher: A single * internal page with the INCOMPLETE_SPLIT flag set might otherwise * prevent us from deleting hundreds of empty leaf pages from one level * down. */ Assert(!P_INCOMPLETE_SPLIT(opaque)); if (parentoffset < maxoff) { /* * Child is not the rightmost child in parent, so it's safe to delete * the subtree whose root/topparent is child page */ *subtreeparent = pbuf; *poffset = parentoffset; return true; } /* * Child is the rightmost child of parent. * * Since it's the rightmost child of parent, deleting the child (or * deleting the subtree whose root/topparent is the child page) is only * safe when it's also possible to delete the parent. */ Assert(parentoffset == maxoff); if (parentoffset != P_FIRSTDATAKEY(opaque) || P_RIGHTMOST(opaque)) { /* * Child isn't parent's only child, or parent is rightmost on its * entire level. Definitely cannot delete any pages. */ _bt_relbuf(rel, pbuf); return false; } /* * Now make sure that the parent deletion is itself safe by examining the * child's grandparent page. Recurse, passing the parent page as the * child page (child's grandparent is the parent on the next level up). If * parent deletion is unsafe, then child deletion must also be unsafe (in * which case caller cannot delete any pages at all). */ *topparent = parent; *topparentrightsib = opaque->btpo_next; /* * Release lock on parent before recursing. * * It's OK to release page locks on parent before recursive call locks * grandparent. An internal page can only acquire an entry if the child * is split, but that cannot happen as long as we still hold a lock on the * leafbuf page. */ _bt_relbuf(rel, pbuf); /* * Before recursing, check that the left sibling of parent (if any) is not * marked with INCOMPLETE_SPLIT flag first (must do so after we drop the * parent lock). * * Note: We deliberately avoid completing incomplete splits here. */ if (_bt_leftsib_splitflag(rel, heaprel, leftsibparent, parent)) return false; /* Recurse to examine child page's grandparent page */ return _bt_lock_subtree_parent(rel, heaprel, parent, stack->bts_parent, subtreeparent, poffset, topparent, topparentrightsib); } /* * Initialize local memory state used by VACUUM for _bt_pendingfsm_finalize * optimization. * * Called at the start of a btvacuumscan(). Caller's cleanuponly argument * indicates if ongoing VACUUM has not (and will not) call btbulkdelete(). * * We expect to allocate memory inside VACUUM's top-level memory context here. * The working buffer is subject to a limit based on work_mem. Our strategy * when the array can no longer grow within the bounds of that limit is to * stop saving additional newly deleted pages, while proceeding as usual with * the pages that we can fit. */ void _bt_pendingfsm_init(Relation rel, BTVacState *vstate, bool cleanuponly) { int64 maxbufsize; /* * Don't bother with optimization in cleanup-only case -- we don't expect * any newly deleted pages. Besides, cleanup-only calls to btvacuumscan() * can only take place because this optimization didn't work out during * the last VACUUM. */ if (cleanuponly) return; /* * Cap maximum size of array so that we always respect work_mem. Avoid * int overflow here. */ vstate->bufsize = 256; maxbufsize = (work_mem * 1024L) / sizeof(BTPendingFSM); maxbufsize = Min(maxbufsize, INT_MAX); maxbufsize = Min(maxbufsize, MaxAllocSize / sizeof(BTPendingFSM)); /* Stay sane with small work_mem */ maxbufsize = Max(maxbufsize, vstate->bufsize); vstate->maxbufsize = maxbufsize; /* Allocate buffer, indicate that there are currently 0 pending pages */ vstate->pendingpages = palloc(sizeof(BTPendingFSM) * vstate->bufsize); vstate->npendingpages = 0; } /* * Place any newly deleted pages (i.e. pages that _bt_pagedel() deleted during * the ongoing VACUUM operation) into the free space map -- though only when * it is actually safe to do so by now. * * Called at the end of a btvacuumscan(), just before free space map vacuuming * takes place. * * Frees memory allocated by _bt_pendingfsm_init(), if any. */ void _bt_pendingfsm_finalize(Relation rel, BTVacState *vstate) { IndexBulkDeleteResult *stats = vstate->stats; Relation heaprel = vstate->info->heaprel; Assert(stats->pages_newly_deleted >= vstate->npendingpages); if (vstate->npendingpages == 0) { /* Just free memory when nothing to do */ if (vstate->pendingpages) pfree(vstate->pendingpages); return; } #ifdef DEBUG_BTREE_PENDING_FSM /* * Debugging aid: Sleep for 5 seconds to greatly increase the chances of * placing pending pages in the FSM. Note that the optimization will * never be effective without some other backend concurrently consuming an * XID. */ pg_usleep(5000000L); #endif /* * Recompute VACUUM XID boundaries. * * We don't actually care about the oldest non-removable XID. Computing * the oldest such XID has a useful side-effect that we rely on: it * forcibly updates the XID horizon state for this backend. This step is * essential; GlobalVisCheckRemovableFullXid() will not reliably recognize * that it is now safe to recycle newly deleted pages without this step. */ GetOldestNonRemovableTransactionId(heaprel); for (int i = 0; i < vstate->npendingpages; i++) { BlockNumber target = vstate->pendingpages[i].target; FullTransactionId safexid = vstate->pendingpages[i].safexid; /* * Do the equivalent of checking BTPageIsRecyclable(), but without * accessing the page again a second time. * * Give up on finding the first non-recyclable page -- all later pages * must be non-recyclable too, since _bt_pendingfsm_add() adds pages * to the array in safexid order. */ if (!GlobalVisCheckRemovableFullXid(heaprel, safexid)) break; RecordFreeIndexPage(rel, target); stats->pages_free++; } pfree(vstate->pendingpages); } /* * Maintain array of pages that were deleted during current btvacuumscan() * call, for use in _bt_pendingfsm_finalize() */ static void _bt_pendingfsm_add(BTVacState *vstate, BlockNumber target, FullTransactionId safexid) { Assert(vstate->npendingpages <= vstate->bufsize); Assert(vstate->bufsize <= vstate->maxbufsize); #ifdef USE_ASSERT_CHECKING /* * Verify an assumption made by _bt_pendingfsm_finalize(): pages from the * array will always be in safexid order (since that is the order that we * save them in here) */ if (vstate->npendingpages > 0) { FullTransactionId lastsafexid = vstate->pendingpages[vstate->npendingpages - 1].safexid; Assert(FullTransactionIdFollowsOrEquals(safexid, lastsafexid)); } #endif /* * If temp buffer reaches maxbufsize/work_mem capacity then we discard * information about this page. * * Note that this also covers the case where we opted to not use the * optimization in _bt_pendingfsm_init(). */ if (vstate->npendingpages == vstate->maxbufsize) return; /* Consider enlarging buffer */ if (vstate->npendingpages == vstate->bufsize) { int newbufsize = vstate->bufsize * 2; /* Respect work_mem */ if (newbufsize > vstate->maxbufsize) newbufsize = vstate->maxbufsize; vstate->bufsize = newbufsize; vstate->pendingpages = repalloc(vstate->pendingpages, sizeof(BTPendingFSM) * vstate->bufsize); } /* Save metadata for newly deleted page */ vstate->pendingpages[vstate->npendingpages].target = target; vstate->pendingpages[vstate->npendingpages].safexid = safexid; vstate->npendingpages++; }