Never store 0 as the nextMXact
authorHeikki Linnakangas <heikki.linnakangas@iki.fi>
Fri, 12 Dec 2025 08:47:34 +0000 (10:47 +0200)
committerHeikki Linnakangas <heikki.linnakangas@iki.fi>
Fri, 12 Dec 2025 08:47:34 +0000 (10:47 +0200)
Before this commit, when multixid wraparound happens,
MultiXactState->nextMXact goes to 0, which is invalid. All the readers
need to deal with that possibility and skip over the 0. That's
error-prone and we've missed it a few times in the past. This commit
changes the responsibility so that all the writers of
MultiXactState->nextMXact skip over the zero already, and readers can
trust that it's never 0.

We were already doing that for MultiXactState->oldestMultiXactId; none
of its writers would set it to 0. ReadMultiXactIdRange() was
nevertheless checking for that possibility. For clarity, remove that
check.

Reviewed-by: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Reviewed-by: Maxim Orlov <orlovmg@gmail.com>
Discussion: https://www.postgresql.org/message-id/3624730d-6dae-42bf-9458-76c4c965fb27@iki.fi

src/backend/access/transam/multixact.c
src/bin/pg_resetwal/pg_resetwal.c
src/bin/pg_resetwal/t/001_basic.pl

index 8ba2f4529dc19bb928dc8a1fcc1b5196818179dc..f4fab7edfee9a742931d644d2da5ae57ae2559f8 100644 (file)
 #define MULTIXACT_MEMBER_LOW_THRESHOLD     UINT64CONST(2000000000)
 #define MULTIXACT_MEMBER_HIGH_THRESHOLD        UINT64CONST(4000000000)
 
+static inline MultiXactId
+NextMultiXactId(MultiXactId multi)
+{
+   return multi == MaxMultiXactId ? FirstMultiXactId : multi + 1;
+}
+
 static inline MultiXactId
 PreviousMultiXactId(MultiXactId multi)
 {
@@ -552,14 +558,7 @@ MultiXactIdSetOldestMember(void)
         */
        LWLockAcquire(MultiXactGenLock, LW_SHARED);
 
-       /*
-        * We have to beware of the possibility that nextMXact is in the
-        * wrapped-around state.  We don't fix the counter itself here, but we
-        * must be sure to store a valid value in our array entry.
-        */
        nextMXact = MultiXactState->nextMXact;
-       if (nextMXact < FirstMultiXactId)
-           nextMXact = FirstMultiXactId;
 
        OldestMemberMXactId[MyProcNumber] = nextMXact;
 
@@ -596,15 +595,7 @@ MultiXactIdSetOldestVisible(void)
 
        LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
 
-       /*
-        * We have to beware of the possibility that nextMXact is in the
-        * wrapped-around state.  We don't fix the counter itself here, but we
-        * must be sure to store a valid value in our array entry.
-        */
        oldestMXact = MultiXactState->nextMXact;
-       if (oldestMXact < FirstMultiXactId)
-           oldestMXact = FirstMultiXactId;
-
        for (i = 0; i < MaxOldestSlot; i++)
        {
            MultiXactId thisoldest = OldestMemberMXactId[i];
@@ -637,9 +628,6 @@ ReadNextMultiXactId(void)
    mxid = MultiXactState->nextMXact;
    LWLockRelease(MultiXactGenLock);
 
-   if (mxid < FirstMultiXactId)
-       mxid = FirstMultiXactId;
-
    return mxid;
 }
 
@@ -654,11 +642,6 @@ ReadMultiXactIdRange(MultiXactId *oldest, MultiXactId *next)
    *oldest = MultiXactState->oldestMultiXactId;
    *next = MultiXactState->nextMXact;
    LWLockRelease(MultiXactGenLock);
-
-   if (*oldest < FirstMultiXactId)
-       *oldest = FirstMultiXactId;
-   if (*next < FirstMultiXactId)
-       *next = FirstMultiXactId;
 }
 
 
