Re: [PATCH v2] Fix exported snapshot xmin handoff race

From: "chee(dot)wooson" <chee(dot)wooson(at)gmail(dot)com>
To: Peter Geoghegan <pg(at)bowt(dot)ie>
Cc: pgsql-hackers(at)lists(dot)postgresql(dot)org, "Drouvot, Bertrand" <bertranddrouvot(dot)pg(at)gmail(dot)com>
Subject: Re: [PATCH v2] Fix exported snapshot xmin handoff race
Date: 2026-07-30 07:58:09
Message-ID: 20260730075809.799527-1-chee.wooson@gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi Peter,

Thanks for the pointers. I spent some time looking at both threads.

The standby subxact-overflow export issue seems separate from this patch: it is
about preserving the contents of a recovery snapshot during export/import.

Scott's recovery-conflict issue is much closer. My v2 makes
ProcArrayInstallImportedXmin() take ProcArrayLock exclusively, which prevents a
snapshot import from installing xmin concurrently with a procarray scan such as
GetConflictingVirtualXIDs(). However, I don't think that alone fixes Scott's
case.

Consider this sequence:

1. Standby recovery is replaying a cleanup record with snapshot conflict
horizon H.

2. Backend A has xmin <= H, so GetConflictingVirtualXIDs() returns A's VXID.

3. GetConflictingVirtualXIDs() releases ProcArrayLock. The recovery conflict
wait list is now fixed to A's VXID.

4. Before A actually ends, backend B imports A's exported snapshot.

5. With v2, B's import takes ProcArrayLock exclusively, but the procarray scan
in step 2 has already finished. So B can still install the same old xmin.

6. Recovery waits for, or cancels, only A's VXID.

7. Once A is gone, recovery can replay the cleanup record even though B is still
running with the imported snapshot.

One possible way to close that remaining hole, other than rescanning, would be
to mark source PGPROCs that have been selected as snapshot-conflict waiters by
recovery, and make ProcArrayInstallImportedXmin() reject importing from such a
source.

The intended sequence would be:

1. Standby recovery calls GetConflictingVirtualXIDs(H).

2. While holding ProcArrayLock, GetConflictingVirtualXIDs() finds backend A with
xmin <= H.

3. Before releasing ProcArrayLock, recovery sets a flag in A's PGPROC saying
that A's current VXID has been selected as a snapshot-conflict waiter.

4. Later, backend B tries to import A's exported snapshot.

5. ProcArrayInstallImportedXmin() takes ProcArrayLock exclusively and finds A as
the source VXID.

6. Because A's PGPROC says that its current VXID is already a recovery
snapshot-conflict waiter, B's import is rejected instead of installing A's
old xmin.

7. A can no longer create new conflicting importers after recovery has selected
it for conflict resolution. Once recovery has waited for or cancelled A, the
original wait list is complete.

This relies on the exclusive ProcArrayLock in ProcArrayInstallImportedXmin():
while recovery is scanning the procarray and setting the flag, a concurrent
import cannot slip through before the flag becomes visible.

I have not included that in this patch because it seems to belong to Scott's
recovery-conflict thread, and it changes the scope of the fix. But I can help
explore that direction there if people think it is preferable to rescanning.

Regards,
Chee

In response to

Browse pgsql-hackers by date

  From Date Subject
Previous Message Nikita Malakhov 2026-07-30 07:53:39 Re: Direct Toast PoC