]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix VM clear WAL logging by registering VM blocks
authorMelanie Plageman <melanieplageman@gmail.com>
Wed, 15 Jul 2026 21:32:17 +0000 (17:32 -0400)
committerMelanie Plageman <melanieplageman@gmail.com>
Wed, 15 Jul 2026 21:32:17 +0000 (17:32 -0400)
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.

Bumps XLOG_PAGE_MAGIC. Though it is late in the cycle (post-beta 2) to
be doing so, that seemed better than maintaining the backwards
compatability code in yet another branch.

Author: Melanie Plageman <melanieplageman@gmail.com>
Author: Andres Freund <andres@anarazel.de>
Reviewed-by: Robert Haas <robertmhaas@gmail.com>
Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru>
Discussion: https://postgr.es/m/flat/CAAKRu_bn%2Be7F4yPFBgFbnP%2BsyJRKyNK092bjD2LKvZW7O4Svag>
Backpatch-through: 17

contrib/pg_surgery/heap_surgery.c
src/backend/access/heap/heapam.c
src/backend/access/heap/heapam_xlog.c
src/backend/access/heap/pruneheap.c
src/backend/access/heap/visibilitymap.c
src/bin/pg_walsummary/t/002_blocks.pl
src/include/access/heapam_xlog.h
src/include/access/xlog_internal.h

index b8ce10957827d449f05d4d7520e33d3035ee84d7..a10876cb800cdeadf0a83c65ba345bec647b151f 100644 (file)
@@ -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(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. */
index 4594e746c67fe954b94c0aed55e6bc20eebdaf5e..182740c818393bee2f8e68ae9f21ce6593e4ee65 100644 (file)
@@ -60,7 +60,8 @@
 static HeapTuple heap_prepare_insert(Relation relation, HeapTuple tup,
                                                                         TransactionId xid, CommandId cid, uint32 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,
                                                                  bool walLogical);
@@ -2009,7 +2010,8 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid,
        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) <=
@@ -2053,19 +2055,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(page))
+       if (clear_all_visible)
        {
-               all_visible_cleared = true;
+               /* It's possible the VM bits were already clear */
+               if (visibilitymap_clear(relation,
+                                                               ItemPointerGetBlockNumber(&(heaptup->t_self)),
+                                                               vmbuffer, VISIBILITYMAP_VALID_BITS))
+                       vmbuffer_modified = true;
+
                PageClearAllVisible(page);
-               visibilitymap_clear(relation,
-                                                       ItemPointerGetBlockNumber(&(heaptup->t_self)),
-                                                       vmbuffer, VISIBILITYMAP_VALID_BITS);
        }
 
        /*
@@ -2116,7 +2127,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;
@@ -2161,15 +2172,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);
 
        /*
@@ -2350,8 +2374,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();
@@ -2396,6 +2421,12 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples,
                        /* Lock the vmbuffer before entering the critical section */
                        LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE);
                }
+               else if (PageIsAllVisible(page) && !(options & HEAP_INSERT_FROZEN))
+               {
+                       clear_all_visible = true;
+                       /* Lock the vmbuffer before entering the critical section */
+                       LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE);
+               }
 
                /* NO EREPORT(ERROR) from here till changes are logged */
                START_CRIT_SECTION();
@@ -2438,13 +2469,16 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples,
                 * page, mark it as all-frozen and update the visibility map. We're
                 * already holding a pin on the vmbuffer.
                 */
-               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(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)
                {
@@ -2502,10 +2536,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;
 
                        /*
@@ -2577,7 +2611,7 @@ 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 (all_frozen_set)
+                       if (all_frozen_set || vmbuffer_modified)
                                XLogRegisterBuffer(HEAP_MULTI_INSERT_BLKREF_VM, vmbuffer, 0);
 
                        XLogRegisterBufData(HEAP_MULTI_INSERT_BLKREF_HEAP, tupledata,
@@ -2589,7 +2623,7 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples,
                        recptr = XLogInsert(RM_HEAP2_ID, info);
 
                        PageSetLSN(page, recptr);
-                       if (all_frozen_set)
+                       if (all_frozen_set || vmbuffer_modified)
                        {
                                Assert(BufferIsDirty(vmbuffer));
                                PageSetLSN(BufferGetPage(vmbuffer), recptr);
@@ -2598,7 +2632,11 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples,
 
                END_CRIT_SECTION();
 
-               if (all_frozen_set)
+               /*
+                * We locked vmbuffer if clear_all_visible was true regardless of
+                * whether or not we ended up modifying the vmbuffer.
+                */
+               if (all_frozen_set || clear_all_visible)
                        LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK);
 
                UnlockReleaseBuffer(buffer);
