From cc80ad12f7415bdcaa7cb57b775c117e7d141ada Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Fri, 30 Oct 2026 13:22:41 +0530 Subject: [PATCH v3_REL18 3/3] Hash: keep the WAL of the previous changes replayable across minor versions The previous two commits stop registering the primary bucket page in XLOG_HASH_DELETE, XLOG_HASH_MOVE_PAGE_CONTENTS and XLOG_HASH_SQUEEZE_PAGE records unless the record changes it, and append its block number to the record instead. In a back branch, WAL generated by earlier minor versions must still be replayable, and so must WAL generated by this one on a standby that has not been upgraded. Replay: accept both forms. A record that registers the primary bucket page as block 0, as written by older minor versions, is replayed through that block, as before; so is a squeeze record that registers the write page, when it is the primary bucket page, as an unmodified block 1. Otherwise the page is found through the appended block number. Write: older minor versions cannot replay a record without block 0 (they fail with "failed to locate backup block with ID 0 in WAL record"), so the primary bucket page is still registered as before, and its block number not appended, guarded by HASH_WAL_REGISTER_BUCKET_PAGE, until it is acceptable to stop doing so. The macro must be 1 while standbys of older minor versions are supported, in which case the records are the same as those of the older minor versions, and may be 0 once all servers replaying the WAL have this change. The CRC problem under wal_consistency_checking does not depend on this: the registered block is REGBUF_NO_CHANGE, for which no image is taken. XLOG_PAGE_MAGIC is not changed. --- src/backend/access/hash/README | 6 ++-- src/backend/access/hash/hash.c | 15 +++++++-- src/backend/access/hash/hash_xlog.c | 26 +++++++++++---- src/backend/access/hash/hashovfl.c | 39 ++++++++++++++++++++-- src/include/access/hash_xlog.h | 50 ++++++++++++++++++++++------- 5 files changed, 111 insertions(+), 25 deletions(-) diff --git a/src/backend/access/hash/README b/src/backend/access/hash/README index 7774e1d2fe0..eef866d9b22 100644 --- a/src/backend/access/hash/README +++ b/src/backend/access/hash/README @@ -647,8 +647,10 @@ that no scan on a hot standby is in progress in the bucket or can start in it while tuples are removed or moved; such a scan could otherwise miss tuples or return them twice. When the primary bucket page is not one of the pages the record changes, the operation holds only a pin on it, so it -is not registered in the record. The record then carries its block number -instead, and replay locks that block directly. +is not required in the record. The record then carries its block number +instead, and replay locks that block directly. Records written by older +minor versions register the page instead, and replay accepts both forms. +See HASH_WAL_REGISTER_BUCKET_PAGE in hash_xlog.h. Other Notes diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c index 0aa60619269..261487a7fb0 100644 --- a/src/backend/access/hash/hash.c +++ b/src/backend/access/hash/hash.c @@ -832,11 +832,22 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf, /* * If buf is an overflow page, the primary bucket page is not * changed and we hold only a pin on it, so it is not - * registered. Append its block number instead, and replay - * takes a cleanup lock on it. It is not needed otherwise. + * registered, unless older standbys must be supported. Append + * its block number instead, and replay takes a cleanup lock on + * it. It is not needed otherwise. */ +#if HASH_WAL_REGISTER_BUCKET_PAGE + /* for standbys that predate the block number, see hash_xlog.h */ + if (!xlrec.is_primary_bucket_page) + { + uint8 flags = REGBUF_STANDARD | REGBUF_NO_IMAGE | REGBUF_NO_CHANGE; + + XLogRegisterBuffer(0, bucket_buf, flags); + } +#else if (!xlrec.is_primary_bucket_page) XLogRegisterData(&bucket_blkno, sizeof(BlockNumber)); +#endif XLogRegisterBuffer(1, buf, REGBUF_STANDARD); XLogRegisterBufData(1, deletable, diff --git a/src/backend/access/hash/hash_xlog.c b/src/backend/access/hash/hash_xlog.c index e2edfe1afc0..511b1dcdd46 100644 --- a/src/backend/access/hash/hash_xlog.c +++ b/src/backend/access/hash/hash_xlog.c @@ -473,16 +473,20 @@ hash_xlog_split_complete(XLogReaderState *record) /* * Take a cleanup lock on the primary bucket page of the bucket that a - * squeeze or delete record operates on, when that page is not one of the - * pages the record changes and so is not registered in it. Its block number - * is stored in the main data of the record, right after the fixed-size struct - * of rec_size bytes. + * move-page-contents, squeeze or delete record operates on, when that page is + * not one of the pages the record changes. * * This serializes replay against scans on a hot standby, which pin the * primary bucket page for their whole duration; see the comments in the * callers. The page is not modified, so nothing is done with it besides - * locking it. block_id must be a block of the same index registered in - * the record. + * locking it. + * + * Records written by older minor versions, and by this one while + * HASH_WAL_REGISTER_BUCKET_PAGE is set, register the primary bucket page as + * block 0; for those, the lock is taken through block 0. Otherwise the + * block number of the page is stored in the main data of the record, right + * after the fixed-size struct of rec_size bytes, and block_id must be a block + * of the same index registered in the record. */ static Buffer hash_xlog_lock_bucket(XLogReaderState *record, uint8 block_id, Size rec_size) @@ -492,7 +496,15 @@ hash_xlog_lock_bucket(XLogReaderState *record, uint8 block_id, Size rec_size) BlockNumber bucket_blkno; Buffer buf; - Assert(XLogRecGetDataLen(record) == rec_size + sizeof(BlockNumber)); + if (XLogRecHasBlockRef(record, 0)) + { + /* primary bucket page registered, as older minor versions do */ + (void) XLogReadBufferForRedoExtended(record, 0, RBM_NORMAL, true, &buf); + return buf; + } + + if (XLogRecGetDataLen(record) < rec_size + sizeof(BlockNumber)) + elog(PANIC, "hash WAL record has neither the primary bucket page nor its block number"); memcpy(&bucket_blkno, XLogRecGetData(record) + rec_size, sizeof(BlockNumber)); XLogRecGetBlockTag(record, block_id, &rlocator, &forknum, NULL); diff --git a/src/backend/access/hash/hashovfl.c b/src/backend/access/hash/hashovfl.c index cbd299acc08..a8ba27957c7 100644 --- a/src/backend/access/hash/hashovfl.c +++ b/src/backend/access/hash/hashovfl.c @@ -645,7 +645,9 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf, if (RelationNeedsWAL(rel)) { xl_hash_squeeze_page xlrec; +#if !HASH_WAL_REGISTER_BUCKET_PAGE BlockNumber bucket_blkno = BufferGetBlockNumber(bucketbuf); +#endif XLogRecPtr recptr; int i; bool mod_wbuf; @@ -666,10 +668,20 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf, * Register only the pages this record changes. The primary bucket * page is registered only if it is the write page and is modified; * otherwise we may hold only a pin on it, so append its block number - * and replay takes a cleanup lock on it. + * and replay takes a cleanup lock on it, unless older standbys must + * be supported, see hash_xlog.h. */ +#if HASH_WAL_REGISTER_BUCKET_PAGE + if (!xlrec.is_prim_bucket_same_wrt) + { + uint8 flags = REGBUF_STANDARD | REGBUF_NO_IMAGE | REGBUF_NO_CHANGE; + + XLogRegisterBuffer(0, bucketbuf, flags); + } +#else if (!(xlrec.is_prim_bucket_same_wrt && mod_wbuf)) XLogRegisterData(&bucket_blkno, sizeof(BlockNumber)); +#endif if (mod_wbuf) { @@ -682,6 +694,16 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf, XLogRegisterBufData(1, itups[i], tups_size[i]); } } +#if HASH_WAL_REGISTER_BUCKET_PAGE + else if (xlrec.is_prim_bucket_same_wrt) + { + /* + * The primary page is the write page and nothing moves onto it. + * Older minor versions expect it as block 1, to lock it. + */ + XLogRegisterBuffer(1, wbuf, REGBUF_STANDARD | REGBUF_NO_CHANGE); + } +#endif XLogRegisterBuffer(2, ovflbuf, REGBUF_STANDARD); @@ -975,11 +997,22 @@ readpage: /* * If wbuf is an overflow page, the primary bucket * page is not changed and we hold only a pin on it, - * so it is not registered. Append its block number - * instead, and replay takes a cleanup lock on it. + * so it is not registered, unless older standbys must + * be supported. Append its block number instead, and + * replay takes a cleanup lock on it. */ +#if HASH_WAL_REGISTER_BUCKET_PAGE + /* for standbys that predate the block number */ + if (!xlrec.is_prim_bucket_same_wrt) + { + int flags = REGBUF_STANDARD | REGBUF_NO_IMAGE | REGBUF_NO_CHANGE; + + XLogRegisterBuffer(0, bucket_buf, flags); + } +#else if (!xlrec.is_prim_bucket_same_wrt) XLogRegisterData(&bucket_blkno, sizeof(BlockNumber)); +#endif XLogRegisterBuffer(1, wbuf, REGBUF_STANDARD); XLogRegisterBufData(1, itup_offsets, diff --git a/src/include/access/hash_xlog.h b/src/include/access/hash_xlog.h index 731bff5fb28..0512841d4c5 100644 --- a/src/include/access/hash_xlog.h +++ b/src/include/access/hash_xlog.h @@ -21,6 +21,30 @@ /* Number of buffers required for XLOG_HASH_SQUEEZE_PAGE operation */ #define HASH_XLOG_FREE_OVFL_BUFS 6 +/* + * Compatibility with other minor versions. + * + * Records of XLOG_HASH_DELETE, XLOG_HASH_MOVE_PAGE_CONTENTS and + * XLOG_HASH_SQUEEZE_PAGE written by minor versions before the one that + * introduced the appended block number of the primary bucket page register + * that page as a block reference, only so that replay can take a cleanup lock + * on it: as block 0 if it is not the page the record changes, and, for + * XLOG_HASH_SQUEEZE_PAGE, as an unmodified block 1 if it is the write page + * and nothing is moved onto it. Replay accepts both forms: if the record + * registers the page, that block is used, otherwise the appended block number + * is. + * + * A minor version without that replay code cannot replay a record that does + * not register the page; it fails with "failed to locate backup block with ID + * 0 in WAL record". While standbys running such versions must be supported, + * keep writing the block reference, by leaving this set to 1; the records are + * then the same as those of the older minor versions. Setting it to 0 stops + * registering the page and appends its block number instead, and requires that + * all servers replaying this WAL are at least at the version that introduced + * it. + */ +#define HASH_WAL_REGISTER_BUCKET_PAGE 1 + /* * XLOG records for hash operations */ @@ -132,10 +156,11 @@ typedef struct xl_hash_split_complete * Backup Blk 1: page containing moved tuples * Backup Blk 2: page from which tuples will be removed * - * The primary bucket page is not registered. If it is not the page moved to - * (is_prim_bucket_same_wrt is false), the block number of the primary bucket - * page follows the struct as a BlockNumber, and replay takes a cleanup lock - * on that block. + * If it is not the page moved to (is_prim_bucket_same_wrt is false), the + * primary bucket page is either registered as block 0, as long as + * HASH_WAL_REGISTER_BUCKET_PAGE is set and in records written by older minor + * versions, or else its block number follows the struct as a BlockNumber. + * Replay takes a cleanup lock on that page. */ typedef struct xl_hash_move_page_contents { @@ -161,10 +186,12 @@ typedef struct xl_hash_move_page_contents * Backup Blk 5: bitmap page containing info of freed overflow page * Backup Blk 6: meta page * - * The primary bucket page is registered only as block 1, when it is the - * modified write page. Otherwise the block number of the primary bucket page - * follows the struct as a BlockNumber, and replay takes a cleanup lock on that - * block. + * The primary bucket page is registered as block 1 when it is the write page + * and is modified. Otherwise it is registered as block 0 (or, if it is the + * write page, as an unmodified block 1), as long as + * HASH_WAL_REGISTER_BUCKET_PAGE is set and in records written by older minor + * versions, or else its block number follows the struct as a BlockNumber. + * Replay takes a cleanup lock on that page. */ typedef struct xl_hash_squeeze_page { @@ -191,9 +218,10 @@ typedef struct xl_hash_squeeze_page * Backup Blk 1: page from which tuples are deleted * * If block 1 is not the primary bucket page (is_primary_bucket_page is - * false), the block number of the primary bucket page follows the struct as a - * BlockNumber, and replay takes a cleanup lock on that block. The primary - * bucket page is not registered. + * false), the primary bucket page is either registered as block 0, as long as + * HASH_WAL_REGISTER_BUCKET_PAGE is set and in records written by older minor + * versions, or else its block number follows the struct as a BlockNumber. + * Replay takes a cleanup lock on that page. */ typedef struct xl_hash_delete { -- 2.55.0