Re: PANIC: wrong buffer passed to visibilitymap_clear - Mailing list pgsql-hackers

From Tom Lane
Subject Re: PANIC: wrong buffer passed to visibilitymap_clear
Date
Msg-id 2902168.1618244388@sss.pgh.pa.us
Whole thread Raw
In response to Re: PANIC: wrong buffer passed to visibilitymap_clear  (Peter Geoghegan <pg@bowt.ie>)
Responses Re: PANIC: wrong buffer passed to visibilitymap_clear  (Peter Geoghegan <pg@bowt.ie>)
List pgsql-hackers
Peter Geoghegan <pg@bowt.ie> writes:
> On Sun, Apr 11, 2021 at 11:16 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
>> It wasn't very clear, because I hadn't thought it through very much;
>> but what I'm imagining is that we discard most of the thrashing around
>> all-visible rechecks and have just one such test somewhere very late
>> in heap_update, after we've successfully acquired a target buffer for
>> the update and are no longer going to possibly need to release any
>> buffer lock.  If at that one point we see the page is all-visible
>> and we don't have the vmbuffer, then we have to release all our locks
>> and go back to "l2".  Which is less efficient than some of the existing
>> code paths, but given how hard this problem is to reproduce, it seems
>> clear that optimizing for the occurrence is just not worth it.

> Oh! That sounds way better.

After poking at this for awhile, it seems like it won't work very nicely.
The problem is that once we've invoked the toaster, we really don't want
to just abandon that work; we'd leak any toasted out-of-line data that
was created.

So I think we have to stick with the current basic design, and just
tighten things up to make sure that visibility pins are accounted for
in the places that are missing it.

Hence, I propose the attached.  It passes check-world, but that proves
absolutely nothing of course :-(.  I wonder if there is any way to
exercise these code paths deterministically.

(I have realized BTW that I was exceedingly fortunate to reproduce
the buildfarm report here --- I have run hundreds of additional
cycles of the same test scenario without getting a second failure.)

            regards, tom lane

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 03d4abc938..265b31e981 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -3784,7 +3784,7 @@ l2:
          * overhead would be unchanged, that doesn't seem necessarily
          * worthwhile.
          */
-        if (PageIsAllVisible(BufferGetPage(buffer)) &&
+        if (PageIsAllVisible(page) &&
             visibilitymap_clear(relation, block, vmbuffer,
                                 VISIBILITYMAP_ALL_FROZEN))
             cleared_all_frozen = true;
@@ -3846,36 +3846,46 @@ l2:
          * first".  To implement this, we must do RelationGetBufferForTuple
          * while not holding the lock on the old page, and we must rely on it
          * to get the locks on both pages in the correct order.
+         *
+         * Another consideration is that we need visibility map page pin(s) if
+         * we will have to clear the all-visible flag on either page.  If we
+         * call RelationGetBufferForTuple, we rely on it to acquire any such
+         * pins; but if we don't, we have to handle that here.  Hence we need
+         * a loop.
          */
-        if (newtupsize > pagefree)
-        {
-            /* Assume there's no chance to put heaptup on same page. */
-            newbuf = RelationGetBufferForTuple(relation, heaptup->t_len,
-                                               buffer, 0, NULL,
-                                               &vmbuffer_new, &vmbuffer);
-        }
-        else
+        for (;;)
         {
+            if (newtupsize > pagefree)
+            {
+                /* It doesn't fit, must use RelationGetBufferForTuple. */
+                newbuf = RelationGetBufferForTuple(relation, heaptup->t_len,
+                                                   buffer, 0, NULL,
+                                                   &vmbuffer_new, &vmbuffer);
+                /* We're all done. */
+                break;
+            }
+            /* Acquire VM page pin if needed and we don't have it. */
+            if (vmbuffer == InvalidBuffer && PageIsAllVisible(page))
+                visibilitymap_pin(relation, block, &vmbuffer);
             /* Re-acquire the lock on the old tuple's page. */
             LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
             /* Re-check using the up-to-date free space */
             pagefree = PageGetHeapFreeSpace(page);
-            if (newtupsize > pagefree)
+            if (newtupsize > pagefree ||
+                (vmbuffer == InvalidBuffer && PageIsAllVisible(page)))
             {
                 /*
-                 * Rats, it doesn't fit anymore.  We must now unlock and
-                 * relock to avoid deadlock.  Fortunately, this path should
-                 * seldom be taken.
+                 * Rats, it doesn't fit anymore, or somebody just now set the
+                 * all-visible flag.  We must now unlock and loop to avoid
+                 * deadlock.  Fortunately, this path should seldom be taken.
                  */
                 LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
-                newbuf = RelationGetBufferForTuple(relation, heaptup->t_len,
-                                                   buffer, 0, NULL,
-                                                   &vmbuffer_new, &vmbuffer);
             }
             else
             {
-                /* OK, it fits here, so we're done. */
+                /* We're all done. */
                 newbuf = buffer;
+                break;
             }
         }
     }
diff --git a/src/backend/access/heap/hio.c b/src/backend/access/heap/hio.c
index 37a1be4114..ffc89685bf 100644
--- a/src/backend/access/heap/hio.c
+++ b/src/backend/access/heap/hio.c
@@ -293,9 +293,13 @@ RelationAddExtraBlocks(Relation relation, BulkInsertState bistate)
  *    happen if space is freed in that page after heap_update finds there's not
  *    enough there).  In that case, the page will be pinned and locked only once.
  *
- *    For the vmbuffer and vmbuffer_other arguments, we avoid deadlock by
- *    locking them only after locking the corresponding heap page, and taking
- *    no further lwlocks while they are locked.
+ *    We also handle the possibility that the all-visible flag will need to be
+ *    cleared on one or both pages.  If so, pin on the associated visibility map
+ *    page must be acquired before acquiring buffer lock(s), to avoid possibly
+ *    doing I/O while holding buffer locks.  The pins are passed back to the
+ *    caller using the input-output arguments vmbuffer and vmbuffer_other.
+ *    Note that in some cases the caller might have already acquired such pins,
+ *    which is indicated by these arguments not being InvalidBuffer on entry.
  *
  *    We normally use FSM to help us find free space.  However,
  *    if HEAP_INSERT_SKIP_FSM is specified, we just append a new empty page to
@@ -666,6 +670,8 @@ loop:
     if (otherBuffer != InvalidBuffer)
     {
         Assert(otherBuffer != buffer);
+        targetBlock = BufferGetBlockNumber(buffer);
+        Assert(targetBlock > otherBlock);

         if (unlikely(!ConditionalLockBuffer(otherBuffer)))
         {
@@ -674,10 +680,16 @@ loop:
             LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);

             /*
-             * Because the buffer was unlocked for a while, it's possible,
-             * although unlikely, that the page was filled. If so, just retry
-             * from start.
+             * Because the buffers were unlocked for a while, it's possible,
+             * although unlikely, that an all-visible flag became set or that
+             * somebody used up the available space in the new page.  We can
+             * use GetVisibilityMapPins to deal with the first case.  In the
+             * second case, just retry from start.
              */
+            GetVisibilityMapPins(relation, otherBuffer, buffer,
+                                 otherBlock, targetBlock, vmbuffer_other,
+                                 vmbuffer);
+
             if (len > PageGetHeapFreeSpace(page))
             {
                 LockBuffer(otherBuffer, BUFFER_LOCK_UNLOCK);

pgsql-hackers by date:

Previous
From: Bruce Momjian
Date:
Subject: Re: Have I found an interval arithmetic bug?
Next
From: Tom Lane
Date:
Subject: Re: psql - add SHOW_ALL_RESULTS option