Fix corruption when relation truncation fails.
authorThomas Munro <[email protected]>
Fri, 20 Dec 2024 08:53:25 +0000 (21:53 +1300)
committerThomas Munro <[email protected]>
Fri, 20 Dec 2024 10:57:26 +0000 (23:57 +1300)
RelationTruncate() does three things, while holding an
AccessExclusiveLock and preventing checkpoints:

1. Logs the truncation.
2. Drops buffers, even if they're dirty.
3. Truncates some number of files.

Step 2 could previously be canceled if it had to wait for I/O, and step
3 could and still can fail in file APIs.  All orderings of these
operations have data corruption hazards if interrupted, so we can't give
up until the whole operation is done.  When dirty pages were discarded
but the corresponding blocks were left on disk due to ERROR, old page
versions could come back from disk, reviving deleted data (see
pgsql-bugs #18146 and several like it).  When primary and standby were
allowed to disagree on relation size, standbys could panic (see
pgsql-bugs #18426) or revive data unknown to visibility management on
the primary (theorized).

Changes:

 * WAL is now unconditionally flushed first
 * smgrtruncate() is now called in a critical section, preventing
   interrupts and causing PANIC on file API failure
 * smgrtruncate() has a new parameter for existing fork sizes,
   because it can't call smgrnblocks() itself inside a critical section

The changes apply to RelationTruncate(), smgr_redo() and
pg_truncate_visibility_map().  That last is also brought up to date with
other evolutions of the truncation protocol.

The VACUUM FileTruncate() failure mode had been discussed in older
reports than the ones referenced below, with independent analysis from
many people, but earlier theories on how to fix it were too complicated
to back-patch.  The more recently invented cancellation bug was
diagnosed by Alexander Lakhin.  Other corruption scenarios were spotted
by me while iterating on this patch and earlier commit 75818b3a.

Back-patch to all supported releases.

Reviewed-by: Michael Paquier <[email protected]>
Reviewed-by: Robert Haas <[email protected]>
Reported-by: [email protected]
Reported-by: Alexander Lakhin <[email protected]>
Discussion: https://postgr.es/m/18146-04e908c662113ad5%40postgresql.org
Discussion: https://postgr.es/m/18426-2d18da6586f152d6%40postgresql.org

contrib/pg_visibility/pg_visibility.c
src/backend/catalog/storage.c
src/backend/storage/smgr/md.c
src/backend/storage/smgr/smgr.c
src/include/storage/md.h
src/include/storage/smgr.h

index 2a4acfd1eee03372decd7c9d2947ddc2b2449785..8c2a493ad2ecbccdc28924df79b6ecc465c778de 100644 (file)
@@ -19,6 +19,7 @@
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
+#include "storage/proc.h"
 #include "storage/procarray.h"
 #include "storage/smgr.h"
 #include "utils/rel.h"
@@ -379,6 +380,7 @@ pg_truncate_visibility_map(PG_FUNCTION_ARGS)
    Relation    rel;
    ForkNumber  fork;
    BlockNumber block;
+   BlockNumber old_block;
 
    rel = relation_open(relid, AccessExclusiveLock);
 
@@ -388,15 +390,22 @@ pg_truncate_visibility_map(PG_FUNCTION_ARGS)
    /* Forcibly reset cached file size */
    RelationGetSmgr(rel)->smgr_cached_nblocks[VISIBILITYMAP_FORKNUM] = InvalidBlockNumber;
 
+   /* Compute new and old size before entering critical section. */
+   fork = VISIBILITYMAP_FORKNUM;
    block = visibilitymap_prepare_truncate(rel, 0);
-   if (BlockNumberIsValid(block))
-   {
-       fork = VISIBILITYMAP_FORKNUM;
-       smgrtruncate(RelationGetSmgr(rel), &fork, 1, &block);
-   }
+   old_block = BlockNumberIsValid(block) ? smgrnblocks(RelationGetSmgr(rel), fork) : 0;
+
+   /*
+    * WAL-logging, buffer dropping, file truncation must be atomic and all on
+    * one side of a checkpoint.  See RelationTruncate() for discussion.
+    */
+   Assert((MyProc->delayChkptFlags & (DELAY_CHKPT_START | DELAY_CHKPT_COMPLETE)) == 0);
+   MyProc->delayChkptFlags |= DELAY_CHKPT_START | DELAY_CHKPT_COMPLETE;
+   START_CRIT_SECTION();
 
    if (RelationNeedsWAL(rel))
    {
+       XLogRecPtr  lsn;
        xl_smgr_truncate xlrec;
 
        xlrec.blkno = 0;
@@ -406,9 +415,17 @@ pg_truncate_visibility_map(PG_FUNCTION_ARGS)
        XLogBeginInsert();
        XLogRegisterData((char *) &xlrec, sizeof(xlrec));
 
-       XLogInsert(RM_SMGR_ID, XLOG_SMGR_TRUNCATE | XLR_SPECIAL_REL_UPDATE);
+       lsn = XLogInsert(RM_SMGR_ID,
+                        XLOG_SMGR_TRUNCATE | XLR_SPECIAL_REL_UPDATE);
+       XLogFlush(lsn);
    }
 
+   if (BlockNumberIsValid(block))
+       smgrtruncate(RelationGetSmgr(rel), &fork, 1, &old_block, &block);
+
+   END_CRIT_SECTION();
+   MyProc->delayChkptFlags &= ~(DELAY_CHKPT_START | DELAY_CHKPT_COMPLETE);
+
    /*
     * Release the lock right away, not at commit time.
     *
index fc300f81199c2ec45b0d3644669caf44c353a3a8..740c6f4d921f2ed7fa94a00275283bbc700b6c94 100644 (file)
@@ -290,6 +290,7 @@ RelationTruncate(Relation rel, BlockNumber nblocks)
    bool        vm;
    bool        need_fsm_vacuum = false;
    ForkNumber  forks[MAX_FORKNUM];
+   BlockNumber old_blocks[MAX_FORKNUM];
    BlockNumber blocks[MAX_FORKNUM];
    int         nforks = 0;
    SMgrRelation reln;
@@ -305,6 +306,7 @@ RelationTruncate(Relation rel, BlockNumber nblocks)
 
    /* Prepare for truncation of MAIN fork of the relation */
    forks[nforks] = MAIN_FORKNUM;
+   old_blocks[nforks] = smgrnblocks(reln, MAIN_FORKNUM);
    blocks[nforks] = nblocks;
    nforks++;
 
@@ -316,6 +318,7 @@ RelationTruncate(Relation rel, BlockNumber nblocks)
        if (BlockNumberIsValid(blocks[nforks]))
        {
            forks[nforks] = FSM_FORKNUM;
+           old_blocks[nforks] = smgrnblocks(reln, FSM_FORKNUM);
            nforks++;
            need_fsm_vacuum = true;
        }
@@ -329,6 +332,7 @@ RelationTruncate(Relation rel, BlockNumber nblocks)
        if (BlockNumberIsValid(blocks[nforks]))
        {
            forks[nforks] = VISIBILITYMAP_FORKNUM;
+           old_blocks[nforks] = smgrnblocks(reln, VISIBILITYMAP_FORKNUM);
            nforks++;
        }
    }
@@ -365,14 +369,20 @@ RelationTruncate(Relation rel, BlockNumber nblocks)
    MyProc->delayChkptFlags |= DELAY_CHKPT_START | DELAY_CHKPT_COMPLETE;
 
    /*
-    * We WAL-log the truncation before actually truncating, which means
-    * trouble if the truncation fails. If we then crash, the WAL replay
-    * likely isn't going to succeed in the truncation either, and cause a
-    * PANIC. It's tempting to put a critical section here, but that cure
-    * would be worse than the disease. It would turn a usually harmless
-    * failure to truncate, that might spell trouble at WAL replay, into a
-    * certain PANIC.
+    * We WAL-log the truncation first and then truncate in a critical
+    * section. Truncation drops buffers, even if dirty, and then truncates
+    * disk files. All of that work needs to complete before the lock is
+    * released, or else old versions of pages on disk that are missing recent
+    * changes would become accessible again.  We'll try the whole operation
+    * again in crash recovery if we panic, but even then we can't give up
+    * because we don't want standbys' relation sizes to diverge and break
+    * replay or visibility invariants downstream.  The critical section also
+    * suppresses interrupts.
+    *
+    * (See also pg_visibilitymap.c if changing this code.)
     */
+   START_CRIT_SECTION();
+
    if (RelationNeedsWAL(rel))
    {
        /*
@@ -396,10 +406,10 @@ RelationTruncate(Relation rel, BlockNumber nblocks)
         * hit the disk before the WAL record, and the truncation of the FSM
         * or visibility map. If we crashed during that window, we'd be left
         * with a truncated heap, but the FSM or visibility map would still
-        * contain entries for the non-existent heap pages.
+        * contain entries for the non-existent heap pages, and standbys would
+        * also never replay the truncation.
         */
-       if (fsm || vm)
-           XLogFlush(lsn);
+       XLogFlush(lsn);
    }
 
    /*
@@ -407,7 +417,9 @@ RelationTruncate(Relation rel, BlockNumber nblocks)
     * longer exist after truncation is complete, and then truncate the
     * corresponding files on disk.
     */
-   smgrtruncate(RelationGetSmgr(rel), forks, nforks, blocks);
+   smgrtruncate(RelationGetSmgr(rel), forks, nforks, old_blocks, blocks);
+
+   END_CRIT_SECTION();
 
    /* We've done all the critical work, so checkpoints are OK now. */
    MyProc->delayChkptFlags &= ~(DELAY_CHKPT_START | DELAY_CHKPT_COMPLETE);
@@ -991,6 +1003,7 @@ smgr_redo(XLogReaderState *record)
        Relation    rel;
        ForkNumber  forks[MAX_FORKNUM];
        BlockNumber blocks[MAX_FORKNUM];
+       BlockNumber old_blocks[MAX_FORKNUM];
        int         nforks = 0;
        bool        need_fsm_vacuum = false;
 
@@ -1025,6 +1038,7 @@ smgr_redo(XLogReaderState *record)
        if ((xlrec->flags & SMGR_TRUNCATE_HEAP) != 0)
        {
            forks[nforks] = MAIN_FORKNUM;
+           old_blocks[nforks] = smgrnblocks(reln, MAIN_FORKNUM);
            blocks[nforks] = xlrec->blkno;
            nforks++;
 
@@ -1042,6 +1056,7 @@ smgr_redo(XLogReaderState *record)
            if (BlockNumberIsValid(blocks[nforks]))
            {
                forks[nforks] = FSM_FORKNUM;
+               old_blocks[nforks] = smgrnblocks(reln, FSM_FORKNUM);
                nforks++;
                need_fsm_vacuum = true;
            }
@@ -1053,13 +1068,18 @@ smgr_redo(XLogReaderState *record)
            if (BlockNumberIsValid(blocks[nforks]))
            {
                forks[nforks] = VISIBILITYMAP_FORKNUM;
+               old_blocks[nforks] = smgrnblocks(reln, VISIBILITYMAP_FORKNUM);
                nforks++;
            }
        }
 
        /* Do the real work to truncate relation forks */
        if (nforks > 0)
-           smgrtruncate(reln, forks, nforks, blocks);
+       {
+           START_CRIT_SECTION();
+           smgrtruncate(reln, forks, nforks, old_blocks, blocks);
+           END_CRIT_SECTION();
+       }
 
        /*
         * Update upper-level FSM pages to account for the truncation. This is
index fdecbad17095743c1b0e178d3a6dbd98aa6817fd..2a0dcc3194563db1c6ddbef32bcd74123eb4bd24 100644 (file)
@@ -990,19 +990,21 @@ mdnblocks(SMgrRelation reln, ForkNumber forknum)
 
 /*
  * mdtruncate() -- Truncate relation to specified number of blocks.
+ *
+ * Guaranteed not to allocate memory, so it can be used in a critical section.
+ * Caller must have called smgrnblocks() to obtain curnblk while holding a
+ * sufficient lock to prevent a change in relation size, and not used any smgr
+ * functions for this relation or handled interrupts in between.  This makes
+ * sure we have opened all active segments, so that truncate loop will get
+ * them all!
  */
 void
-mdtruncate(SMgrRelation reln, ForkNumber forknum, BlockNumber nblocks)
+mdtruncate(SMgrRelation reln, ForkNumber forknum,
+          BlockNumber curnblk, BlockNumber nblocks)
 {
-   BlockNumber curnblk;
    BlockNumber priorblocks;
    int         curopensegs;
 
-   /*
-    * NOTE: mdnblocks makes sure we have opened all active segments, so that
-    * truncation loop will get them all!
-    */
-   curnblk = mdnblocks(reln, forknum);
    if (nblocks > curnblk)
    {
        /* Bogus request ... but no complaint if InRecovery */
@@ -1298,7 +1300,7 @@ _fdvec_resize(SMgrRelation reln,
        reln->md_seg_fds[forknum] =
            MemoryContextAlloc(MdCxt, sizeof(MdfdVec) * nseg);
    }
-   else
+   else if (nseg > reln->md_num_open_segs[forknum])
    {
        /*
         * It doesn't seem worthwhile complicating the code to amortize
@@ -1310,6 +1312,16 @@ _fdvec_resize(SMgrRelation reln,
            repalloc(reln->md_seg_fds[forknum],
                     sizeof(MdfdVec) * nseg);
    }
+   else
+   {
+       /*
+        * We don't reallocate a smaller array, because we want mdtruncate()
+        * to be able to promise that it won't allocate memory, so that it is
+        * allowed in a critical section.  This means that a bit of space in
+        * the array is now wasted, until the next time we add a segment and
+        * reallocate.
+        */
+   }
 
    reln->md_num_open_segs[forknum] = nseg;
 }
index e4a4f66b7ee2859788f829c9cf2ff05c7e140d82..7b5d6754bb91cb3ebb278b45b63b470a59d67b78 100644 (file)
@@ -63,7 +63,7 @@ typedef struct f_smgr
                                   BlockNumber blocknum, BlockNumber nblocks);
    BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
    void        (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
-                                 BlockNumber nblocks);
+                                 BlockNumber old_blocks, BlockNumber nblocks);
    void        (*smgr_immedsync) (SMgrRelation reln, ForkNumber forknum);
 } f_smgr;
 
