From 6e3eca0e1327de6a768cc094f2b277b53ad17784 Mon Sep 17 00:00:00 2001 From: Andrey Borodin Date: Sat, 22 Aug 2026 11:23:12 +0300 Subject: [PATCH v2 3/3] Use full XIDs in deleted GIN posting-tree pages GIN stores page deletion horizons as 32-bit XIDs that are not protected against wraparound. Once such a horizon is interpreted as belonging to the next epoch, an otherwise reusable page can remain unrecyclable for about two billion transactions. Store a FullTransactionId in newly deleted posting-tree pages and in the associated WAL record. Mark the new on-page representation with a flag, so indexes upgraded from older releases retain the conservative behavior for old deleted pages until those pages are reused. This changes the GIN delete-page WAL record and requires an XLOG_PAGE_MAGIC bump. Discussion: https://postgr.es/m/CAH2-Wz=sKJcn+OtfVN9rdg+Ps9e4cuQWNP-9t12UE2d8nEG90Q@mail.gmail.com --- contrib/pageinspect/ginfuncs.c | 5 ++++- src/backend/access/gin/ginutil.c | 29 ++++++++++++++++++----------- src/backend/access/gin/ginvacuum.c | 15 +++++++-------- src/backend/access/gin/ginxlog.c | 7 ++++--- src/include/access/ginblock.h | 26 +++++++++++++++++++++++++- src/include/access/ginxlog.h | 2 +- 6 files changed, 59 insertions(+), 25 deletions(-) diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c index 058ad52b671..c4f0e3c0697 100644 --- a/contrib/pageinspect/ginfuncs.c +++ b/contrib/pageinspect/ginfuncs.c @@ -148,8 +148,11 @@ gin_page_opaque_info(PG_FUNCTION_ARGS) flags[nflags++] = CStringGetTextDatum("incomplete_split"); if (flagbits & GIN_COMPRESSED) flags[nflags++] = CStringGetTextDatum("compressed"); + if (flagbits & GIN_DELETED_FULL_XID) + flags[nflags++] = CStringGetTextDatum("deleted_full_xid"); flagbits &= ~(GIN_DATA | GIN_LEAF | GIN_DELETED | GIN_META | GIN_LIST | - GIN_LIST_FULLROW | GIN_INCOMPLETE_SPLIT | GIN_COMPRESSED); + GIN_LIST_FULLROW | GIN_INCOMPLETE_SPLIT | GIN_COMPRESSED | + GIN_DELETED_FULL_XID); if (flagbits) { /* any flags we don't recognize are printed in hex */ diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c index 434efe78de6..1e4f00a5cbc 100644 --- a/src/backend/access/gin/ginutil.c +++ b/src/backend/access/gin/ginutil.c @@ -332,21 +332,28 @@ GinNewBuffer(Relation index) Page page = BufferGetPage(buffer); if (XLogStandbyInfoActive() && RelationNeedsWAL(index) && - GinPageGetDeleteXid(page) != InvalidTransactionId) + (GinPageHasFullDeleteXid(page) || + TransactionIdIsValid(GinPageGetDeleteXid(page)))) { - FullTransactionId nextfxid = ReadNextFullTransactionId(); FullTransactionId deletefxid; Relation heaprel; - /* - * Legacy GIN pages only store the low 32 bits of the - * deletion XID. Interpret those as the latest occurrence - * not later than nextFullXid. This can give a conservative - * result for a page that survived an entire XID epoch, but - * avoids weakening recovery conflict handling. - */ - deletefxid = FullTransactionIdFromAllowableAt( - nextfxid, GinPageGetDeleteXid(page)); + if (GinPageHasFullDeleteXid(page)) + deletefxid = GinPageGetDeleteFullXid(page); + else + { + FullTransactionId nextfxid = ReadNextFullTransactionId(); + + /* + * Legacy GIN pages only store the low 32 bits of the + * deletion XID. Interpret those as the latest occurrence + * not later than nextFullXid. This can give a conservative + * result for a page that survived an entire XID epoch, but + * avoids weakening recovery conflict handling. + */ + deletefxid = FullTransactionIdFromAllowableAt( + nextfxid, GinPageGetDeleteXid(page)); + } heaprel = table_open(index->rd_index->indrelid, NoLock); diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c index 7a1a1da7fb2..f7ed75c62f8 100644 --- a/src/backend/access/gin/ginvacuum.c +++ b/src/backend/access/gin/ginvacuum.c @@ -204,8 +204,7 @@ ginDeletePostingPage(GinVacuumState *gvs, Buffer dBuffer, Buffer lBuffer, * Mark page as deleted, and remember last xid which could know its * address. */ - GinPageSetDeleted(page); - GinPageSetDeleteXid(page, ReadNextTransactionId()); + GinPageSetDeleteFullXid(page, ReadNextFullTransactionId()); MarkBufferDirty(pBuffer); MarkBufferDirty(lBuffer); @@ -231,7 +230,7 @@ ginDeletePostingPage(GinVacuumState *gvs, Buffer dBuffer, Buffer lBuffer, data.parentOffset = myoff; data.rightLink = GinPageGetOpaque(page)->rightlink; - data.deleteXid = GinPageGetDeleteXid(page); + data.deleteXid = GinPageGetDeleteFullXid(page); XLogRegisterData(&data, sizeof(ginxlogDeletePage)); @@ -896,22 +895,22 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) bool GinPageIsRecyclable(Page page) { - TransactionId delete_xid; - if (PageIsNew(page)) return true; if (!GinPageIsDeleted(page)) return false; - delete_xid = GinPageGetDeleteXid(page); + if (GinPageHasFullDeleteXid(page)) + return GlobalVisCheckRemovableFullXid(NULL, + GinPageGetDeleteFullXid(page)); - if (!TransactionIdIsValid(delete_xid)) + if (!TransactionIdIsValid(GinPageGetDeleteXid(page))) return true; /* * If no backend still could view delete_xid as in running, all scans * concurrent with ginDeletePostingPage() must have finished. */ - return GlobalVisCheckRemovableXid(NULL, delete_xid); + return GlobalVisCheckRemovableXid(NULL, GinPageGetDeleteXid(page)); } diff --git a/src/backend/access/gin/ginxlog.c b/src/backend/access/gin/ginxlog.c index b1fee3c281f..a353a6e235c 100644 --- a/src/backend/access/gin/ginxlog.c +++ b/src/backend/access/gin/ginxlog.c @@ -499,8 +499,7 @@ ginRedoDeletePage(XLogReaderState *record) { page = BufferGetPage(dbuffer); Assert(GinPageIsData(page)); - GinPageSetDeleted(page); - GinPageSetDeleteXid(page, data->deleteXid); + GinPageSetDeleteFullXid(page, data->deleteXid); PageSetLSN(page, lsn); MarkBufferDirty(dbuffer); } @@ -804,7 +803,9 @@ gin_mask(char *pagedata, BlockNumber blkno) * the whole page content. For other pages, mask the hole if pd_lower * appears to have been set correctly. */ - if (opaque->flags & GIN_DELETED) + if (opaque->flags & GIN_DELETED_FULL_XID) + mask_unused_space(page); + else if (opaque->flags & GIN_DELETED) mask_page_content(page); else if (pagehdr->pd_lower > SizeOfPageHeaderData) mask_unused_space(page); diff --git a/src/include/access/ginblock.h b/src/include/access/ginblock.h index 2d75023179a..c15324037b1 100644 --- a/src/include/access/ginblock.h +++ b/src/include/access/ginblock.h @@ -47,6 +47,7 @@ typedef GinPageOpaqueData *GinPageOpaque; #define GIN_INCOMPLETE_SPLIT (1 << 6) /* page was split, but parent not * updated */ #define GIN_COMPRESSED (1 << 7) +#define GIN_DELETED_FULL_XID (1 << 8) /* deleted page stores a full XID */ /* Page numbers of fixed-location pages */ #define GIN_METAPAGE_BLKNO (0) @@ -126,6 +127,7 @@ typedef struct GinMetaPageData #define GinPageSetDeleted(page) ( GinPageGetOpaque(page)->flags |= GIN_DELETED) #define GinPageSetNonDeleted(page) ( GinPageGetOpaque(page)->flags &= ~GIN_DELETED) #define GinPageIsIncompleteSplit(page) ( (GinPageGetOpaque(page)->flags & GIN_INCOMPLETE_SPLIT) != 0 ) +#define GinPageHasFullDeleteXid(page) ( (GinPageGetOpaque(page)->flags & GIN_DELETED_FULL_XID) != 0 ) #define GinPageRightMost(page) ( GinPageGetOpaque(page)->rightlink == InvalidBlockNumber) @@ -134,7 +136,29 @@ typedef struct GinMetaPageData * its deletion is over. */ #define GinPageGetDeleteXid(page) ( ((PageHeader) (page))->pd_prune_xid ) -#define GinPageSetDeleteXid(page, xid) ( ((PageHeader) (page))->pd_prune_xid = xid) + +typedef struct GINDeletedPageContents +{ + FullTransactionId deleteXid; +} GINDeletedPageContents; + +static inline FullTransactionId +GinPageGetDeleteFullXid(Page page) +{ + Assert(GinPageIsDeleted(page)); + Assert(GinPageHasFullDeleteXid(page)); + + return ((GINDeletedPageContents *) PageGetContents(page))->deleteXid; +} + +static inline void +GinPageSetDeleteFullXid(Page page, FullTransactionId deleteXid) +{ + GinPageGetOpaque(page)->flags |= GIN_DELETED | GIN_DELETED_FULL_XID; + ((PageHeader) page)->pd_lower = + MAXALIGN(SizeOfPageHeaderData) + sizeof(GINDeletedPageContents); + ((GINDeletedPageContents *) PageGetContents(page))->deleteXid = deleteXid; +} extern bool GinPageIsRecyclable(Page page); /* diff --git a/src/include/access/ginxlog.h b/src/include/access/ginxlog.h index 74fdf9b3d7e..65bd457d96f 100644 --- a/src/include/access/ginxlog.h +++ b/src/include/access/ginxlog.h @@ -156,7 +156,7 @@ typedef struct ginxlogDeletePage { OffsetNumber parentOffset; BlockNumber rightLink; - TransactionId deleteXid; /* last Xid which could see this page in scan */ + FullTransactionId deleteXid; /* last XID that could see the page */ } ginxlogDeletePage; #define XLOG_GIN_UPDATE_META_PAGE 0x60 -- That's all, folks. May the source be with you.