@@ -794,9 +777,7 @@ RecordNewMultiXact(MultiXactId multi, MultiXactOffset offset,
    entryno = MultiXactIdToOffsetEntry(multi);
 
    /* position of the next multixid */
-   next = multi + 1;
-   if (next < FirstMultiXactId)
-       next = FirstMultiXactId;
+   next = NextMultiXactId(multi);
    next_pageno = MultiXactIdToOffsetPage(next);
    next_entryno = MultiXactIdToOffsetEntry(next);
 
@@ -955,10 +936,6 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset)
 
    LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
 
-   /* Handle wraparound of the nextMXact counter */
-   if (MultiXactState->nextMXact < FirstMultiXactId)
-       MultiXactState->nextMXact = FirstMultiXactId;
-
    /* Assign the MXID */
    result = MultiXactState->nextMXact;
 
@@ -1025,7 +1002,7 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset)
         * request only once per 64K multis generated.  This still gives
         * plenty of chances before we get into real trouble.
         */
-       if (IsUnderPostmaster && (result % 65536) == 0)
+       if (IsUnderPostmaster && ((result % 65536) == 0 || result == FirstMultiXactId))
            SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER);
 
        if (!MultiXactIdPrecedes(result, multiWarnLimit))
@@ -1056,15 +1033,13 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset)
        /* Re-acquire lock and start over */
        LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
        result = MultiXactState->nextMXact;
-       if (result < FirstMultiXactId)
-           result = FirstMultiXactId;
    }
 
    /*
     * Make sure there is room for the next MXID in the file.  Assigning this
     * MXID sets the next MXID's offset already.
     */
-   ExtendMultiXactOffset(result + 1);
+   ExtendMultiXactOffset(NextMultiXactId(result));
 
    /*
     * Reserve the members space, similarly to above.
@@ -1098,15 +1073,8 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset)
    /*
     * Advance counters.  As in GetNewTransactionId(), this must not happen
     * until after file extension has succeeded!
-    *
-    * We don't care about MultiXactId wraparound here; it will be handled by
-    * the next iteration.  But note that nextMXact may be InvalidMultiXactId
-    * or the first value on a segment-beginning page after this routine
-    * exits, so anyone else looking at the variable must be prepared to deal
-    * with either case.
     */
-   (MultiXactState->nextMXact)++;
-
+   MultiXactState->nextMXact = NextMultiXactId(result);
    MultiXactState->nextOffset += nmembers;
 
    LWLockRelease(MultiXactGenLock);
@@ -1255,9 +1223,7 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members,
        MultiXactId tmpMXact;
 
        /* handle wraparound if needed */
-       tmpMXact = multi + 1;
-       if (tmpMXact < FirstMultiXactId)
-           tmpMXact = FirstMultiXactId;
+       tmpMXact = NextMultiXactId(multi);
 
        prev_pageno = pageno;
 
@@ -1907,7 +1873,7 @@ TrimMultiXact(void)
        LWLock     *lock = SimpleLruGetBankLock(MultiXactOffsetCtl, pageno);
 
        LWLockAcquire(lock, LW_EXCLUSIVE);
-       if (entryno == 0)
+       if (entryno == 0 || nextMXact == FirstMultiXactId)
            slotno = SimpleLruZeroPage(MultiXactOffsetCtl, pageno);
        else
            slotno = SimpleLruReadPage(MultiXactOffsetCtl, pageno, true, nextMXact);
