From b1e24be9ad9c50387d45c700461c1a585eaa5b74 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Mon, 2 Nov 2026 22:12:02 -0500 Subject: [PATCH 3/6] 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 a0ab2528167..e24fbe6f4ff 100644 --- a/src/backend/access/hash/hashpage.c +++ b/src/backend/access/hash/hashpage.c @@ -1025,6 +1025,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. + */ + PageSetChecksum(page, lastblock); + smgrextend(RelationGetSmgr(rel), MAIN_FORKNUM, lastblock, zerobuf.data, + false); + if (RelationNeedsWAL(rel)) log_newpage(&rel->rd_locator, MAIN_FORKNUM, @@ -1032,10 +1048,6 @@ _hash_alloc_buckets(Relation rel, BlockNumber firstblock, uint32 nblocks) zerobuf.data, true); - PageSetChecksum(page, lastblock); - smgrextend(RelationGetSmgr(rel), MAIN_FORKNUM, lastblock, zerobuf.data, - false); - return true; } -- 2.37.1 (Apple Git-137.1)