@@ -2730,6 +2768,7 @@ heap_delete(Relation relation, const ItemPointerData *tid,
        BlockNumber block;
        Buffer          buffer;
        Buffer          vmbuffer = InvalidBuffer;
+       bool            vmbuffer_modified = false;
        TransactionId new_xmax;
        uint16          new_infomask,
                                new_infomask2;
@@ -2737,7 +2776,7 @@ heap_delete(Relation relation, const ItemPointerData *tid,
        bool            walLogical = (options & TABLE_DELETE_NO_LOGICAL) == 0;
        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;
 
@@ -2987,6 +3026,13 @@ l1:
                                                          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();
 
        /*
@@ -2998,12 +3044,14 @@ l1:
         */
        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(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 */
@@ -3043,7 +3091,7 @@ l1:
                        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;
@@ -3094,13 +3142,27 @@ l1:
                /* 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)
@@ -3229,6 +3291,8 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
                                newbuf,
                                vmbuffer = InvalidBuffer,
                                vmbuffer_new = InvalidBuffer;
+       bool            unlock_vmbuffer = false;
+       bool            unlock_vmbuffer_new = false;
        bool            need_toast;
        Size            newtupsize,
                                pagefree;
@@ -3237,8 +3301,10 @@ heap_update(Relation relation, const ItemPointerData *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;
@@ -3813,6 +3879,12 @@ l2:
 
                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 ... */
@@ -3835,10 +3907,13 @@ l2:
                 * 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(relation, block, vmbuffer,
+                                                                       VISIBILITYMAP_ALL_FROZEN))
+                               cleared_all_frozen = true;
+               }
 
                MarkBufferDirty(buffer);
 
@@ -3857,12 +3932,24 @@ l2:
                        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);
 
                /*
@@ -4013,6 +4100,69 @@ l2:
                                                                                   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();
 
@@ -4063,20 +4213,42 @@ l2:
        /* 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(page))
+       /*
+        * 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;
+               if (visibilitymap_clear(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);
-               visibilitymap_clear(relation, BufferGetBlockNumber(buffer),
-                                                       vmbuffer, VISIBILITYMAP_VALID_BITS);
        }
-       if (newbuf != buffer && PageIsAllVisible(newpage))
+       if (clear_all_visible_new)
        {
-               all_visible_cleared_new = true;
+               /*
+                * 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(relation, BufferGetBlockNumber(newbuf),
+                                                               vmbuffer_new, VISIBILITYMAP_VALID_BITS))
+                       vmbuffer_new_modified = true;
+
                PageClearAllVisible(newpage);
-               visibilitymap_clear(relation, BufferGetBlockNumber(newbuf),
-                                                       vmbuffer_new, VISIBILITYMAP_VALID_BITS);
        }
 
        if (newbuf != buffer)
@@ -4099,20 +4271,33 @@ l2:
                }
 
                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,
                                                                 walLogical);
                if (newbuf != buffer)
                {
                        PageSetLSN(newpage, recptr);
                }
                PageSetLSN(page, 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);
@@ -4550,6 +4735,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple,
        ItemId          lp;
        Page            page;
        Buffer          vmbuffer = InvalidBuffer;
+       bool            unlock_vmbuffer = false;
        BlockNumber block;
        TransactionId xid,
                                xmax;
@@ -4563,6 +4749,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
@@ -4570,12 +4757,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));
 
@@ -5130,6 +5316,13 @@ failed:
                                                          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();
 
        /*
@@ -5161,11 +5354,12 @@ failed:
                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))
+       {
+               if (visibilitymap_clear(relation, block, vmbuffer,
+                                                               VISIBILITYMAP_ALL_FROZEN))
+                       cleared_all_frozen = true;
+       }
 
        MarkBufferDirty(*buffer);
 
@@ -5196,19 +5390,33 @@ failed:
                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))
@@ -5671,6 +5879,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,
@@ -5680,6 +5889,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);
@@ -5688,6 +5898,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));
 
@@ -5707,13 +5918,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;
@@ -5734,7 +5947,7 @@ l4:
                 * 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);
@@ -5915,10 +6128,11 @@ l4:
                                                                  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();
 
@@ -5931,12 +6145,19 @@ l4:
 
                MarkBufferDirty(buf);
 
+               if (PageIsAllVisible(page))
+               {
+                       /* It's possible all-frozen was already clear */
+                       if (visibilitymap_clear(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);
@@ -5949,13 +6170,26 @@ l4:
 
                        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 ||
@@ -5981,6 +6215,7 @@ out_locked:
 out_unlocked:
        if (vmbuffer != InvalidBuffer)
                ReleaseBuffer(vmbuffer);
+       Assert(!unlock_vmbuffer);
 
        return result;
 }
