On Mon, Mar 2, 2026 at 12:15 PM Melanie Plageman
<[email protected]> wrote:
> > It's already possible (though not particularly common) for VACUUM to
> > fully set a Multi xmax to InvalidTransactionId when the Multi is >
> > OldestMxact -- it can even happen when most of the individual members
> > are still > OldestXmin. As long as the operation cannot affect tuple
> > visibility on standbys, no special snapshotConflictHorizon is
> > required. We do currently end up using OldestXmin-1 as our
> > snapshotConflictHorizon when this happens, but as I said in the other
> > email, I don't think that that's really required.
>
> I wrote the attached patch to keep track of the newest to-be-frozen
> xid. I think the freeze_xmin and replace_xvac cases are right. But I'm
> not sure about the freeze_xmax and replace_xmax for multixacts cases.

This looks directionally correct to me, at a minimum.

> For the freeze_xmax case for regular transaction IDs, are you saying
> that the only way you can have one older than OldestXmin is if the
> update transaction aborted?

I don't quite follow. Are you talking about xmin or xmax? The xmax in
question must come from the original tuple, not the updated successor
version? (The successor is the tuple that pruning must have already
removed by the time we reach heap_prepare_freeze_tuple, preventing
heap_prepare_freeze_tuple from seeing an aborted xmin.)

The important principle here is that we don't need a recovery conflict
to handle cleanup after an aborted update/delete, regardless of the
details. This is a logical consequence of the fact that an aborted
transaction "never existed in the logical database".

This principle has nothing to do with implementation details such as
freeze plans and tuple headers. Of course, heap_prepare_freeze_tuple
sometimes needs to prepare a freeze plan that modifies a tuple updated
by an aborted transaction, just to set xmax to InvalidTransactionID.
But, again, no recovery conflict is required to do this safely because
this is for an aborted xact (same as with pruning that removed the
successor version).

> Or are there other ways you can have an
> xmax older than OldestXmin?

Again, are you talking about xmin or xmax? It's normal for
heap_prepare_freeze_tuple to see an xmax older than OldestXmin, last I
checked.

Don't forget about plain XIDs that end up as xmax due to a SELECT FOR
UPDATE. They usually don't result from aborted transactions.

Another important principle (which you've clearly followed here):
cleaning up after a locker doesn't require a recovery conflict because
it cannot affect tuple visibility on standbys.

> My patch does not consider any multi member xids when calculating the
> newest to-be-frozen xid. Locker-only xmaxes shouldn't affect
> visibility on the standby.

But neither should the XIDs of updaters that aborted. I don't think
you should handle those at all.

> And I think, partially based on what you
> are saying above and partially from reading the code, that update XIDs
> older than OldestXmin don't matter because they would be from aborted
> transactions and XIDs newer than OldestXmin won't be removed or
> frozen. Does this sound right?

I think that as a general rule VACUUM should never generate a
snapshotConflictHorizon that exactly equals OldestXmin (or a later
one). See commit 66fbcb0d.

I notice that you have an assertion
prstate->pagefrz.FreezePageConflictXid <= OldestXmin. I wonder if you
should strengthen the assertion to
prstate->pagefrz.FreezePageConflictXid < OldestXmin? I also wonder if
the existing assertion can fail due to an aborted update leaving an
xmax > OldestXmin.

--
Peter Geoghegan


Reply via email to