On Fri, Jul 10, 2026 at 3:06 PM Melanie Plageman <[email protected]> wrote: > > For 0003, I am much less confident. I've never written test > infrastructure in Perl before. I used AI to help me write it, so I > could really use review from someone experienced with adding TAP test > utility functions. A few thoughts on it/questions:
I could still use review on 0003. I hadn't checked CI before posting v4, and it caught a bug. Attached v5 (based on REL_18_STABLE) fixes that bug. In heap_update(), when the two heap pages' VM bits were on the same VM page and I was only clearing the old VM bits, I was registering the VM buffer with the wrong label. This made me wonder if I should change the logic in heap_update() for setting vmbuffer_[new_]modified to handle the "same VM page and both bits clear" case separately. It duplicates code but it could be clearer. I am on the fence. I included a file heap_update.diff that shows what that would look like. I've also finished all branch versions of this. Each one's code differs, but I've managed to keep the commit messages the same, which should help release notes. Note that REL_19_STABLE bumps XLOG_PAGE_MAGIC. This would keep us from having to maintain the backwards compatibility code there, which I think is what we want. Here are those branches. - Melanie 17: https://github.com/melanieplageman/postgres/tree/17_vm-clear-logging 18: https://github.com/melanieplageman/postgres/tree/18_vm-clear-logging 19: https://github.com/melanieplageman/postgres/tree/19_vm-clear-logging 20 (master): https://github.com/melanieplageman/postgres/tree/20_vm-clear-logging
From 52df426a99b428a818fd135282328496fb56dd61 Mon Sep 17 00:00:00 2001 From: Melanie Plageman <[email protected]> Date: Mon, 4 May 2026 10:41:13 -0400 Subject: [PATCH v5 1/4] Introduce macros for WAL block reference IDs of some heap record types When registering a buffer with the WAL machinery, the caller assigns it a block reference ID, and replay must read each block back by that same ID. Today these IDs are bare integers assigned by convention (0, 1, 2, ...), which is easy to follow when a record registers a single block, or when the blocks are handled during replay in their registration order. An upcoming bug fix registers up to two visibility map blocks in addition to the heap block(s) when clearing the VM, and these are not handled during replay in a straightforward 1:1, in-registration-order fashion. Relying on bare integers for the block IDs in that case is error-prone. Introduce macros naming the block reference IDs for the heap record types that the upcoming commit extends to register visibility map blocks, so the registration and replay sites refer to the same block by a meaningful name. Author: Melanie Plageman <[email protected]> Reviewed-by: Robert Haas <[email protected]> Discussion: https://postgr.es/m/66mqpfyti3qhfttcsv6r2lbvqqd32rrmpn6i47ovrsnvguts46%40gou54xc> Backpatch through: 17 --- src/backend/access/heap/heapam.c | 43 +++++++++++--------- src/backend/access/heap/heapam_xlog.c | 56 +++++++++++++++++---------- src/include/access/heapam_xlog.h | 28 ++++++++++---- 3 files changed, 82 insertions(+), 45 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index aff02cb003e..cf37956a2bf 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -2218,10 +2218,12 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, * write the whole page to the xlog, we don't need to store * xl_heap_header in the xlog. */ - XLogRegisterBuffer(0, buffer, REGBUF_STANDARD | bufflags); - XLogRegisterBufData(0, &xlhdr, SizeOfHeapHeader); + XLogRegisterBuffer(HEAP_INSERT_BLKREF_HEAP, buffer, + REGBUF_STANDARD | bufflags); + XLogRegisterBufData(HEAP_INSERT_BLKREF_HEAP, &xlhdr, + SizeOfHeapHeader); /* PG73FORMAT: write bitmap [+ padding] [+ oid] + data */ - XLogRegisterBufData(0, + XLogRegisterBufData(HEAP_INSERT_BLKREF_HEAP, (char *) heaptup->t_data + SizeofHeapTupleHeader, heaptup->t_len - SizeofHeapTupleHeader); @@ -2619,9 +2621,11 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, XLogBeginInsert(); XLogRegisterData(xlrec, tupledata - scratch.data); - XLogRegisterBuffer(0, buffer, REGBUF_STANDARD | bufflags); + XLogRegisterBuffer(HEAP_MULTI_INSERT_BLKREF_HEAP, buffer, + REGBUF_STANDARD | bufflags); - XLogRegisterBufData(0, tupledata, totaldatalen); + XLogRegisterBufData(HEAP_MULTI_INSERT_BLKREF_HEAP, tupledata, + totaldatalen); /* filtering by origin on a row level is much more efficient */ XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN); @@ -3112,7 +3116,7 @@ heap_delete(Relation relation, ItemPointer tid, XLogBeginInsert(); XLogRegisterData(&xlrec, SizeOfHeapDelete); - XLogRegisterBuffer(0, buffer, REGBUF_STANDARD); + XLogRegisterBuffer(HEAP_DELETE_BLKREF_HEAP, buffer, REGBUF_STANDARD); /* * Log replica identity of the deleted tuple if there is one @@ -3883,7 +3887,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, XLogRecPtr recptr; XLogBeginInsert(); - XLogRegisterBuffer(0, buffer, REGBUF_STANDARD); + XLogRegisterBuffer(HEAP_LOCK_BLKREF_HEAP, buffer, REGBUF_STANDARD); xlrec.offnum = ItemPointerGetOffsetNumber(&oldtup.t_self); xlrec.xmax = xmax_lock_old_tuple; @@ -5219,7 +5223,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, XLogRecPtr recptr; XLogBeginInsert(); - XLogRegisterBuffer(0, *buffer, REGBUF_STANDARD); + XLogRegisterBuffer(HEAP_LOCK_BLKREF_HEAP, *buffer, REGBUF_STANDARD); xlrec.offnum = ItemPointerGetOffsetNumber(&tuple->t_self); xlrec.xmax = xid; @@ -5971,7 +5975,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, Page page = BufferGetPage(buf); XLogBeginInsert(); - XLogRegisterBuffer(0, buf, REGBUF_STANDARD); + XLogRegisterBuffer(HEAP_LOCK_BLKREF_HEAP, buf, REGBUF_STANDARD); xlrec.offnum = ItemPointerGetOffsetNumber(&mytup.t_self); xlrec.xmax = new_xmax; @@ -8972,9 +8976,9 @@ log_heap_update(Relation reln, Buffer oldbuf, if (need_tuple_data) bufflags |= REGBUF_KEEP_DATA; - XLogRegisterBuffer(0, newbuf, bufflags); + XLogRegisterBuffer(HEAP_UPDATE_BLKREF_HEAP_NEW, newbuf, bufflags); if (oldbuf != newbuf) - XLogRegisterBuffer(1, oldbuf, REGBUF_STANDARD); + XLogRegisterBuffer(HEAP_UPDATE_BLKREF_HEAP_OLD, oldbuf, REGBUF_STANDARD); XLogRegisterData(&xlrec, SizeOfHeapUpdate); @@ -8987,15 +8991,18 @@ log_heap_update(Relation reln, Buffer oldbuf, { prefix_suffix[0] = prefixlen; prefix_suffix[1] = suffixlen; - XLogRegisterBufData(0, &prefix_suffix, sizeof(uint16) * 2); + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, &prefix_suffix, + sizeof(uint16) * 2); } else if (prefixlen > 0) { - XLogRegisterBufData(0, &prefixlen, sizeof(uint16)); + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, &prefixlen, + sizeof(uint16)); } else { - XLogRegisterBufData(0, &suffixlen, sizeof(uint16)); + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, &suffixlen, + sizeof(uint16)); } } @@ -9009,10 +9016,10 @@ log_heap_update(Relation reln, Buffer oldbuf, * * The 'data' doesn't include the common prefix or suffix. */ - XLogRegisterBufData(0, &xlhdr, SizeOfHeapHeader); + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, &xlhdr, SizeOfHeapHeader); if (prefixlen == 0) { - XLogRegisterBufData(0, + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, (char *) newtup->t_data + SizeofHeapTupleHeader, newtup->t_len - SizeofHeapTupleHeader - suffixlen); } @@ -9025,13 +9032,13 @@ log_heap_update(Relation reln, Buffer oldbuf, /* bitmap [+ padding] [+ oid] */ if (newtup->t_data->t_hoff - SizeofHeapTupleHeader > 0) { - XLogRegisterBufData(0, + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, (char *) newtup->t_data + SizeofHeapTupleHeader, newtup->t_data->t_hoff - SizeofHeapTupleHeader); } /* data after common prefix */ - XLogRegisterBufData(0, + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, (char *) newtup->t_data + newtup->t_data->t_hoff + prefixlen, newtup->t_len - newtup->t_data->t_hoff - prefixlen - suffixlen); } diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.c index eb4bd3d6ae3..87b5c44fd46 100644 --- a/src/backend/access/heap/heapam_xlog.c +++ b/src/backend/access/heap/heapam_xlog.c @@ -350,7 +350,8 @@ heap_xlog_delete(XLogReaderState *record) RelFileLocator target_locator; ItemPointerData target_tid; - XLogRecGetBlockTag(record, 0, &target_locator, NULL, &blkno); + XLogRecGetBlockTag(record, HEAP_DELETE_BLKREF_HEAP, &target_locator, NULL, + &blkno); ItemPointerSetBlockNumber(&target_tid, blkno); ItemPointerSetOffsetNumber(&target_tid, xlrec->offnum); @@ -369,7 +370,8 @@ heap_xlog_delete(XLogReaderState *record) FreeFakeRelcacheEntry(reln); } - if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) + if (XLogReadBufferForRedo(record, HEAP_DELETE_BLKREF_HEAP, + &buffer) == BLK_NEEDS_REDO) { page = BufferGetPage(buffer); @@ -434,7 +436,8 @@ heap_xlog_insert(XLogReaderState *record) ItemPointerData target_tid; XLogRedoAction action; - XLogRecGetBlockTag(record, 0, &target_locator, NULL, &blkno); + XLogRecGetBlockTag(record, HEAP_INSERT_BLKREF_HEAP, &target_locator, NULL, + &blkno); ItemPointerSetBlockNumber(&target_tid, blkno); ItemPointerSetOffsetNumber(&target_tid, xlrec->offnum); @@ -462,13 +465,14 @@ heap_xlog_insert(XLogReaderState *record) */ if (XLogRecGetInfo(record) & XLOG_HEAP_INIT_PAGE) { - buffer = XLogInitBufferForRedo(record, 0); + buffer = XLogInitBufferForRedo(record, HEAP_INSERT_BLKREF_HEAP); page = BufferGetPage(buffer); PageInit(page, BufferGetPageSize(buffer), 0); action = BLK_NEEDS_REDO; } else - action = XLogReadBufferForRedo(record, 0, &buffer); + action = XLogReadBufferForRedo(record, HEAP_INSERT_BLKREF_HEAP, + &buffer); if (action == BLK_NEEDS_REDO) { Size datalen; @@ -479,7 +483,7 @@ heap_xlog_insert(XLogReaderState *record) if (PageGetMaxOffsetNumber(page) + 1 < xlrec->offnum) elog(PANIC, "invalid max offset number"); - data = XLogRecGetBlockData(record, 0, &datalen); + data = XLogRecGetBlockData(record, HEAP_INSERT_BLKREF_HEAP, &datalen); newlen = datalen - SizeOfHeapHeader; Assert(datalen > SizeOfHeapHeader && newlen <= MaxHeapTupleSize); @@ -559,7 +563,8 @@ heap_xlog_multi_insert(XLogReaderState *record) */ xlrec = (xl_heap_multi_insert *) XLogRecGetData(record); - XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno); + XLogRecGetBlockTag(record, HEAP_MULTI_INSERT_BLKREF_HEAP, &rlocator, NULL, + &blkno); /* check that the mutually exclusive flags are not both set */ Assert(!((xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) && @@ -582,13 +587,14 @@ heap_xlog_multi_insert(XLogReaderState *record) if (isinit) { - buffer = XLogInitBufferForRedo(record, 0); + buffer = XLogInitBufferForRedo(record, HEAP_MULTI_INSERT_BLKREF_HEAP); page = BufferGetPage(buffer); PageInit(page, BufferGetPageSize(buffer), 0); action = BLK_NEEDS_REDO; } else - action = XLogReadBufferForRedo(record, 0, &buffer); + action = XLogReadBufferForRedo(record, HEAP_MULTI_INSERT_BLKREF_HEAP, + &buffer); if (action == BLK_NEEDS_REDO) { char *tupdata; @@ -596,7 +602,8 @@ heap_xlog_multi_insert(XLogReaderState *record) Size len; /* Tuples are stored as block data */ - tupdata = XLogRecGetBlockData(record, 0, &len); + tupdata = XLogRecGetBlockData(record, HEAP_MULTI_INSERT_BLKREF_HEAP, + &len); endptr = tupdata + len; page = (Page) BufferGetPage(buffer); @@ -713,8 +720,10 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) oldtup.t_data = NULL; oldtup.t_len = 0; - XLogRecGetBlockTag(record, 0, &rlocator, NULL, &newblk); - if (XLogRecGetBlockTagExtended(record, 1, NULL, NULL, &oldblk, NULL)) + XLogRecGetBlockTag(record, HEAP_UPDATE_BLKREF_HEAP_NEW, &rlocator, NULL, + &newblk); + if (XLogRecGetBlockTagExtended(record, HEAP_UPDATE_BLKREF_HEAP_OLD, NULL, NULL, + &oldblk, NULL)) { /* HOT updates are never done across pages */ Assert(!hot_update); @@ -750,7 +759,8 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) */ /* Deal with old tuple version */ - oldaction = XLogReadBufferForRedo(record, (oldblk == newblk) ? 0 : 1, + oldaction = XLogReadBufferForRedo(record, (oldblk == newblk) ? + HEAP_UPDATE_BLKREF_HEAP_NEW : HEAP_UPDATE_BLKREF_HEAP_OLD, &obuffer); if (oldaction == BLK_NEEDS_REDO) { @@ -800,13 +810,14 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) } else if (XLogRecGetInfo(record) & XLOG_HEAP_INIT_PAGE) { - nbuffer = XLogInitBufferForRedo(record, 0); + nbuffer = XLogInitBufferForRedo(record, HEAP_UPDATE_BLKREF_HEAP_NEW); page = (Page) BufferGetPage(nbuffer); PageInit(page, BufferGetPageSize(nbuffer), 0); newaction = BLK_NEEDS_REDO; } else - newaction = XLogReadBufferForRedo(record, 0, &nbuffer); + newaction = XLogReadBufferForRedo(record, HEAP_UPDATE_BLKREF_HEAP_NEW, + &nbuffer); /* * The visibility map may need to be fixed even if the heap page is @@ -831,7 +842,8 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) Size datalen; Size tuplen; - recdata = XLogRecGetBlockData(record, 0, &datalen); + recdata = XLogRecGetBlockData(record, HEAP_UPDATE_BLKREF_HEAP_NEW, + &datalen); recdata_end = recdata + datalen; page = BufferGetPage(nbuffer); @@ -1015,7 +1027,8 @@ heap_xlog_lock(XLogReaderState *record) BlockNumber block; Relation reln; - XLogRecGetBlockTag(record, 0, &rlocator, NULL, &block); + XLogRecGetBlockTag(record, HEAP_LOCK_BLKREF_HEAP, &rlocator, NULL, + &block); reln = CreateFakeRelcacheEntry(rlocator); visibilitymap_pin(reln, block, &vmbuffer); @@ -1025,7 +1038,8 @@ heap_xlog_lock(XLogReaderState *record) FreeFakeRelcacheEntry(reln); } - if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) + if (XLogReadBufferForRedo(record, HEAP_LOCK_BLKREF_HEAP, + &buffer) == BLK_NEEDS_REDO) { page = (Page) BufferGetPage(buffer); @@ -1091,7 +1105,8 @@ heap_xlog_lock_updated(XLogReaderState *record) BlockNumber block; Relation reln; - XLogRecGetBlockTag(record, 0, &rlocator, NULL, &block); + XLogRecGetBlockTag(record, HEAP_LOCK_BLKREF_HEAP, &rlocator, NULL, + &block); reln = CreateFakeRelcacheEntry(rlocator); visibilitymap_pin(reln, block, &vmbuffer); @@ -1101,7 +1116,8 @@ heap_xlog_lock_updated(XLogReaderState *record) FreeFakeRelcacheEntry(reln); } - if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) + if (XLogReadBufferForRedo(record, HEAP_LOCK_BLKREF_HEAP, + &buffer) == BLK_NEEDS_REDO) { page = BufferGetPage(buffer); diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h index 277df6b3cf0..92744c53899 100644 --- a/src/include/access/heapam_xlog.h +++ b/src/include/access/heapam_xlog.h @@ -110,6 +110,8 @@ (XLH_DELETE_CONTAINS_OLD_TUPLE | XLH_DELETE_CONTAINS_OLD_KEY) /* This is what we need to know about delete */ +#define HEAP_DELETE_BLKREF_HEAP 0 + typedef struct xl_heap_delete { TransactionId xmax; /* xmax of the deleted tuple */ @@ -157,12 +159,14 @@ typedef struct xl_heap_header #define SizeOfHeapHeader (offsetof(xl_heap_header, t_hoff) + sizeof(uint8)) /* This is what we need to know about insert */ +#define HEAP_INSERT_BLKREF_HEAP 0 + typedef struct xl_heap_insert { OffsetNumber offnum; /* inserted tuple's offset */ uint8 flags; - /* xl_heap_header & TUPLE DATA in backup block 0 */ + /* xl_heap_header & TUPLE DATA in HEAP_INSERT_BLKREF_HEAP */ } xl_heap_insert; #define SizeOfHeapInsert (offsetof(xl_heap_insert, flags) + sizeof(uint8)) @@ -173,10 +177,13 @@ typedef struct xl_heap_insert * The main data of the record consists of this xl_heap_multi_insert header. * 'offsets' array is omitted if the whole page is reinitialized * (XLOG_HEAP_INIT_PAGE). - * - * In block 0's data portion, there is an xl_multi_insert_tuple struct, - * followed by the tuple data for each tuple. There is padding to align - * each xl_multi_insert_tuple struct. + */ +#define HEAP_MULTI_INSERT_BLKREF_HEAP 0 + +/* + * In HEAP_MULTI_INSERT_BLKREF_HEAP's data portion, there is an + * xl_multi_insert_tuple struct, followed by the tuple data for each tuple. + * There is padding to align each xl_multi_insert_tuple struct. */ typedef struct xl_heap_multi_insert { @@ -201,7 +208,7 @@ typedef struct xl_multi_insert_tuple /* * This is what we need to know about update|hot_update * - * Backup blk 0: new page + * HEAP_UPDATE_BLKREF_HEAP_NEW: new page * * If XLH_UPDATE_PREFIX_FROM_OLD or XLH_UPDATE_SUFFIX_FROM_OLD flags are set, * the prefix and/or suffix come first, as one or two uint16s. @@ -213,8 +220,13 @@ typedef struct xl_multi_insert_tuple * If XLH_UPDATE_CONTAINS_NEW_TUPLE flag is given, the tuple data is * included even if a full-page image was taken. * - * Backup blk 1: old page, if different. (no data, just a reference to the blk) + * HEAP_UPDATE_BLKREF_HEAP_OLD: old page, if different. (no data, just a reference + * to the block) */ + +#define HEAP_UPDATE_BLKREF_HEAP_NEW 0 +#define HEAP_UPDATE_BLKREF_HEAP_OLD 1 + typedef struct xl_heap_update { TransactionId old_xmax; /* xmax of the old tuple */ @@ -393,6 +405,8 @@ typedef struct xlhp_prune_items #define XLH_LOCK_ALL_FROZEN_CLEARED 0x01 /* This is what we need to know about lock */ +#define HEAP_LOCK_BLKREF_HEAP 0 + typedef struct xl_heap_lock { TransactionId xmax; /* might be a MultiXactId */ -- 2.47.3
From edb0c2262eab9ca3515214e24813b855b76e1885 Mon Sep 17 00:00:00 2001 From: Melanie Plageman <[email protected]> Date: Wed, 29 Apr 2026 12:53:49 -0400 Subject: [PATCH v5 2/4] Fix VM clear WAL logging by registering VM blocks Heap WAL records that clear bits on the visibility map (like inserts and deletes) did not register the visibility map blocks they modified. Because the WAL summarizer only records registered blocks, an incremental backup taken over such operations would omit the changed VM pages. On restore, the VM would retain stale all-visible/all-frozen bits, which can cause wrong results from index-only scans and incorrect relfrozenxid advancement due to vacuum page skipping. Not registering the VM buffer also meant we never emitted FPIs of VM pages when clearing bits. A torn VM page won't raise an error because the VM is read with ZERO_ON_ERROR; with checksums on, it would be detected and zeroed, but with checksums off, it is accepted as-is and can lead to data corruption. Fix this by registering the VM buffer in the WAL record when clearing VM bits. The VM buffer must now be locked throughout the critical section that modifies the VM and heap pages and emits the WAL record. This can slow down operations that clear the VM, since the VM lock is held longer and VM FPIs may be emitted, but it is required for correctness. Note that this fix does not repair existing incremental backups. Author: Melanie Plageman <[email protected]> Author: Andres Freund <[email protected]> Reviewed-by: Robert Haas <[email protected]> Reviewed-by: Andrey Borodin <[email protected]> Discussion: https://postgr.es/m/flat/CAAKRu_bn%2Be7F4yPFBgFbnP%2BsyJRKyNK092bjD2LKvZW7O4Svag> Backpatch-through: 17 --- contrib/pg_surgery/heap_surgery.c | 40 ++- src/backend/access/heap/heapam.c | 396 ++++++++++++++++++++---- src/backend/access/heap/heapam_xlog.c | 252 ++++++++++----- src/backend/access/heap/visibilitymap.c | 34 +- src/bin/pg_walsummary/t/002_blocks.pl | 7 +- src/include/access/heapam_xlog.h | 17 +- src/include/access/visibilitymap.h | 2 + 7 files changed, 593 insertions(+), 155 deletions(-) diff --git a/contrib/pg_surgery/heap_surgery.c b/contrib/pg_surgery/heap_surgery.c index 602aca66c60..6a38ac577c6 100644 --- a/contrib/pg_surgery/heap_surgery.c +++ b/contrib/pg_surgery/heap_surgery.c @@ -17,6 +17,7 @@ #include "access/visibilitymap.h" #include "access/xloginsert.h" #include "catalog/pg_am_d.h" +#include "catalog/pg_control.h" #include "miscadmin.h" #include "storage/bufmgr.h" #include "utils/acl.h" @@ -146,6 +147,7 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) { Buffer buf; Buffer vmbuf = InvalidBuffer; + bool unlock_vmbuf = false; Page page; BlockNumber blkno; OffsetNumber curoff; @@ -233,11 +235,15 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) } /* - * Before entering the critical section, pin the visibility map page - * if it appears to be necessary. + * Before entering the critical section, pin and lock the visibility + * map page if it appears to be necessary. */ if (heap_force_opt == HEAP_FORCE_KILL && PageIsAllVisible(page)) + { visibilitymap_pin(rel, blkno, &vmbuf); + LockBuffer(vmbuf, BUFFER_LOCK_EXCLUSIVE); + unlock_vmbuf = true; + } /* No ereport(ERROR) from here until all the changes are logged. */ START_CRIT_SECTION(); @@ -266,10 +272,11 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) */ if (PageIsAllVisible(page)) { + if (visibilitymap_clear_locked(rel, blkno, vmbuf, + VISIBILITYMAP_VALID_BITS)) + did_modify_vm = true; + PageClearAllVisible(page); - visibilitymap_clear(rel, blkno, vmbuf, - VISIBILITYMAP_VALID_BITS); - did_modify_vm = true; } } else @@ -320,18 +327,29 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) /* XLOG stuff */ if (RelationNeedsWAL(rel)) - log_newpage_buffer(buf, true); + { + XLogRecPtr recptr; + + XLogBeginInsert(); + XLogRegisterBuffer(0, buf, REGBUF_STANDARD | REGBUF_FORCE_IMAGE); + /* Include the VM page if it was modified. */ + if (did_modify_vm) + XLogRegisterBuffer(1, vmbuf, REGBUF_FORCE_IMAGE); + recptr = XLogInsert(RM_XLOG_ID, XLOG_FPI); + if (did_modify_vm) + PageSetLSN(BufferGetPage(vmbuf), recptr); + PageSetLSN(BufferGetPage(buf), recptr); + } } - /* WAL log the VM page if it was modified. */ - if (did_modify_vm && RelationNeedsWAL(rel)) - log_newpage_buffer(vmbuf, false); - END_CRIT_SECTION(); UnlockReleaseBuffer(buf); - if (vmbuf != InvalidBuffer) + if (unlock_vmbuf) + LockBuffer(vmbuf, BUFFER_LOCK_UNLOCK); + + if (BufferIsValid(vmbuf)) ReleaseBuffer(vmbuf); /* Update the current_start_ptr before moving to the next page. */ diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index cf37956a2bf..fd8ca72728f 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -58,7 +58,8 @@ static HeapTuple heap_prepare_insert(Relation relation, HeapTuple tup, TransactionId xid, CommandId cid, int options); static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf, - Buffer newbuf, HeapTuple oldtup, + Buffer vmbuffer_old, Buffer newbuf, + Buffer vmbuffer_new, HeapTuple oldtup, HeapTuple newtup, HeapTuple old_key_tuple, bool all_visible_cleared, bool new_all_visible_cleared); #ifdef USE_ASSERT_CHECKING @@ -2083,8 +2084,10 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, TransactionId xid = GetCurrentTransactionId(); HeapTuple heaptup; Buffer buffer; + Page page; Buffer vmbuffer = InvalidBuffer; - bool all_visible_cleared = false; + bool clear_all_visible = false; + bool vmbuffer_modified = false; /* Cheap, simplistic check that the tuple matches the rel's rowtype. */ Assert(HeapTupleHeaderGetNatts(tup->t_data) <= @@ -2108,6 +2111,7 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, InvalidBuffer, options, bistate, &vmbuffer, NULL, 0); + page = BufferGetPage(buffer); /* * We're about to do the actual insert -- but check for conflict first, to @@ -2126,19 +2130,28 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, */ CheckForSerializableConflictIn(relation, NULL, InvalidBlockNumber); + /* Lock the vmbuffer before the critical section */ + if (PageIsAllVisible(page)) + { + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + clear_all_visible = true; + } + /* NO EREPORT(ERROR) from here till changes are logged */ START_CRIT_SECTION(); RelationPutHeapTuple(relation, buffer, heaptup, (options & HEAP_INSERT_SPECULATIVE) != 0); - if (PageIsAllVisible(BufferGetPage(buffer))) + if (clear_all_visible) { - all_visible_cleared = true; - PageClearAllVisible(BufferGetPage(buffer)); - visibilitymap_clear(relation, - ItemPointerGetBlockNumber(&(heaptup->t_self)), - vmbuffer, VISIBILITYMAP_VALID_BITS); + /* It's possible the VM bits were already clear */ + if (visibilitymap_clear_locked(relation, + ItemPointerGetBlockNumber(&(heaptup->t_self)), + vmbuffer, VISIBILITYMAP_VALID_BITS)) + vmbuffer_modified = true; + + PageClearAllVisible(page); } /* @@ -2160,7 +2173,6 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, xl_heap_insert xlrec; xl_heap_header xlhdr; XLogRecPtr recptr; - Page page = BufferGetPage(buffer); uint8 info = XLOG_HEAP_INSERT; int bufflags = 0; @@ -2185,7 +2197,7 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, xlrec.offnum = ItemPointerGetOffsetNumber(&heaptup->t_self); xlrec.flags = 0; - if (all_visible_cleared) + if (clear_all_visible) xlrec.flags |= XLH_INSERT_ALL_VISIBLE_CLEARED; if (options & HEAP_INSERT_SPECULATIVE) xlrec.flags |= XLH_INSERT_IS_SPECULATIVE; @@ -2230,15 +2242,28 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, /* filtering by origin on a row level is much more efficient */ XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN); + if (vmbuffer_modified) + XLogRegisterBuffer(HEAP_INSERT_BLKREF_VM, vmbuffer, 0); + recptr = XLogInsert(RM_HEAP_ID, info); PageSetLSN(page, recptr); + + if (vmbuffer_modified) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); UnlockReleaseBuffer(buffer); - if (vmbuffer != InvalidBuffer) + + /* + * We locked vmbuffer if clear_all_visible was true regardless of whether + * or not we ended up modifying the vmbuffer. + */ + if (clear_all_visible) + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + if (BufferIsValid(vmbuffer)) ReleaseBuffer(vmbuffer); /* @@ -2419,8 +2444,9 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, while (ndone < ntuples) { Buffer buffer; - bool all_visible_cleared = false; + bool clear_all_visible = false; bool all_frozen_set = false; + bool vmbuffer_modified = false; int nthispage; CHECK_FOR_INTERRUPTS(); @@ -2462,6 +2488,17 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, if (starting_with_empty_page && (options & HEAP_INSERT_FROZEN)) all_frozen_set = true; + /* + * If clearing all-visible, take the VM buffer lock before entering + * the critical section where that action will be WAL-logged. Setting + * the VM all-frozen is done and WAL-logged separately. + */ + if (PageIsAllVisible(page) && !(options & HEAP_INSERT_FROZEN)) + { + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + clear_all_visible = true; + } + /* NO EREPORT(ERROR) from here till changes are logged */ START_CRIT_SECTION(); @@ -2502,13 +2539,16 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, * If we're only adding already frozen rows to a previously empty * page, mark it as all-visible. */ - if (PageIsAllVisible(page) && !(options & HEAP_INSERT_FROZEN)) + if (clear_all_visible) { - all_visible_cleared = true; + Assert(!(options & HEAP_INSERT_FROZEN)); + /* It's possible the VM bits were already clear */ + if (visibilitymap_clear_locked(relation, + BufferGetBlockNumber(buffer), + vmbuffer, VISIBILITYMAP_VALID_BITS)) + vmbuffer_modified = true; + PageClearAllVisible(page); - visibilitymap_clear(relation, - BufferGetBlockNumber(buffer), - vmbuffer, VISIBILITYMAP_VALID_BITS); } else if (all_frozen_set) PageSetAllVisible(page); @@ -2554,10 +2594,10 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, tupledata = scratchptr; /* check that the mutually exclusive flags are not both set */ - Assert(!(all_visible_cleared && all_frozen_set)); + Assert(!(clear_all_visible && all_frozen_set)); xlrec->flags = 0; - if (all_visible_cleared) + if (clear_all_visible) xlrec->flags = XLH_INSERT_ALL_VISIBLE_CLEARED; if (all_frozen_set) xlrec->flags = XLH_INSERT_ALL_FROZEN_SET; @@ -2623,6 +2663,8 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, XLogRegisterData(xlrec, tupledata - scratch.data); XLogRegisterBuffer(HEAP_MULTI_INSERT_BLKREF_HEAP, buffer, REGBUF_STANDARD | bufflags); + if (vmbuffer_modified) + XLogRegisterBuffer(HEAP_MULTI_INSERT_BLKREF_VM, vmbuffer, 0); XLogRegisterBufData(HEAP_MULTI_INSERT_BLKREF_HEAP, tupledata, totaldatalen); @@ -2633,10 +2675,19 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, recptr = XLogInsert(RM_HEAP2_ID, info); PageSetLSN(page, recptr); + if (vmbuffer_modified) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); + /* + * We locked vmbuffer if clear_all_visible was true regardless of + * whether or not we ended up modifying the vmbuffer. + */ + if (clear_all_visible) + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + /* * If we've frozen everything on the page, update the visibilitymap. * We're already holding pin on the vmbuffer. @@ -2786,12 +2837,13 @@ heap_delete(Relation relation, ItemPointer tid, BlockNumber block; Buffer buffer; Buffer vmbuffer = InvalidBuffer; + bool vmbuffer_modified = false; TransactionId new_xmax; uint16 new_infomask, new_infomask2; bool have_tuple_lock = false; bool iscombo; - bool all_visible_cleared = false; + bool clear_all_visible = false; HeapTuple old_key_tuple = NULL; /* replica identity of the tuple */ bool old_key_copied = false; @@ -3040,6 +3092,13 @@ heap_delete(Relation relation, ItemPointer tid, xid, LockTupleExclusive, true, &new_xmax, &new_infomask, &new_infomask2); + /* Lock the VM before entering the critical section */ + if (PageIsAllVisible(page)) + { + clear_all_visible = true; + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + } + START_CRIT_SECTION(); /* @@ -3051,12 +3110,14 @@ heap_delete(Relation relation, ItemPointer tid, */ PageSetPrunable(page, xid); - if (PageIsAllVisible(page)) + if (clear_all_visible) { - all_visible_cleared = true; + /* It's possible the VM bits were already clear */ + if (visibilitymap_clear_locked(relation, BufferGetBlockNumber(buffer), + vmbuffer, VISIBILITYMAP_VALID_BITS)) + vmbuffer_modified = true; + PageClearAllVisible(page); - visibilitymap_clear(relation, BufferGetBlockNumber(buffer), - vmbuffer, VISIBILITYMAP_VALID_BITS); } /* store transaction information of xact deleting the tuple */ @@ -3096,7 +3157,7 @@ heap_delete(Relation relation, ItemPointer tid, log_heap_new_cid(relation, &tp); xlrec.flags = 0; - if (all_visible_cleared) + if (clear_all_visible) xlrec.flags |= XLH_DELETE_ALL_VISIBLE_CLEARED; if (changingPart) xlrec.flags |= XLH_DELETE_IS_PARTITION_MOVE; @@ -3137,13 +3198,27 @@ heap_delete(Relation relation, ItemPointer tid, /* filtering by origin on a row level is much more efficient */ XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN); + if (vmbuffer_modified) + XLogRegisterBuffer(HEAP_DELETE_BLKREF_VM, vmbuffer, 0); + recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_DELETE); PageSetLSN(page, recptr); + + if (vmbuffer_modified) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); + /* + * Release VM lock first, since it covers many heap blocks. We locked + * vmbuffer if clear_all_visible was true regardless of whether or not we + * ended up modifying the vmbuffer. + */ + if (clear_all_visible) + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + LockBuffer(buffer, BUFFER_LOCK_UNLOCK); if (vmbuffer != InvalidBuffer) @@ -3261,13 +3336,16 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, HeapTuple heaptup; HeapTuple old_key_tuple = NULL; bool old_key_copied = false; - Page page; + Page page, + newpage; BlockNumber block; MultiXactStatus mxact_status; Buffer buffer, newbuf, vmbuffer = InvalidBuffer, vmbuffer_new = InvalidBuffer; + bool unlock_vmbuffer = false; + bool unlock_vmbuffer_new = false; bool need_toast; Size newtupsize, pagefree; @@ -3276,8 +3354,10 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, bool use_hot_update = false; bool summarized_update = false; bool key_intact; - bool all_visible_cleared = false; - bool all_visible_cleared_new = false; + bool clear_all_visible = false; + bool clear_all_visible_new = false; + bool vmbuffer_modified = false; + bool vmbuffer_new_modified = false; bool checked_lockers; bool locker_remains; bool id_has_external = false; @@ -3852,6 +3932,12 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, Assert(HEAP_XMAX_IS_LOCKED_ONLY(infomask_lock_old_tuple)); + if (PageIsAllVisible(page)) + { + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + unlock_vmbuffer = true; + } + START_CRIT_SECTION(); /* Clear obsolete visibility flags ... */ @@ -3874,10 +3960,13 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, * overhead would be unchanged, that doesn't seem necessarily * worthwhile. */ - if (PageIsAllVisible(page) && - visibilitymap_clear(relation, block, vmbuffer, - VISIBILITYMAP_ALL_FROZEN)) - cleared_all_frozen = true; + if (PageIsAllVisible(page)) + { + /* It's possible all-frozen was already clear */ + if (visibilitymap_clear_locked(relation, block, vmbuffer, + VISIBILITYMAP_ALL_FROZEN)) + cleared_all_frozen = true; + } MarkBufferDirty(buffer); @@ -3896,12 +3985,24 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, xlrec.flags = cleared_all_frozen ? XLH_LOCK_ALL_FROZEN_CLEARED : 0; XLogRegisterData(&xlrec, SizeOfHeapLock); + + if (cleared_all_frozen) + XLogRegisterBuffer(HEAP_LOCK_BLKREF_VM, vmbuffer, 0); + recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_LOCK); PageSetLSN(page, recptr); + + if (cleared_all_frozen) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); + /* release VM lock first, since it covers many heap blocks */ + if (unlock_vmbuffer) + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + unlock_vmbuffer = false; + LockBuffer(buffer, BUFFER_LOCK_UNLOCK); /* @@ -3987,6 +4088,8 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, heaptup = newtup; } + newpage = BufferGetPage(newbuf); + /* * We're about to do the actual update -- check for conflict first, to * avoid possibly having to roll back work we've just done. @@ -4050,6 +4153,69 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, id_has_external, &old_key_copied); + clear_all_visible = PageIsAllVisible(page); + clear_all_visible_new = newbuf != buffer && PageIsAllVisible(newpage); + + /* + * Clear PD_ALL_VISIBLE flags and reset visibility map bits for any heap + * pages that were all-visible. If there are two heap pages, we may need + * to clear VM bits for both. + */ + if (clear_all_visible && clear_all_visible_new && + vmbuffer_new == vmbuffer) + { + /* + * This is the more complicated case: both the new and old heap pages + * are all-visible and both their VM bits are on the same page of the + * VM, so we register a single VM buffer as HEAP_UPDATE_BLKREF_VM_NEW + * in the WAL record. We must be careful to only lock and register one + * buffer, even though we modify it twice -- once for each heap + * block's VM bits. + */ + LockBuffer(vmbuffer_new, BUFFER_LOCK_EXCLUSIVE); + unlock_vmbuffer_new = true; + + /* We will not lock or attempt to modify old VM buffer */ + } + else + { + /* + * In all the remaining cases, we will clear at most one heap block's + * VM bits per VM page. + */ + Buffer vmbuffers[2] = { + clear_all_visible ? vmbuffer : InvalidBuffer, + clear_all_visible_new ? vmbuffer_new : InvalidBuffer + }; + + /* + * When both pages need different VM pages cleared, acquire the VM + * buffer locks in VM block order to avoid deadlocks between backends + * updating tuples in opposite directions across VM pages. + */ + if (clear_all_visible && clear_all_visible_new && + BufferGetBlockNumber(vmbuffers[0]) > BufferGetBlockNumber(vmbuffers[1])) + { + Buffer swap = vmbuffers[0]; + + vmbuffers[0] = vmbuffers[1]; + vmbuffers[1] = swap; + } + + Assert((!BufferIsValid(vmbuffers[0]) && !BufferIsValid(vmbuffers[1])) || + vmbuffers[0] != vmbuffers[1]); + + if (BufferIsValid(vmbuffers[0])) + LockBuffer(vmbuffers[0], BUFFER_LOCK_EXCLUSIVE); + if (BufferIsValid(vmbuffers[1])) + LockBuffer(vmbuffers[1], BUFFER_LOCK_EXCLUSIVE); + + if (clear_all_visible) + unlock_vmbuffer = true; + if (clear_all_visible_new) + unlock_vmbuffer_new = true; + } + /* NO EREPORT(ERROR) from here till changes are logged */ START_CRIT_SECTION(); @@ -4086,7 +4252,6 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, RelationPutHeapTuple(relation, newbuf, heaptup, false); /* insert new tuple */ - /* Clear obsolete visibility flags, possibly set by ourselves above... */ oldtup.t_data->t_infomask &= ~(HEAP_XMAX_BITS | HEAP_MOVED); oldtup.t_data->t_infomask2 &= ~HEAP_KEYS_UPDATED; @@ -4100,20 +4265,42 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, /* record address of new tuple in t_ctid of old one */ oldtup.t_data->t_ctid = heaptup->t_self; - /* clear PD_ALL_VISIBLE flags, reset all visibilitymap bits */ - if (PageIsAllVisible(BufferGetPage(buffer))) + /* + * Clear PD_ALL_VISIBLE flags and reset all visibilitymap bits. In all + * cases, it's possible that PD_ALL_VISIBLE was set but the corresponding + * visibility map bits were already clear. + */ + if (clear_all_visible) { - all_visible_cleared = true; - PageClearAllVisible(BufferGetPage(buffer)); - visibilitymap_clear(relation, BufferGetBlockNumber(buffer), - vmbuffer, VISIBILITYMAP_VALID_BITS); + if (visibilitymap_clear_locked(relation, block, + vmbuffer, VISIBILITYMAP_VALID_BITS)) + { + /* + * When old and new heap blocks' VM bits are on the same VM page, + * that page is registered in the WAL record only once. If both + * heap pages were PD_ALL_VISIBLE and either VM bit needs + * clearing, we register the VM buffer as + * HEAP_UPDATE_BLKREF_VM_NEW. + */ + if (clear_all_visible_new && vmbuffer == vmbuffer_new) + vmbuffer_new_modified = true; + else + vmbuffer_modified = true; + } + + PageClearAllVisible(page); } - if (newbuf != buffer && PageIsAllVisible(BufferGetPage(newbuf))) + if (clear_all_visible_new) { - all_visible_cleared_new = true; - PageClearAllVisible(BufferGetPage(newbuf)); - visibilitymap_clear(relation, BufferGetBlockNumber(newbuf), - vmbuffer_new, VISIBILITYMAP_VALID_BITS); + /* + * If both heap blocks' VM bits are on the same VM buffer, this will + * clear the new heap block's VM bits from the shared vmbuffer. + */ + if (visibilitymap_clear_locked(relation, BufferGetBlockNumber(newbuf), + vmbuffer_new, VISIBILITYMAP_VALID_BITS)) + vmbuffer_new_modified = true; + + PageClearAllVisible(newpage); } if (newbuf != buffer) @@ -4136,19 +4323,30 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, } recptr = log_heap_update(relation, buffer, - newbuf, &oldtup, heaptup, + vmbuffer_modified ? vmbuffer : InvalidBuffer, + newbuf, + vmbuffer_new_modified ? vmbuffer_new : InvalidBuffer, + &oldtup, heaptup, old_key_tuple, - all_visible_cleared, - all_visible_cleared_new); + clear_all_visible, + clear_all_visible_new); if (newbuf != buffer) - { - PageSetLSN(BufferGetPage(newbuf), recptr); - } + PageSetLSN(newpage, recptr); PageSetLSN(BufferGetPage(buffer), recptr); + + if (vmbuffer_modified) + PageSetLSN(BufferGetPage(vmbuffer), recptr); + if (vmbuffer_new_modified) + PageSetLSN(BufferGetPage(vmbuffer_new), recptr); } END_CRIT_SECTION(); + if (unlock_vmbuffer) + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + if (unlock_vmbuffer_new) + LockBuffer(vmbuffer_new, BUFFER_LOCK_UNLOCK); + if (newbuf != buffer) LockBuffer(newbuf, BUFFER_LOCK_UNLOCK); LockBuffer(buffer, BUFFER_LOCK_UNLOCK); @@ -4586,6 +4784,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, ItemId lp; Page page; Buffer vmbuffer = InvalidBuffer; + bool unlock_vmbuffer = false; BlockNumber block; TransactionId xid, xmax; @@ -4599,6 +4798,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, *buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(tid)); block = ItemPointerGetBlockNumber(tid); + page = BufferGetPage(*buffer); /* * Before locking the buffer, pin the visibility map page if it appears to @@ -4606,12 +4806,11 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, * in the middle of changing this, so we'll need to recheck after we have * the lock. */ - if (PageIsAllVisible(BufferGetPage(*buffer))) + if (PageIsAllVisible(page)) visibilitymap_pin(relation, block, &vmbuffer); LockBuffer(*buffer, BUFFER_LOCK_EXCLUSIVE); - page = BufferGetPage(*buffer); lp = PageGetItemId(page, ItemPointerGetOffsetNumber(tid)); Assert(ItemIdIsNormal(lp)); @@ -5166,6 +5365,13 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, GetCurrentTransactionId(), mode, false, &xid, &new_infomask, &new_infomask2); + /* Lock VM buffer before entering critical section */ + if (PageIsAllVisible(page)) + { + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + unlock_vmbuffer = true; + } + START_CRIT_SECTION(); /* @@ -5197,11 +5403,13 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, tuple->t_data->t_ctid = *tid; /* Clear only the all-frozen bit on visibility map if needed */ - if (PageIsAllVisible(page) && - visibilitymap_clear(relation, block, vmbuffer, - VISIBILITYMAP_ALL_FROZEN)) - cleared_all_frozen = true; - + if (PageIsAllVisible(page)) + { + /* It's possible all-frozen was already clear */ + if (visibilitymap_clear_locked(relation, block, vmbuffer, + VISIBILITYMAP_ALL_FROZEN)) + cleared_all_frozen = true; + } MarkBufferDirty(*buffer); @@ -5232,19 +5440,33 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, xlrec.flags = cleared_all_frozen ? XLH_LOCK_ALL_FROZEN_CLEARED : 0; XLogRegisterData(&xlrec, SizeOfHeapLock); + if (cleared_all_frozen) + XLogRegisterBuffer(HEAP_LOCK_BLKREF_VM, vmbuffer, 0); + /* we don't decode row locks atm, so no need to log the origin */ recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_LOCK); PageSetLSN(page, recptr); + + if (cleared_all_frozen) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); + /* release VM lock first, since it covers many heap blocks */ + if (unlock_vmbuffer) + { + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + unlock_vmbuffer = false; + } + result = TM_Ok; out_locked: LockBuffer(*buffer, BUFFER_LOCK_UNLOCK); + Assert(!unlock_vmbuffer); out_unlocked: if (BufferIsValid(vmbuffer)) @@ -5707,6 +5929,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, ItemPointerData tupid; HeapTupleData mytup; Buffer buf; + Page page; uint16 new_infomask, new_infomask2, old_infomask, @@ -5716,6 +5939,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, bool cleared_all_frozen = false; bool pinned_desired_page; Buffer vmbuffer = InvalidBuffer; + bool unlock_vmbuffer = false; BlockNumber block; ItemPointerCopy(tid, &tupid); @@ -5724,6 +5948,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, { new_infomask = 0; new_xmax = InvalidTransactionId; + cleared_all_frozen = false; block = ItemPointerGetBlockNumber(&tupid); ItemPointerCopy(&tupid, &(mytup.t_self)); @@ -5743,13 +5968,15 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, l4: CHECK_FOR_INTERRUPTS(); + page = BufferGetPage(buf); + /* * Before locking the buffer, pin the visibility map page if it * appears to be necessary. Since we haven't got the lock yet, * someone else might be in the middle of changing this, so we'll need * to recheck after we have the lock. */ - if (PageIsAllVisible(BufferGetPage(buf))) + if (PageIsAllVisible(page)) { visibilitymap_pin(rel, block, &vmbuffer); pinned_desired_page = true; @@ -5770,7 +5997,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, * this page. If this page isn't all-visible, we won't use the vm * page, but we hold onto such a pin till the end of the function. */ - if (!pinned_desired_page && PageIsAllVisible(BufferGetPage(buf))) + if (!pinned_desired_page && PageIsAllVisible(page)) { LockBuffer(buf, BUFFER_LOCK_UNLOCK); visibilitymap_pin(rel, block, &vmbuffer); @@ -5951,10 +6178,11 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, xid, mode, false, &new_xmax, &new_infomask, &new_infomask2); - if (PageIsAllVisible(BufferGetPage(buf)) && - visibilitymap_clear(rel, block, vmbuffer, - VISIBILITYMAP_ALL_FROZEN)) - cleared_all_frozen = true; + if (PageIsAllVisible(page)) + { + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + unlock_vmbuffer = true; + } START_CRIT_SECTION(); @@ -5967,12 +6195,19 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, MarkBufferDirty(buf); + if (PageIsAllVisible(page)) + { + /* It's possible all-frozen was already clear */ + if (visibilitymap_clear_locked(rel, block, vmbuffer, + VISIBILITYMAP_ALL_FROZEN)) + cleared_all_frozen = true; + } + /* XLOG stuff */ if (RelationNeedsWAL(rel)) { xl_heap_lock_updated xlrec; XLogRecPtr recptr; - Page page = BufferGetPage(buf); XLogBeginInsert(); XLogRegisterBuffer(HEAP_LOCK_BLKREF_HEAP, buf, REGBUF_STANDARD); @@ -5985,13 +6220,26 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, XLogRegisterData(&xlrec, SizeOfHeapLockUpdated); + if (cleared_all_frozen) + XLogRegisterBuffer(HEAP_LOCK_BLKREF_VM, vmbuffer, 0); + recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_LOCK_UPDATED); PageSetLSN(page, recptr); + + if (cleared_all_frozen) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); + /* release VM lock first, since it covers many heap blocks */ + if (unlock_vmbuffer) + { + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + unlock_vmbuffer = false; + } + next: /* if we find the end of update chain, we're done. */ if (mytup.t_data->t_infomask & HEAP_XMAX_INVALID || @@ -6017,6 +6265,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, out_unlocked: if (vmbuffer != InvalidBuffer) ReleaseBuffer(vmbuffer); + Assert(!unlock_vmbuffer); return result; } @@ -8848,8 +9097,9 @@ log_heap_visible(Relation rel, Buffer heap_buffer, Buffer vm_buffer, * have modified the buffer(s) and marked them dirty. */ static XLogRecPtr -log_heap_update(Relation reln, Buffer oldbuf, - Buffer newbuf, HeapTuple oldtup, HeapTuple newtup, +log_heap_update(Relation reln, Buffer oldbuf, Buffer vmbuffer_old, + Buffer newbuf, Buffer vmbuffer_new, + HeapTuple oldtup, HeapTuple newtup, HeapTuple old_key_tuple, bool all_visible_cleared, bool new_all_visible_cleared) { @@ -9058,6 +9308,20 @@ log_heap_update(Relation reln, Buffer oldbuf, old_key_tuple->t_len - SizeofHeapTupleHeader); } + /* + * Register VM buffers. If the old and new heap pages' VM bits are on the + * same VM page, the caller passes only vmbuffer_new (mirroring the heap + * page convention where block 0 = new is always registered). + */ + Assert((BufferIsInvalid(vmbuffer_old) && BufferIsInvalid(vmbuffer_new)) || + (vmbuffer_old != vmbuffer_new)); + + if (BufferIsValid(vmbuffer_new)) + XLogRegisterBuffer(HEAP_UPDATE_BLKREF_VM_NEW, vmbuffer_new, 0); + + if (BufferIsValid(vmbuffer_old)) + XLogRegisterBuffer(HEAP_UPDATE_BLKREF_VM_OLD, vmbuffer_old, 0); + /* filtering by origin on a row level is much more efficient */ XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN); diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.c index 87b5c44fd46..0ce23dedfe4 100644 --- a/src/backend/access/heap/heapam_xlog.c +++ b/src/backend/access/heap/heapam_xlog.c @@ -22,6 +22,70 @@ #include "storage/freespace.h" #include "storage/standby.h" +/* + * Clear visibility map bits for a single heap block during heap redo. + * + * Used by records that modify one heap block and, at most, its corresponding + * VM block (insert, delete, multi_insert, lock). Records that can touch + * multiple heap or VM blocks (e.g. updates) replay the VM changes inline + * instead. + * + * 'record' is the WAL record being replayed + * 'target_locator' identifies the relation whose VM is being updated + * 'heap_blkno' is the heap block whose VM bits should be cleared + * 'wal_vm_block_id' is the WAL block reference id of the VM page + * 'flags' specifies which visibility map bits to clear + */ +static void +heap_xlog_vm_clear(XLogReaderState *record, + RelFileLocator target_locator, + BlockNumber heap_blkno, + uint8 wal_vm_block_id, uint8 flags) +{ + XLogRecPtr lsn = record->EndRecPtr; + Relation reln = CreateFakeRelcacheEntry(target_locator); + Buffer vmbuffer = InvalidBuffer; + + /* + * If the vmbuffer was registered, use the recovery-specific routines to + * read it. These will either apply an FPI or indicate that we should + * clear the requested bits ourselves. + * + * Originally, clearing the VM did not register the VM buffers, so since + * registering the VM buffer was a bug fix, we keep a fallback path to + * support replay of WAL generated from before the fix. + */ + if (XLogRecHasBlockRef(record, wal_vm_block_id)) + { + if (XLogReadBufferForRedo(record, wal_vm_block_id, + &vmbuffer) == BLK_NEEDS_REDO) + { + if (visibilitymap_clear_locked(reln, + heap_blkno, vmbuffer, + flags)) + PageSetLSN(BufferGetPage(vmbuffer), lsn); + } + if (BufferIsValid(vmbuffer)) + UnlockReleaseBuffer(vmbuffer); + } + else + { + /* + * This is the backwards compatibility path to clear VM bits for + * records predating VM buffer registration. It is also invoked if the + * heap page's PD_ALL_VISIBLE was cleared but the VM bits were already + * clear. The WAL record flags do not distinguish between these two + * situations. Though this is wasted effort, the behavior is + * historical and the situation should be rare. + */ + visibilitymap_pin(reln, heap_blkno, &vmbuffer); + visibilitymap_clear(reln, heap_blkno, vmbuffer, flags); + ReleaseBuffer(vmbuffer); + } + + FreeFakeRelcacheEntry(reln); +} + /* * Replay XLOG_HEAP2_PRUNE_* records. @@ -360,15 +424,9 @@ heap_xlog_delete(XLogReaderState *record) * already up-to-date. */ if (xlrec->flags & XLH_DELETE_ALL_VISIBLE_CLEARED) - { - Relation reln = CreateFakeRelcacheEntry(target_locator); - Buffer vmbuffer = InvalidBuffer; - - visibilitymap_pin(reln, blkno, &vmbuffer); - visibilitymap_clear(reln, blkno, vmbuffer, VISIBILITYMAP_VALID_BITS); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); - } + heap_xlog_vm_clear(record, target_locator, + blkno, HEAP_DELETE_BLKREF_VM, + VISIBILITYMAP_VALID_BITS); if (XLogReadBufferForRedo(record, HEAP_DELETE_BLKREF_HEAP, &buffer) == BLK_NEEDS_REDO) @@ -449,15 +507,9 @@ heap_xlog_insert(XLogReaderState *record) * already up-to-date. */ if (xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) - { - Relation reln = CreateFakeRelcacheEntry(target_locator); - Buffer vmbuffer = InvalidBuffer; - - visibilitymap_pin(reln, blkno, &vmbuffer); - visibilitymap_clear(reln, blkno, vmbuffer, VISIBILITYMAP_VALID_BITS); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); - } + heap_xlog_vm_clear(record, target_locator, + blkno, HEAP_INSERT_BLKREF_VM, + VISIBILITYMAP_VALID_BITS); /* * If we inserted the first and only tuple on the page, re-initialize the @@ -573,17 +625,15 @@ heap_xlog_multi_insert(XLogReaderState *record) /* * The visibility map may need to be fixed even if the heap page is * already up-to-date. + * + * Clear the VM (if needed) before clearing the heap page-level visibility + * flag (PD_ALL_VISIBLE) to prevent the heap page from being marked + * all-visible in the VM while its PD_ALL_VISIBLE is clear. */ if (xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) - { - Relation reln = CreateFakeRelcacheEntry(rlocator); - Buffer vmbuffer = InvalidBuffer; - - visibilitymap_pin(reln, blkno, &vmbuffer); - visibilitymap_clear(reln, blkno, vmbuffer, VISIBILITYMAP_VALID_BITS); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); - } + heap_xlog_vm_clear(record, rlocator, + blkno, HEAP_MULTI_INSERT_BLKREF_VM, + VISIBILITYMAP_VALID_BITS); if (isinit) { @@ -698,6 +748,8 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) Buffer obuffer, nbuffer; Page page; + bool new_cleared, + old_cleared; OffsetNumber offnum; ItemId lp = NULL; HeapTupleData oldtup; @@ -737,14 +789,95 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) * The visibility map may need to be fixed even if the heap page is * already up-to-date. */ - if (xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED) + new_cleared = (xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED) != 0; + old_cleared = (xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED) != 0; + if (new_cleared || old_cleared) { Relation reln = CreateFakeRelcacheEntry(rlocator); - Buffer vmbuffer = InvalidBuffer; + bool has_vm_old = XLogRecHasBlockRef(record, HEAP_UPDATE_BLKREF_VM_OLD); + bool has_vm_new = XLogRecHasBlockRef(record, HEAP_UPDATE_BLKREF_VM_NEW); + + if (has_vm_new) + { + Buffer vmbuffer_new = InvalidBuffer; + + Assert(xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED); + + if (XLogReadBufferForRedo(record, HEAP_UPDATE_BLKREF_VM_NEW, + &vmbuffer_new) == BLK_NEEDS_REDO) + { + /* + * If both the old and new heap pages were all-visible and + * their VM bits are on the same VM page, that single VM page + * is registered as HEAP_UPDATE_BLKREF_VM_NEW. Clear both heap + * blocks' VM bits from the single provided VM buffer. It's + * possible that one of the page's VM bits were already clear, + * but visibilitymap_clear() is harmless as long as we provide + * it the correct bits. + * + * We must verify that oldblk's VM bits really are on this VM + * page, rather than relying on the absence of a separate + * VM_OLD block reference: VM_OLD is also omitted when oldblk + * is on a different VM page but its bit was already clear. + */ + if (xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED && + visibilitymap_pin_ok(oldblk, vmbuffer_new)) + { + if (visibilitymap_clear_locked(reln, oldblk, vmbuffer_new, + VISIBILITYMAP_VALID_BITS)) + PageSetLSN(BufferGetPage(vmbuffer_new), lsn); + } + /* If VM_NEW is registered, we are sure newblk is on VM_NEW */ + if (visibilitymap_clear_locked(reln, newblk, vmbuffer_new, + VISIBILITYMAP_VALID_BITS)) + PageSetLSN(BufferGetPage(vmbuffer_new), lsn); + } + if (BufferIsValid(vmbuffer_new)) + UnlockReleaseBuffer(vmbuffer_new); + } + if (has_vm_old) + { + Buffer vmbuffer_old = InvalidBuffer; + + Assert(xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED); + + if (XLogReadBufferForRedo(record, HEAP_UPDATE_BLKREF_VM_OLD, &vmbuffer_old) == + BLK_NEEDS_REDO) + { + if (visibilitymap_clear_locked(reln, oldblk, vmbuffer_old, + VISIBILITYMAP_VALID_BITS)) + PageSetLSN(BufferGetPage(vmbuffer_old), lsn); + } + if (BufferIsValid(vmbuffer_old)) + UnlockReleaseBuffer(vmbuffer_old); + } + if (!has_vm_old && !has_vm_new) + { + /* + * Backwards compatibility path. Previously, the VM buffers were + * not registered in the WAL record. We need this path to replay + * WAL generated by a not-yet-patched primary during upgrade. + */ + if (old_cleared) + { + Buffer vmbuffer = InvalidBuffer; + + visibilitymap_pin(reln, oldblk, &vmbuffer); + visibilitymap_clear(reln, oldblk, vmbuffer, + VISIBILITYMAP_VALID_BITS); + ReleaseBuffer(vmbuffer); + } + if (new_cleared) + { + Buffer vmbuffer = InvalidBuffer; + + visibilitymap_pin(reln, newblk, &vmbuffer); + visibilitymap_clear(reln, newblk, vmbuffer, + VISIBILITYMAP_VALID_BITS); + ReleaseBuffer(vmbuffer); + } + } - visibilitymap_pin(reln, oldblk, &vmbuffer); - visibilitymap_clear(reln, oldblk, vmbuffer, VISIBILITYMAP_VALID_BITS); - ReleaseBuffer(vmbuffer); FreeFakeRelcacheEntry(reln); } @@ -752,10 +885,12 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) * In normal operation, it is important to lock the two pages in * page-number order, to avoid possible deadlocks against other update * operations going the other way. However, during WAL replay there can - * be no other update happening, so we don't need to worry about that. But - * we *do* need to worry that we don't expose an inconsistent state to Hot - * Standby queries --- so the original page can't be unlocked before we've - * added the new tuple to the new page. + * be no other update happening, so we don't need to worry about that. + * Notice we also don't worry about this when locking VM buffers above. + * + * But we *do* need to worry that we don't expose an inconsistent state to + * Hot Standby queries --- so the original page can't be unlocked before + * we've added the new tuple to the new page. */ /* Deal with old tuple version */ @@ -793,7 +928,7 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) /* Mark the page as a candidate for pruning */ PageSetPrunable(page, XLogRecGetXid(record)); - if (xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED) + if (old_cleared) PageClearAllVisible(page); PageSetLSN(page, lsn); @@ -819,21 +954,6 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) newaction = XLogReadBufferForRedo(record, HEAP_UPDATE_BLKREF_HEAP_NEW, &nbuffer); - /* - * The visibility map may need to be fixed even if the heap page is - * already up-to-date. - */ - if (xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED) - { - Relation reln = CreateFakeRelcacheEntry(rlocator); - Buffer vmbuffer = InvalidBuffer; - - visibilitymap_pin(reln, newblk, &vmbuffer); - visibilitymap_clear(reln, newblk, vmbuffer, VISIBILITYMAP_VALID_BITS); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); - } - /* Deal with new tuple */ if (newaction == BLK_NEEDS_REDO) { @@ -930,7 +1050,7 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) if (offnum == InvalidOffsetNumber) elog(PANIC, "failed to add tuple"); - if (xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED) + if (new_cleared) PageClearAllVisible(page); freespace = PageGetHeapFreeSpace(page); /* needed to update FSM below */ @@ -1022,20 +1142,15 @@ heap_xlog_lock(XLogReaderState *record) */ if (xlrec->flags & XLH_LOCK_ALL_FROZEN_CLEARED) { - RelFileLocator rlocator; - Buffer vmbuffer = InvalidBuffer; BlockNumber block; - Relation reln; + RelFileLocator rlocator; XLogRecGetBlockTag(record, HEAP_LOCK_BLKREF_HEAP, &rlocator, NULL, &block); - reln = CreateFakeRelcacheEntry(rlocator); - - visibilitymap_pin(reln, block, &vmbuffer); - visibilitymap_clear(reln, block, vmbuffer, VISIBILITYMAP_ALL_FROZEN); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); + heap_xlog_vm_clear(record, rlocator, + block, HEAP_LOCK_BLKREF_VM, + VISIBILITYMAP_ALL_FROZEN); } if (XLogReadBufferForRedo(record, HEAP_LOCK_BLKREF_HEAP, @@ -1100,20 +1215,15 @@ heap_xlog_lock_updated(XLogReaderState *record) */ if (xlrec->flags & XLH_LOCK_ALL_FROZEN_CLEARED) { - RelFileLocator rlocator; - Buffer vmbuffer = InvalidBuffer; BlockNumber block; - Relation reln; + RelFileLocator rlocator; XLogRecGetBlockTag(record, HEAP_LOCK_BLKREF_HEAP, &rlocator, NULL, &block); - reln = CreateFakeRelcacheEntry(rlocator); - - visibilitymap_pin(reln, block, &vmbuffer); - visibilitymap_clear(reln, block, vmbuffer, VISIBILITYMAP_ALL_FROZEN); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); + heap_xlog_vm_clear(record, rlocator, + block, HEAP_LOCK_BLKREF_VM, + VISIBILITYMAP_ALL_FROZEN); } if (XLogReadBufferForRedo(record, HEAP_LOCK_BLKREF_HEAP, diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c index 1874a3fda37..d44620eca7f 100644 --- a/src/backend/access/heap/visibilitymap.c +++ b/src/backend/access/heap/visibilitymap.c @@ -135,9 +135,38 @@ static Buffer vm_extend(Relation rel, BlockNumber vm_nblocks); * You must pass a buffer containing the correct map page to this function. * Call visibilitymap_pin first to pin the right one. This function doesn't do * any I/O. Returns true if any bits have been cleared and false otherwise. + * + * Most callers should use visibilitymap_clear_locked rather than this + * function. It is usually necessary to register the VM buffer in the WAL + * record, and this necessitates holding the lock for longer than it is held + * here. However, we retain this function for behavioral compatibility on the + * back branches (out-of-tree callers may expect it to take the lock). And, + * since we have it, we use it for in-tree callers that don't have to manage + * the VM buffer lock themselves. */ bool visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags) +{ + bool cleared = false; + + LockBuffer(vmbuf, BUFFER_LOCK_EXCLUSIVE); + + cleared = visibilitymap_clear_locked(rel, heapBlk, vmbuf, flags); + + LockBuffer(vmbuf, BUFFER_LOCK_UNLOCK); + + return cleared; +} + +/* + * Clear specified bits, caller holds VM lock + * + * Like visibilitymap_clear(), except the caller must already hold the VM + * buffer exclusive lock and is responsible for unlocking it. + * Returns true if any bits were actually cleared, false otherwise. + */ +bool +visibilitymap_clear_locked(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags) { BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk); int mapByte = HEAPBLK_TO_MAPBYTE(heapBlk); @@ -157,7 +186,8 @@ visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags if (!BufferIsValid(vmbuf) || BufferGetBlockNumber(vmbuf) != mapBlock) elog(ERROR, "wrong buffer passed to visibilitymap_clear"); - LockBuffer(vmbuf, BUFFER_LOCK_EXCLUSIVE); + Assert(BufferIsExclusiveLocked(vmbuf)); + map = PageGetContents(BufferGetPage(vmbuf)); if (map[mapByte] & mask) @@ -168,8 +198,6 @@ visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags cleared = true; } - LockBuffer(vmbuf, BUFFER_LOCK_UNLOCK); - return cleared; } diff --git a/src/bin/pg_walsummary/t/002_blocks.pl b/src/bin/pg_walsummary/t/002_blocks.pl index 0f98c7df82e..fcb701e1b98 100644 --- a/src/bin/pg_walsummary/t/002_blocks.pl +++ b/src/bin/pg_walsummary/t/002_blocks.pl @@ -93,13 +93,14 @@ my $filename = sprintf "%s/pg_wal/summaries/%08s%08s%08s%08s%08s.summary", split(m@/@, $end_lsn); ok(-f $filename, "WAL summary file exists"); -# Run pg_walsummary on it. We expect exactly two blocks to be modified, -# block 0 and one other. +# Run pg_walsummary on it. We expect exactly three blocks to be modified, +# block 0 (old tuple), another block (new tuple), and the block for the VM. my ($stdout, $stderr) = run_command([ 'pg_walsummary', '-i', $filename ]); note($stdout); @lines = split(/\n/, $stdout); like($stdout, qr/FORK main: block 0$/m, "stdout shows block 0 modified"); +like($stdout, qr/FORK vm: block 0$/m, "stdout shows VM block 0 modified"); is($stderr, '', 'stderr is empty'); -is(0 + @lines, 2, "UPDATE modified 2 blocks"); +is(0 + @lines, 3, "UPDATE modified 3 blocks"); done_testing(); diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h index 92744c53899..c4c3c3f15fa 100644 --- a/src/include/access/heapam_xlog.h +++ b/src/include/access/heapam_xlog.h @@ -111,6 +111,7 @@ /* This is what we need to know about delete */ #define HEAP_DELETE_BLKREF_HEAP 0 +#define HEAP_DELETE_BLKREF_VM 1 typedef struct xl_heap_delete { @@ -160,6 +161,7 @@ typedef struct xl_heap_header /* This is what we need to know about insert */ #define HEAP_INSERT_BLKREF_HEAP 0 +#define HEAP_INSERT_BLKREF_VM 1 typedef struct xl_heap_insert { @@ -179,6 +181,7 @@ typedef struct xl_heap_insert * (XLOG_HEAP_INIT_PAGE). */ #define HEAP_MULTI_INSERT_BLKREF_HEAP 0 +#define HEAP_MULTI_INSERT_BLKREF_VM 1 /* * In HEAP_MULTI_INSERT_BLKREF_HEAP's data portion, there is an @@ -222,10 +225,21 @@ typedef struct xl_multi_insert_tuple * * HEAP_UPDATE_BLKREF_HEAP_OLD: old page, if different. (no data, just a reference * to the block) + * + * HEAP_UPDATE_BLKREF_VM_NEW: VM page covering the new heap page. Registered + * when XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED is set and the new heap page's VM + * bit was actually cleared. Also covers the old heap page's VM bits when both + * heap pages map to the same VM page. + * + * HEAP_UPDATE_BLKREF_VM_OLD: VM page covering the old heap page. Only + * registered when XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED is set, the old heap + * page's VM bits are on a different VM page from the new heap page's, and the + * old heap page's VM bit was actually cleared. */ - #define HEAP_UPDATE_BLKREF_HEAP_NEW 0 #define HEAP_UPDATE_BLKREF_HEAP_OLD 1 +#define HEAP_UPDATE_BLKREF_VM_NEW 2 +#define HEAP_UPDATE_BLKREF_VM_OLD 3 typedef struct xl_heap_update { @@ -406,6 +420,7 @@ typedef struct xlhp_prune_items /* This is what we need to know about lock */ #define HEAP_LOCK_BLKREF_HEAP 0 +#define HEAP_LOCK_BLKREF_VM 1 typedef struct xl_heap_lock { diff --git a/src/include/access/visibilitymap.h b/src/include/access/visibilitymap.h index ea889bf9ec7..1bc59c9ac33 100644 --- a/src/include/access/visibilitymap.h +++ b/src/include/access/visibilitymap.h @@ -26,6 +26,8 @@ #define VM_ALL_FROZEN(r, b, v) \ ((visibilitymap_get_status((r), (b), (v)) & VISIBILITYMAP_ALL_FROZEN) != 0) +extern bool visibilitymap_clear_locked(Relation rel, BlockNumber heapBlk, + Buffer vmbuf, uint8 flags); extern bool visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags); extern void visibilitymap_pin(Relation rel, BlockNumber heapBlk, -- 2.47.3
From 2cbd9da9b46f7541032dd000dfb9013fe91ed730 Mon Sep 17 00:00:00 2001 From: Melanie Plageman <[email protected]> Date: Thu, 30 Apr 2026 16:06:06 -0400 Subject: [PATCH v5 3/4] Put pg_combinebackup debug test output in temp file Having --debug as the default option to pg_combinebackup in init_from_backup() makes the logs extremely verbose. It is used for debugging failures, so put the output in a temp file and only keep it around if tests fail. Author: Melanie Plageman <[email protected]> Disussion: https://postgr.es/m/y73coty2smkxgvelh4e32kqrt5pyn2pb7cz7x4mrz6yaxitqd5%40nzhkeazqf4bn Backpatch-through: 17 --- src/test/perl/PostgreSQL/Test/Cluster.pm | 83 +++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm index 2e31c23a6ac..3388e45b726 100644 --- a/src/test/perl/PostgreSQL/Test/Cluster.pm +++ b/src/test/perl/PostgreSQL/Test/Cluster.pm @@ -125,6 +125,8 @@ our $min_compat = 12; # list of file reservations made by get_free_port my @port_reservation_files; +my @combinebackup_debug_logs; +my %combinebackup_debug_logs_printed; # We want to choose a server port above the range that servers typically use # on Unix systems and below the range those systems typically use for ephemeral @@ -898,7 +900,9 @@ pathnames that should be used for the target cluster. To restore from an incremental backup, pass the parameter combine_with_prior as a reference to an array of prior backup names with which this backup -is to be combined using pg_combinebackup. +is to be combined using pg_combinebackup. pg_combinebackup is run with +--debug, but the output is captured in a temporary file and only copied to the +test log if pg_combinebackup fails or if a later test fails. Streaming replication can be enabled on this node by passing the keyword parameter has_streaming => 1. This is disabled by default. @@ -964,7 +968,7 @@ sub init_from_backup } push @combineargs, @prior_backup_path, $backup_path, '--output' => $data_path; - PostgreSQL::Test::Utils::system_or_bail(@combineargs); + _run_pg_combinebackup_with_debug_log(@combineargs); } elsif (defined $params{tar_program}) { @@ -1079,6 +1083,81 @@ port = $port return; } +sub _run_pg_combinebackup_with_debug_log +{ + my (@cmd) = @_; + my $debug_log = PostgreSQL::Test::Utils::tempdir('pg_combinebackup') . + '/debug.log'; + + push @combinebackup_debug_logs, $debug_log; + + print("# Running: " . join(" ", @cmd) . "\n"); + # If this pg_combinebackup invocation itself fails, print its captured + # output immediately. Successful invocations may still be printed later if + # some subsequent TAP assertion in this test script fails. + if (!IPC::Run::run(\@cmd, '&>' => $debug_log)) + { + _print_pg_combinebackup_debug_log($debug_log); + + if ($? == -1) + { + BAIL_OUT( + sprintf( + "failed to execute command \"%s\": $!", join(" ", @cmd))); + } + elsif ($? & 127) + { + BAIL_OUT( + sprintf( + "command \"%s\" died with signal %d", + join(" ", @cmd), $? & 127)); + } + else + { + BAIL_OUT( + sprintf( + "command \"%s\" exited with value %d", + join(" ", @cmd), $? >> 8)); + } + } +} + +sub _print_pg_combinebackup_debug_log +{ + my ($debug_log) = @_; + + return unless -e $debug_log; + return if $combinebackup_debug_logs_printed{$debug_log}++; + + diag("pg_combinebackup debug output from $debug_log:\n" . + PostgreSQL::Test::Utils::slurp_file($debug_log)); +} + +# Keep pg_combinebackup debug logs until test exit. If pg_combinebackup itself +# fails, its log is printed at the failure site above. If pg_combinebackup +# succeeds but a later TAP assertion fails, print the retained logs here so the +# regress output includes the combine step that produced the restored state. +# Remove the logs after a clean run. +END +{ + # take care not to change the script's exit value + my $exit_code = $?; + + if ($exit_code == 0 && PostgreSQL::Test::Utils::all_tests_passing()) + { + unlink @combinebackup_debug_logs; + } + else + { + foreach my $debug_log (@combinebackup_debug_logs) + { + _print_pg_combinebackup_debug_log($debug_log); + } + } + + $? = $exit_code; +} + =pod =item $node->rotate_logfile() -- 2.47.3
From 414cf30429bc77429a26e267906f56b6a51cc463 Mon Sep 17 00:00:00 2001 From: Melanie Plageman <[email protected]> Date: Thu, 23 Apr 2026 11:13:23 -0400 Subject: [PATCH v5 4/4] Test that VM clear registers VM buffers The WAL summarizer only tracks registered buffers, so unregistered VM clears are ommitted from incremental backups, corrupting the restored visibility map. Test those cases are now fixed. Author: Melanie Plageman <[email protected]> Reviewed-by: Andrey Borodin <[email protected]> Discussion: https://postgr.es/m/oqcsevg35xjan2327x5kdfth6q4fgeqboxfo3v3imeyih2uiny%406sez5dzxl6nt Backpatch-through: 17 --- src/bin/pg_combinebackup/Makefile | 2 + src/bin/pg_combinebackup/meson.build | 1 + .../pg_combinebackup/t/012_vm_consistency.pl | 258 ++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 src/bin/pg_combinebackup/t/012_vm_consistency.pl diff --git a/src/bin/pg_combinebackup/Makefile b/src/bin/pg_combinebackup/Makefile index 33a1f4483bf..bdba0353412 100644 --- a/src/bin/pg_combinebackup/Makefile +++ b/src/bin/pg_combinebackup/Makefile @@ -12,6 +12,8 @@ PGFILEDESC = "pg_combinebackup - combine incremental backups" PGAPPICON=win32 +EXTRA_INSTALL=contrib/pg_visibility + subdir = src/bin/pg_combinebackup top_builddir = ../../.. include $(top_builddir)/src/Makefile.global diff --git a/src/bin/pg_combinebackup/meson.build b/src/bin/pg_combinebackup/meson.build index bbc4c5735ba..757b78e2fe8 100644 --- a/src/bin/pg_combinebackup/meson.build +++ b/src/bin/pg_combinebackup/meson.build @@ -39,6 +39,7 @@ tests += { 't/009_no_full_file.pl', 't/010_hardlink.pl', 't/011_ib_truncation.pl', + 't/012_vm_consistency.pl', ], } } diff --git a/src/bin/pg_combinebackup/t/012_vm_consistency.pl b/src/bin/pg_combinebackup/t/012_vm_consistency.pl new file mode 100644 index 00000000000..b0810f8022a --- /dev/null +++ b/src/bin/pg_combinebackup/t/012_vm_consistency.pl @@ -0,0 +1,258 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group +# +# Test that heap operations clearing visibility map bits (INSERT, UPDATE, +# DELETE, SELECT FOR UPDATE, COPY) correctly register visibility map buffers, +# since incremental backups rely on the WAL summarizer, which only tracks +# registered buffers. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $tempdir = PostgreSQL::Test::Utils::tempdir_short(); +my $mode = $ENV{PG_TEST_PG_COMBINEBACKUP_MODE} || '--copy'; + +# Set up primary with WAL summarization enabled. +my $primary = PostgreSQL::Test::Cluster->new('primary'); +$primary->init(allows_streaming => 1); +$primary->append_conf('postgresql.conf', <<EOF); +summarize_wal = on +autovacuum = off +EOF +$primary->start; + +$primary->safe_psql('postgres', q{CREATE EXTENSION pg_visibility}); + +my @tests = ( + { + label => 'INSERT', + table => 'vm_insert_test', + setup => q{CREATE TABLE vm_insert_test (id int); + INSERT INTO vm_insert_test DEFAULT VALUES;}, + modify => q{INSERT INTO vm_insert_test VALUES (1)}, + visible_op => '<', + frozen_op => '<', + }, + { + label => 'DELETE', + table => 'vm_delete_test', + setup => q{CREATE TABLE vm_delete_test (id int); + INSERT INTO vm_delete_test VALUES (1), (2);}, + modify => q{DELETE FROM vm_delete_test WHERE id = 1}, + visible_op => '<', + frozen_op => '<', + }, + { + label => 'UPDATE', + table => 'vm_update_test', + # Include both same-page and cross-page updates. val is stored PLAIN + # so the large update stays inline. + setup => q{CREATE TABLE vm_update_test (id INT, val TEXT); + ALTER TABLE vm_update_test ALTER COLUMN val SET STORAGE PLAIN; + INSERT INTO vm_update_test VALUES (1, 'same page'), (2, 'cross page'); + INSERT INTO vm_update_test SELECT i, repeat('a', 200) + FROM generate_series(3, 70) i;}, + modify => q{UPDATE vm_update_test SET id = 0 WHERE id = 1; + UPDATE vm_update_test SET val = repeat('b', 4000) WHERE id = 2;}, + # Confirm the small update stays on the same heap page while the large + # update relocates the tuple to a different heap page. + ctid_checks => [ + { + before => 'id = 1', + after => 'id = 0', + same => 1, + desc => 'small update stays on the same heap page', + }, + { + before => 'id = 2', + after => 'id = 2', + same => 0, + desc => 'large update moves the tuple to a different heap page', + }, + ], + visible_op => '<', + frozen_op => '<', + }, + { + label => 'LOCK', + table => 'vm_lock_test', + setup => q{CREATE TABLE vm_lock_test (id int); + INSERT INTO vm_lock_test VALUES (1), (2);}, + modify => q{SELECT * FROM vm_lock_test WHERE id = 1 FOR UPDATE}, + visible_op => '==', + frozen_op => '<', + }, + { + label => 'COPY', + table => 'vm_copy_test', + setup => q{CREATE TABLE vm_copy_test (id int); + INSERT INTO vm_copy_test DEFAULT VALUES;}, + modify => q{COPY vm_copy_test FROM PROGRAM 'echo 42'}, + visible_op => '<', + frozen_op => '<', + }, +); + +sub get_vm_summary +{ + my ($node, $table) = @_; + my $result = $node->safe_psql('postgres', + "SELECT all_visible, all_frozen FROM pg_visibility_map_summary('$table')"); + my @vals = split(/\|/, $result); + return @vals; +} + +# Return the heap block number of the (single) row matching $where. The ctid +# is "(block,offset)"; casting it through point lets us pull out the block. +sub heap_block +{ + my ($node, $table, $where) = @_; + return $node->safe_psql('postgres', + "SELECT (ctid::text::point)[0]::int FROM $table WHERE $where"); +} + +# Confirm VACUUM (FREEZE) set VM bits before testing whether later heap +# modifications clear those bits and are captured by incremental backup. We +# could perhaps be more exact than > 0, but the coarseness attempts to avoid +# tset flakes. +sub check_vacuumed_vm +{ + my ($node, $test) = @_; + my ($all_visible, $all_frozen) = get_vm_summary($node, $test->{table}); + + cmp_ok($all_visible, '>', 0, + "$test->{label} test: pages are all-visible after vacuum"); + cmp_ok($all_frozen, '>', 0, + "$test->{label} test: pages are all-frozen after vacuum"); + + return ($all_visible, $all_frozen); +} + +# Check the VM bit counts after a heap modification against the post-vacuum +# baseline. Most operations clear both bits; tuple locking clears all-frozen +# without clearing all-visible. +sub check_modified_vm +{ + my ($node, $test) = @_; + my ($post_visible, $post_frozen) = get_vm_summary($node, $test->{table}); + + cmp_ok($post_visible, $test->{visible_op}, $test->{pre_visible}, + "$test->{label} test: all-visible state after modification"); + cmp_ok($post_frozen, $test->{frozen_op}, $test->{pre_frozen}, + "$test->{label} test: all-frozen state after modification"); + + return ($post_visible, $post_frozen); +} + +# Verify the combined backup restored VM state exactly as it exists on the +# primary, and ask pg_visibility to check that visible tuples are consistent. +sub validate_restored_vm +{ + my ($restored, $test) = @_; + + my ($primary_visible, $primary_frozen) = + get_vm_summary($primary, $test->{table}); + my ($restored_visible, $restored_frozen) = + get_vm_summary($restored, $test->{table}); + + is($restored_visible, $primary_visible, + "$test->{label} test: restored all_visible count matches primary"); + is($restored_frozen, $primary_frozen, + "$test->{label} test: restored all_frozen count matches primary"); + + my $corrupt_tids = $restored->safe_psql('postgres', + "SELECT count(*) FROM pg_check_visible('$test->{table}')"); + is($corrupt_tids, '0', + "$test->{label} test: no VM corruption detected by pg_check_visible"); +} + +# Create and populate the tables, then vacuum freeze them to set the VM bits +foreach my $test (@tests) +{ + $primary->safe_psql('postgres', $test->{setup}); + $primary->safe_psql('postgres', "VACUUM (FREEZE) $test->{table}"); + ($test->{pre_visible}, $test->{pre_frozen}) = + check_vacuumed_vm($primary, $test); +} + +# Take a full backup +my $full_name = 'full'; +my $full_path = $primary->backup_dir . "/$full_name"; +$primary->command_ok( + [ + 'pg_basebackup', '--no-sync', + '--pgdata' => $full_path, + '--checkpoint' => 'fast', + ], + 'full backup'); + +# Modify the tables and check that the VM bits are as expected for that test +# after the specified modification. +foreach my $test (@tests) +{ + # Record the heap block of any rows whose same-/cross-page movement we + # want to verify, before the modification relocates them. + foreach my $check (@{ $test->{ctid_checks} // [] }) + { + $check->{before_blk} = + heap_block($primary, $test->{table}, $check->{before}); + } + + $primary->safe_psql('postgres', $test->{modify}); + ($test->{post_visible}, $test->{post_frozen}) = + check_modified_vm($primary, $test); + + # Confirm each checked row did (or did not) move to a different heap page. + foreach my $check (@{ $test->{ctid_checks} // [] }) + { + my $after_blk = heap_block($primary, $test->{table}, $check->{after}); + if ($check->{same}) + { + is($after_blk, $check->{before_blk}, + "$test->{label} test: $check->{desc}"); + } + else + { + isnt($after_blk, $check->{before_blk}, + "$test->{label} test: $check->{desc}"); + } + } +} + +# Take an incremental backup. This will have the changes made in the +# modification step. +my $incr_name = 'incr'; +my $incr_path = $primary->backup_dir . "/$incr_name"; +$primary->command_ok( + [ + 'pg_basebackup', '--no-sync', + '--pgdata' => $incr_path, + '--checkpoint' => 'fast', + '--incremental' => $full_path . '/backup_manifest', + ], + 'incremental backup'); + +# Start a server from a combined backup composed of the incremental and full +# backup. +my $restored = PostgreSQL::Test::Cluster->new('restored'); +$restored->init_from_backup($primary, $incr_name, + combine_with_prior => [$full_name], + combine_mode => $mode); +$restored->append_conf('postgresql.conf', <<EOF); +autovacuum = off +EOF +$restored->start; +$restored->safe_psql('postgres', q{CREATE EXTENSION IF NOT EXISTS pg_visibility}); + +# Confirm that the restored server's visibility map matches the original server +foreach my $test (@tests) +{ + validate_restored_vm($restored, $test); +} + +$restored->stop; +$primary->stop; + +done_testing(); -- 2.47.3
From 10739902aaafa214e4a9df161bea280a7139d34b Mon Sep 17 00:00:00 2001 From: Melanie Plageman <[email protected]> Date: Tue, 14 Jul 2026 14:24:16 -0400 Subject: [PATCH] heap_update --- src/backend/access/heap/heapam.c | 57 ++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index fd8ca72728f..e445ea35096 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -4270,38 +4270,45 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, * cases, it's possible that PD_ALL_VISIBLE was set but the corresponding * visibility map bits were already clear. */ - if (clear_all_visible) - { - if (visibilitymap_clear_locked(relation, block, - vmbuffer, VISIBILITYMAP_VALID_BITS)) - { - /* - * When old and new heap blocks' VM bits are on the same VM page, - * that page is registered in the WAL record only once. If both - * heap pages were PD_ALL_VISIBLE and either VM bit needs - * clearing, we register the VM buffer as - * HEAP_UPDATE_BLKREF_VM_NEW. - */ - if (clear_all_visible_new && vmbuffer == vmbuffer_new) - vmbuffer_new_modified = true; - else - vmbuffer_modified = true; - } - - PageClearAllVisible(page); - } - if (clear_all_visible_new) + if (clear_all_visible && clear_all_visible_new && + vmbuffer_new == vmbuffer) { /* - * If both heap blocks' VM bits are on the same VM buffer, this will - * clear the new heap block's VM bits from the shared vmbuffer. + * When old and new heap blocks' VM bits are on the same VM page, that + * page is registered in the WAL record only once. If both heap pages + * were PD_ALL_VISIBLE and either VM bit needs clearing, we register + * the VM buffer as HEAP_UPDATE_BLKREF_VM_NEW. */ - if (visibilitymap_clear_locked(relation, BufferGetBlockNumber(newbuf), - vmbuffer_new, VISIBILITYMAP_VALID_BITS)) + if (visibilitymap_clear(relation, block, + vmbuffer_new, VISIBILITYMAP_VALID_BITS)) + vmbuffer_new_modified = true; + + if (visibilitymap_clear(relation, BufferGetBlockNumber(newbuf), + vmbuffer_new, VISIBILITYMAP_VALID_BITS)) vmbuffer_new_modified = true; + PageClearAllVisible(page); PageClearAllVisible(newpage); } + else + { + if (clear_all_visible) + { + if (visibilitymap_clear(relation, block, + vmbuffer, VISIBILITYMAP_VALID_BITS)) + vmbuffer_modified = true; + + PageClearAllVisible(page); + } + if (clear_all_visible_new) + { + if (visibilitymap_clear(relation, BufferGetBlockNumber(newbuf), + vmbuffer_new, VISIBILITYMAP_VALID_BITS)) + vmbuffer_new_modified = true; + + PageClearAllVisible(newpage); + } + } if (newbuf != buffer) MarkBufferDirty(newbuf); -- 2.47.3