@@ -8776,8 +9011,9 @@ bottomup_sort_and_shrink(TM_IndexDeleteOp *delstate)
  * 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,
                                bool walLogical)
@@ -8987,6 +9223,21 @@ 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 and both their VM bits were cleared, 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);
 
index 963a886a1b366dd24d8cda0cb0d5f447a6295246..fbda931522cbc4f118e481c7479b661ee9ec8361 100644 (file)
 #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.
+        */
+       if (XLogRecHasBlockRef(record, wal_vm_block_id))
+       {
+               if (XLogReadBufferForRedo(record, wal_vm_block_id,
+                                                                 &vmbuffer) == BLK_NEEDS_REDO)
+               {
+                       if (visibilitymap_clear(reln, heap_blkno, vmbuffer, flags))
+                               PageSetLSN(BufferGetPage(vmbuffer), lsn);
+               }
+               if (BufferIsValid(vmbuffer))
+                       UnlockReleaseBuffer(vmbuffer);
+       }
+
+       FreeFakeRelcacheEntry(reln);
+}
 
 /*
  * Replay XLOG_HEAP2_PRUNE_* records.
@@ -309,15 +352,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)
@@ -398,15 +435,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
@@ -530,17 +561,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);
-
-               visibilitymap_pin(reln, blkno, &vmbuffer);
-               visibilitymap_clear(reln, blkno, vmbuffer, VISIBILITYMAP_VALID_BITS);
-               ReleaseBuffer(vmbuffer);
-               vmbuffer = InvalidBuffer;
-               FreeFakeRelcacheEntry(reln);
-       }
+               heap_xlog_vm_clear(record, rlocator,
+                                                  blkno, HEAP_MULTI_INSERT_BLKREF_VM,
+                                                  VISIBILITYMAP_VALID_BITS);
 
        if (isinit)
        {
@@ -640,7 +669,7 @@ heap_xlog_multi_insert(XLogReaderState *record)
        buffer = InvalidBuffer;
 
        /*
-        * Read and update the visibility map (VM) block.
+        * Read and update the visibility map (VM) block to set it frozen.
         *
         * We must always redo VM changes, even if the corresponding heap page
         * update was skipped due to the LSN interlock. Each VM block covers
@@ -713,6 +742,8 @@ heap_xlog_update(XLogReaderState *record, bool hot_update)
                                nbuffer;
        Page            opage,
                                npage;
+       bool            has_vm_old,
+                               has_vm_new;
        OffsetNumber offnum;
        ItemId          lp;
        HeapTupleData oldtup;
@@ -730,6 +761,7 @@ heap_xlog_update(XLogReaderState *record, bool hot_update)
        Size            freespace = 0;
        XLogRedoAction oldaction;
        XLogRedoAction newaction;
+       Relation        reln = NULL;
 
        /* initialize to keep the compiler quiet */
        oldtup.t_data = NULL;
@@ -752,25 +784,80 @@ 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)
+       has_vm_old = XLogRecHasBlockRef(record, HEAP_UPDATE_BLKREF_VM_OLD);
+       has_vm_new = XLogRecHasBlockRef(record, HEAP_UPDATE_BLKREF_VM_NEW);
+
+       if (has_vm_new || has_vm_old)
+               reln = CreateFakeRelcacheEntry(rlocator);
+
+       if (has_vm_new)
        {
-               Relation        reln = CreateFakeRelcacheEntry(rlocator);
-               Buffer          vmbuffer = InvalidBuffer;
+               Buffer          vmbuffer_new = InvalidBuffer;
 
-               visibilitymap_pin(reln, oldblk, &vmbuffer);
-               visibilitymap_clear(reln, oldblk, vmbuffer, VISIBILITYMAP_VALID_BITS);
-               ReleaseBuffer(vmbuffer);
-               FreeFakeRelcacheEntry(reln);
+               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(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(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(reln, oldblk, vmbuffer_old,
+                                                                       VISIBILITYMAP_VALID_BITS))
+                               PageSetLSN(BufferGetPage(vmbuffer_old), lsn);
+               }
+               if (BufferIsValid(vmbuffer_old))
+                       UnlockReleaseBuffer(vmbuffer_old);
+       }
+
+       if (reln)
+               FreeFakeRelcacheEntry(reln);
 
        /*
         * 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 */
