Prevent GIN deleted pages from being reclaimed too early
authorAlexander Korotkov
Thu, 13 Dec 2018 03:12:31 +0000 (06:12 +0300)
committerAlexander Korotkov
Thu, 13 Dec 2018 03:52:26 +0000 (06:52 +0300)
When GIN vacuum deletes a posting tree page, it assumes that no concurrent
searchers can access it, thanks to ginStepRight() locking two pages at once.
However, since 9.4 searches can skip parts of posting trees descending from the
root.  That leads to the risk that page is deleted and reclaimed before
concurrent search can access it.

This commit prevents the risk of above by waiting for every transaction, which
might wait to reference this page, to finish.  Due to binary compatibility
we can't change GinPageOpaqueData to store corresponding transaction id.
Instead we reuse page header pd_prune_xid field, which is unused in index pages.

Discussion: https://postgr.es/m/31a702a.14dd.166c1366ac1.Coremail.chjischj%40163.com
Author: Andrey Borodin, Alexander Korotkov
Reviewed-by: Alexander Korotkov
Backpatch-through: 9.4

src/backend/access/gin/README
src/backend/access/gin/ginutil.c
src/backend/access/gin/ginvacuum.c
src/include/access/gin_private.h

index fade0cbb617103c52000afa8f1402b09467febc6..d551df1166c27c1cef548f493aaf87da3eaab74e 100644 (file)
@@ -304,12 +304,10 @@ the lock on next page has been acquired.
 The downlink is more tricky. A search descending the tree must release the
 lock on the parent page before locking the child, or it could deadlock with
 a concurrent split of the child page; a page split locks the parent, while
-already holding a lock on the child page. However, posting trees are only
-fully searched from left to right, starting from the leftmost leaf. (The
-tree-structure is only needed by insertions, to quickly find the correct
-insert location). So as long as we don't delete the leftmost page on each
-level, a search can never follow a downlink to page that's about to be
-deleted.
+already holding a lock on the child page. So, deleted page cannot be reclaimed
+immediately. Instead, we have to wait for every transaction, which might wait
+to reference this page, to finish. Corresponding processes must observe that
+the page is marked deleted and recover accordingly.
 
 The previous paragraph's reasoning only applies to searches, and only to
 posting trees. To protect from inserters following a downlink to a deleted
index 3ca0b68434bc6da94dd590d74c5e65276e0fda01..40f11de9d70fdd81d8922c0727ea754dee44a1cd 100644 (file)
@@ -227,12 +227,7 @@ GinNewBuffer(Relation index)
         */
        if (ConditionalLockBuffer(buffer))
        {
-           Page        page = BufferGetPage(buffer);
-
-           if (PageIsNew(page))
-               return buffer;  /* OK to use, if never initialized */
-
-           if (GinPageIsDeleted(page))
+           if (GinPageIsRecyclable(BufferGetPage(buffer)))
                return buffer;  /* OK to use */
 
            LockBuffer(buffer, GIN_UNLOCK);
index cc440d93547396ebde95941ff2b5f52a18eef749..aed60cb7ae58bc80e12acfc97d01a991c675da35 100644 (file)
@@ -261,6 +261,9 @@ ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkn
    page = BufferGetPage(dBuffer);
    rightlink = GinPageGetOpaque(page)->rightlink;
 
+   /* For deleted page remember last xid which could knew its address */
+   GinPageSetDeleteXid(page, ReadNewTransactionId());
+
    page = BufferGetPage(lBuffer);
    GinPageGetOpaque(page)->rightlink = rightlink;
 
@@ -300,6 +303,7 @@ ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkn
        data.parentOffset = myoff;
        data.leftBlkno = leftBlkno;
        data.rightLink = GinPageGetOpaque(page)->rightlink;
+       data.deleteXid = GinPageGetDeleteXid(page);
 
        /*
         * We can't pass buffer_std = TRUE, because we didn't set pd_lower on
@@ -777,7 +781,7 @@ ginvacuumcleanup(PG_FUNCTION_ARGS)
        LockBuffer(buffer, GIN_SHARE);
        page = (Page) BufferGetPage(buffer);
 
-       if (PageIsNew(page) || GinPageIsDeleted(page))
+       if (GinPageIsRecyclable(page))
        {
            Assert(blkno != GIN_ROOT_BLKNO);
            RecordFreeIndexPage(index, blkno);
index e042f5ca1fd604847320bb9f1105936f7cba2f18..6b1ada5d006338b95beff6819cb1ab53a586bf0f 100644 (file)
 #include "access/genam.h"
 #include "access/gin.h"
 #include "access/itup.h"
+#include "access/transam.h"
 #include "fmgr.h"
 #include "storage/bufmgr.h"
 #include "utils/rbtree.h"
+#include "utils/snapmgr.h"
 
 
 /*
@@ -131,6 +133,15 @@ typedef struct GinMetaPageData
 
 #define GinPageRightMost(page) ( GinPageGetOpaque(page)->rightlink == InvalidBlockNumber)
 
+/*
+ * We should reclaim deleted page only once every transaction started before
+ * its deletion is over.
+ */
+#define GinPageGetDeleteXid(page) ( ((PageHeader) (page))->pd_prune_xid )
+#define GinPageSetDeleteXid(page, xid) ( ((PageHeader) (page))->pd_prune_xid = xid)
+#define GinPageIsRecyclable(page) ( PageIsNew(page) || (GinPageIsDeleted(page) \
+   && TransactionIdPrecedes(GinPageGetDeleteXid(page), RecentGlobalDataXmin)))
+
 /*
  * We use our own ItemPointerGet(BlockNumber|OffsetNumber)
  * to avoid Asserts, since sometimes the ip_posid isn't "valid"
@@ -572,6 +583,7 @@ typedef struct ginxlogDeletePage
    OffsetNumber parentOffset;
    BlockNumber leftBlkno;
    BlockNumber rightLink;
+   TransactionId deleteXid;    /* last Xid which could see this page in scan */
 } ginxlogDeletePage;
 
 #define XLOG_GIN_UPDATE_META_PAGE 0x60