@@ -2023,8 +1989,10 @@ void
 MultiXactSetNextMXact(MultiXactId nextMulti,
                      MultiXactOffset nextMultiOffset)
 {
+   Assert(MultiXactIdIsValid(nextMulti));
    debug_elog4(DEBUG2, "MultiXact: setting next multi to %u offset %" PRIu64,
                nextMulti, nextMultiOffset);
+
    LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
    MultiXactState->nextMXact = nextMulti;
    MultiXactState->nextOffset = nextMultiOffset;
@@ -2193,6 +2161,8 @@ void
 MultiXactAdvanceNextMXact(MultiXactId minMulti,
                          MultiXactOffset minMultiOffset)
 {
+   Assert(MultiXactIdIsValid(minMulti));
+
    LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
    if (MultiXactIdPrecedes(MultiXactState->nextMXact, minMulti))
    {
@@ -2330,7 +2300,6 @@ MultiXactId
 GetOldestMultiXactId(void)
 {
    MultiXactId oldestMXact;
-   MultiXactId nextMXact;
    int         i;
 
    /*
@@ -2338,17 +2307,7 @@ GetOldestMultiXactId(void)
     * OldestVisibleMXactId[] entries, or nextMXact if none are valid.
     */
    LWLockAcquire(MultiXactGenLock, LW_SHARED);
-
-   /*
-    * We have to beware of the possibility that nextMXact is in the
-    * wrapped-around state.  We don't fix the counter itself here, but we
-    * must be sure to use a valid value in our calculation.
-    */
-   nextMXact = MultiXactState->nextMXact;
-   if (nextMXact < FirstMultiXactId)
-       nextMXact = FirstMultiXactId;
-
-   oldestMXact = nextMXact;
+   oldestMXact = MultiXactState->nextMXact;
    for (i = 0; i < MaxOldestSlot; i++)
    {
        MultiXactId thisoldest;
@@ -2669,6 +2628,7 @@ TruncateMultiXact(MultiXactId newOldestMulti, Oid newOldestMultiDB)
 
    Assert(!RecoveryInProgress());
    Assert(MultiXactState->finishedStartup);
+   Assert(MultiXactIdIsValid(newOldestMulti));
 
    /*
     * We can only allow one truncation to happen at once. Otherwise parts of
@@ -2683,7 +2643,6 @@ TruncateMultiXact(MultiXactId newOldestMulti, Oid newOldestMultiDB)
    nextOffset = MultiXactState->nextOffset;
    oldestMulti = MultiXactState->oldestMultiXactId;
    LWLockRelease(MultiXactGenLock);
-   Assert(MultiXactIdIsValid(oldestMulti));
 
    /*
     * Make sure to only attempt truncation if there's values to truncate
@@ -2953,7 +2912,7 @@ multixact_redo(XLogReaderState *record)
                           xlrec->members);
 
        /* Make sure nextMXact/nextOffset are beyond what this record has */
-       MultiXactAdvanceNextMXact(xlrec->mid + 1,
+       MultiXactAdvanceNextMXact(NextMultiXactId(xlrec->mid),
                                  xlrec->moff + xlrec->nmembers);
 
        /*
index 56012d5f4c461597ecbc9b6bdb783fa6cd8512c4..9bfab8c307bca6a22234ace497865956da07a1a2 100644 (file)
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
                 * XXX It'd be nice to have more sanity checks here, e.g. so
                 * that oldest is not wrapped around w.r.t. nextMulti.
                 */
+               if (next_mxid_val == 0)
+                   pg_fatal("next multitransaction ID (-m) must not be 0");
                if (oldest_mxid_val == 0)
                    pg_fatal("oldest multitransaction ID (-m) must not be 0");
                mxids_given = true;
index 8bab9add74f475e1c50853e3931363fa1508210b..dde024a7f14736cf97de54f4f1dc2c30ccb48bbe 100644 (file)
@@ -119,19 +119,10 @@ command_fails_like(
    [ 'pg_resetwal', '-m' => '10,bar', $node->data_dir ],
    qr/error: invalid argument for option -m/,
    'fails with incorrect -m option part 2');
-
-# This used to be forbidden, but nextMulti can legitimately be 0 after
-# wraparound, so we now accept it in pg_resetwal too.
-command_ok(
-   [ 'pg_resetwal', '-m' => '0,10', $node->data_dir ],
-   'succeeds with -m value 0 in the first part');
-
-# -0 doesn't make sense however
 command_fails_like(
-   [ 'pg_resetwal', '-m' => '-0,10', $node->data_dir ],
-   qr/error: invalid argument for option -m/,
-   'fails with -m value -0 in the first part');
-
+   [ 'pg_resetwal', '-m' => '0,10', $node->data_dir ],
+   qr/must not be 0/,
+   'fails with -m value 0 in the first part');
 command_fails_like(
    [ 'pg_resetwal', '-m' => '10,0', $node->data_dir ],
    qr/must not be 0/,