From 67bb1166e22ec3358657e04c3c008b0b716c05f5 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Mon, 2 Nov 2026 22:12:02 -0500 Subject: [PATCH REL_16-18] Extend hash index before WAL-logging new bucket pages _hash_alloc_buckets() WAL-logs the last block of a new splitpoint with log_newpage(), and then extends the file with smgrextend(). Nothing kept a checkpoint from setting its redo pointer between the two steps. Recovery from such a checkpoint does not replay the log_newpage() record, and redo of the later records only extends the file up to the bucket pages that are actually used. If the extension did not make it into the checkpoint, the file ends up shorter than the metapage says. This can happen if the fsync request of smgrextend() arrives after the checkpoint has absorbed its requests, followed by an OS crash. It can also happen with a base backup whose start checkpoint lands in the window, if the index file is copied before the extension. Inserts then fail with "access to noncontiguous page in hash index" once an overflow page is needed past the end of the file, until the index is rebuilt. Fix by doing the two steps in the other order. If a checkpoint sets its redo pointer before the WAL record, recovery replays the record. If it sets it after, the extension and its fsync request came first. The checkpoint then syncs the new block, and a base backup sees it. Nothing uses the new page yet. So it is fine to write it before its WAL record, as heap extension does. A crash in between leaves the file longer than the metapage says, which the hash README allows. Backpatch-through: 14 --- src/backend/access/hash/hashpage.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/backend/access/hash/hashpage.c b/src/backend/access/hash/hashpage.c index 3b0c7bb89ec..907dfafcc02 100644 --- a/src/backend/access/hash/hashpage.c +++ b/src/backend/access/hash/hashpage.c @@ -1023,6 +1023,22 @@ _hash_alloc_buckets(Relation rel, BlockNumber firstblock, uint32 nblocks) ovflopaque->hasho_flag = LH_UNUSED_PAGE; ovflopaque->hasho_page_id = HASHO_PAGE_ID; + /* + * Extend the file before writing the WAL record. If a checkpoint sets + * its redo pointer before the record, recovery replays the record and + * extends the file again. If it sets it after the record, the extension + * is already done, and smgrextend() has already registered the fsync + * request. The checkpoint then syncs the new block, and a base backup + * started by it sees the block. So we must not skip the fsync here. + * + * Nothing uses the new page yet, so it is fine to write it before its WAL + * record. If we crash in between, the file is longer than the metapage + * says, which is allowed; see the README. + */ + PageSetChecksumInplace(page, lastblock); + smgrextend(RelationGetSmgr(rel), MAIN_FORKNUM, lastblock, zerobuf.data, + false); + if (RelationNeedsWAL(rel)) log_newpage(&rel->rd_locator, MAIN_FORKNUM, @@ -1030,10 +1046,6 @@ _hash_alloc_buckets(Relation rel, BlockNumber firstblock, uint32 nblocks) zerobuf.data, true); - PageSetChecksumInplace(page, lastblock); - smgrextend(RelationGetSmgr(rel), MAIN_FORKNUM, lastblock, zerobuf.data, - false); - return true; } -- 2.37.1 (Apple Git-137.1)