@@ -834,21 +921,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)
        {
@@ -1041,19 +1113,14 @@ heap_xlog_lock(XLogReaderState *record)
        if (xlrec->flags & XLH_LOCK_ALL_FROZEN_CLEARED)
        {
                RelFileLocator rlocator;
-               Buffer          vmbuffer = InvalidBuffer;
                BlockNumber block;
-               Relation        reln;
 
                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,
@@ -1119,19 +1186,14 @@ heap_xlog_lock_updated(XLogReaderState *record)
        if (xlrec->flags & XLH_LOCK_ALL_FROZEN_CLEARED)
        {
                RelFileLocator rlocator;
-               Buffer          vmbuffer = InvalidBuffer;
                BlockNumber block;
-               Relation        reln;
 
                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,
index 6f3ba9113b5223cdbc345748416e395af37bc794..f00c9b81c1a2686d3d683c187d3d6550ff7ae1ea 100644 (file)
@@ -947,9 +947,11 @@ heap_page_fix_vm_corruption(PruneState *prstate, OffsetNumber offnum,
 
        if (do_clear_vm)
        {
+               LockBuffer(prstate->vmbuffer, BUFFER_LOCK_EXCLUSIVE);
                visibilitymap_clear(prstate->relation, prstate->block,
                                                        prstate->vmbuffer,
                                                        VISIBILITYMAP_VALID_BITS);
+               LockBuffer(prstate->vmbuffer, BUFFER_LOCK_UNLOCK);
                prstate->old_vmbits = 0;
        }
 }
index 4fd470702aae70661474340cdba8f50a7d261d52..c382b25e192cae5c38026c32ebb7daf94c8ae2e7 100644 (file)
 static Buffer vm_readbuf(Relation rel, BlockNumber blkno, bool extend);
 static Buffer vm_extend(Relation rel, BlockNumber vm_nblocks);
 
-
 /*
  *     visibilitymap_clear - clear specified bits for one page in visibility map
  *
- * 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.
+ * You must pass a buffer containing the correct map page to this function,
+ * which already needs to be pinned and locked exclusively.
+ *
+ * This function doesn't do any I/O. Returns true if any bits have been
+ * cleared and false otherwise.
  */
 bool
 visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags)
 {
-       BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk);
        int                     mapByte = HEAPBLK_TO_MAPBYTE(heapBlk);
        int                     mapOffset = HEAPBLK_TO_OFFSET(heapBlk);
+#ifdef USE_ASSERT_CHECKING
+       BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk);
+#endif
        uint8           mask = flags << mapOffset;
+       Page            page;
        char       *map;
        bool            cleared = false;
 
@@ -165,11 +169,11 @@ visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags
        elog(DEBUG1, "vm_clear %s %d", RelationGetRelationName(rel), heapBlk);
 #endif
 
-       if (!BufferIsValid(vmbuf) || BufferGetBlockNumber(vmbuf) != mapBlock)
-               elog(ERROR, "wrong buffer passed to visibilitymap_clear");
+       Assert(BufferIsValid(vmbuf) && BufferGetBlockNumber(vmbuf) == mapBlock);
+       Assert(BufferIsLockedByMeInMode(vmbuf, BUFFER_LOCK_EXCLUSIVE));
 
-       LockBuffer(vmbuf, BUFFER_LOCK_EXCLUSIVE);
-       map = PageGetContents(BufferGetPage(vmbuf));
+       page = BufferGetPage(vmbuf);
+       map = PageGetContents(page);
 
        if (map[mapByte] & mask)
        {
@@ -179,8 +183,6 @@ visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags
                cleared = true;
        }
 
-       LockBuffer(vmbuf, BUFFER_LOCK_UNLOCK);
-
        return cleared;
 }
 
index f5fe94f9f15a46bf08c3053e94777da49e6db7e0..bebc1137c0f18615581df2853291e2a3fe542113 100644 (file)
@@ -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();
index fedee7f3501ddd90dcaa0c770246013d32467fa1..3f79c389a90a7ed66bebf787c6fa62616481d38b 100644 (file)
 
 /* 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
 {
@@ -162,6 +163,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
 {
@@ -225,10 +227,22 @@ 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 and both blocks' VM bits were actually cleared.
+ *
+ * HEAP_UPDATE_BLKREF_VM_OLD: VM page covering the old heap page. Only
+ * registered when XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED is set and the old heap
+ * page's VM bits were actually cleared. Also only registered when the old heap
+ * page's VM bits are on a different VM page than the new heap page's or they
+ * are on the same VM page and only the old block's VM bits are 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
 {
@@ -417,6 +431,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
 {
index 55663e6f4afabf179a67c223c5e4785c59196a32..be718993401bcf696ba0fce6df2a0f8acbd1cc91 100644 (file)
@@ -32,7 +32,7 @@
 /*
  * Each page of XLOG file has a header like this:
  */
-#define XLOG_PAGE_MAGIC 0xD120 /* can be used as WAL version indicator */
+#define XLOG_PAGE_MAGIC 0xD121 /* can be used as WAL version indicator */
 
 typedef struct XLogPageHeaderData
 {