@@ -651,10 +651,15 @@ smgrnblocks_cached(SMgrRelation reln, ForkNumber forknum)
  *
  * The caller must hold AccessExclusiveLock on the relation, to ensure that
  * other backends receive the smgr invalidation event that this function sends
- * before they access any forks of the relation again.
+ * before they access any forks of the relation again.  The current size of
+ * the forks should be provided in old_nblocks.  This function should normally
+ * be called in a critical section, but the current size must be checked
+ * outside the critical section, and no interrupts or smgr functions relating
+ * to this relation should be called in between.
  */
 void
-smgrtruncate(SMgrRelation reln, ForkNumber *forknum, int nforks, BlockNumber *nblocks)
+smgrtruncate(SMgrRelation reln, ForkNumber *forknum, int nforks,
+            BlockNumber *old_nblocks, BlockNumber *nblocks)
 {
    int         i;
 
@@ -682,7 +687,8 @@ smgrtruncate(SMgrRelation reln, ForkNumber *forknum, int nforks, BlockNumber *nb
        /* Make the cached size is invalid if we encounter an error. */
        reln->smgr_cached_nblocks[forknum[i]] = InvalidBlockNumber;
 
-       smgrsw[reln->smgr_which].smgr_truncate(reln, forknum[i], nblocks[i]);
+       smgrsw[reln->smgr_which].smgr_truncate(reln, forknum[i],
+                                              old_nblocks[i], nblocks[i]);
 
        /*
         * We might as well update the local smgr_cached_nblocks values. The
index 941879ee6a8fda631cea27f07f8a40601fce501f..a16235d51fec0e0dfe0e6a3c0734532e37a0f444 100644 (file)
@@ -40,7 +40,7 @@ extern void mdwriteback(SMgrRelation reln, ForkNumber forknum,
                        BlockNumber blocknum, BlockNumber nblocks);
 extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
 extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
-                      BlockNumber nblocks);
+                      BlockNumber old_blocks, BlockNumber nblocks);
 extern void mdimmedsync(SMgrRelation reln, ForkNumber forknum);
 
 extern void ForgetDatabaseSyncRequests(Oid dbid);
index a9a179aabacc488b7bd5d1e9de88c05df3891c3a..8b1157541afbc0618fbc29e94d36b22ae7d6dd29 100644 (file)
@@ -104,8 +104,9 @@ extern void smgrwriteback(SMgrRelation reln, ForkNumber forknum,
                          BlockNumber blocknum, BlockNumber nblocks);
 extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
 extern BlockNumber smgrnblocks_cached(SMgrRelation reln, ForkNumber forknum);
-extern void smgrtruncate(SMgrRelation reln, ForkNumber *forknum,
-                        int nforks, BlockNumber *nblocks);
+extern void smgrtruncate(SMgrRelation reln, ForkNumber *forknum, int nforks,
+                        BlockNumber *old_nblocks,
+                        BlockNumber *nblocks);
 extern void smgrimmedsync(SMgrRelation reln, ForkNumber forknum);
 extern void AtEOXact_SMgr(void);
 extern bool ProcessBarrierSmgrRelease(void);