Skip to content

Commit 40c49bd

Browse files
committed
Fix race between GetNewTransactionId and GetOldestActiveTransactionId.
The race condition goes like this: 1. GetNewTransactionId advances nextXid e.g. from 100 to 101 2. GetOldestActiveTransactionId reads the new nextXid, 101 3. GetOldestActiveTransactionId loops through the proc array. There are no active XIDs there, so it returns 101 as the oldest active XID. 4. GetNewTransactionid stores XID 100 to MyPgXact->xid So, GetOldestActiveTransactionId returned XID 101, even though 100 only just started and is surely still running. This would be hard to hit in practice, and even harder to spot any ill effect if it happens. GetOldestActiveTransactionId is only used when creating a checkpoint in a master server, and the race condition can only happen on an online checkpoint, as there are no backends running during a shutdown checkpoint. The oldestActiveXid value of an online checkpoint is only used when starting up a hot standby server, to determine the starting point where pg_subtrans is initialized from. For the race condition to happen, there must be no other XIDs in the proc array that would hold back the oldest-active XID value, which means that the missed XID must be a top transaction's XID. However, pg_subtrans is not used for top XIDs, so I believe an off-by-one error is in fact inconsequential. Nevertheless, let's fix it, as it's clearly wrong and the fix is simple. This has been wrong ever since hot standby was introduced, so backport to all supported versions. Discussion: https://www.postgresql.org/message-id/e7258662-82b6-7a45-56d4-99b337a32bf7@iki.fi
1 parent 75670ec commit 40c49bd

File tree

1 file changed

+8
-8
lines changed

1 file changed

+8
-8
lines changed

src/backend/storage/ipc/procarray.c

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,20 +1764,21 @@ GetOldestActiveTransactionId(void)
17641764

17651765
Assert(!RecoveryInProgress());
17661766

1767-
LWLockAcquire(ProcArrayLock, LW_SHARED);
1768-
17691767
/*
1770-
* It's okay to read nextXid without acquiring XidGenLock because (1) we
1771-
* assume TransactionIds can be read atomically and (2) we don't care if
1772-
* we get a slightly stale value. It can't be very stale anyway, because
1773-
* the LWLockAcquire above will have done any necessary memory
1774-
* interlocking.
1768+
* Read nextXid, as the upper bound of what's still active.
1769+
*
1770+
* Reading a TransactionId is atomic, but we must grab the lock to make
1771+
* sure that all XIDs < nextXid are already present in the proc array (or
1772+
* have already completed), when we spin over it.
17751773
*/
1774+
LWLockAcquire(XidGenLock, LW_SHARED);
17761775
oldestRunningXid = ShmemVariableCache->nextXid;
1776+
LWLockRelease(XidGenLock);
17771777

17781778
/*
17791779
* Spin over procArray collecting all xids and subxids.
17801780
*/
1781+
LWLockAcquire(ProcArrayLock, LW_SHARED);
17811782
for (index = 0; index < arrayP->numProcs; index++)
17821783
{
17831784
int pgprocno = arrayP->pgprocnos[index];
@@ -1799,7 +1800,6 @@ GetOldestActiveTransactionId(void)
17991800
* smaller than oldestRunningXid
18001801
*/
18011802
}
1802-
18031803
LWLockRelease(ProcArrayLock);
18041804

18051805
return oldestRunningXid;

0 commit comments

Comments
 (0)