Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 7ba325f

Browse files
committed
Fix visibility check when XID is committed in CLOG but not in procarray.
TransactionIdIsInProgress had a fast path to return 'false' if the single-item CLOG cache said that the transaction was known to be committed. However, that was wrong, because a transaction is first marked as committed in the CLOG but doesn't become visible to others until it has removed its XID from the proc array. That could lead to an error: ERROR: t_xmin is uncommitted in tuple to be updated or for an UPDATE to go ahead without blocking, before the previous UPDATE on the same row was made visible. The window is usually very short, but synchronous replication makes it much wider, because the wait for synchronous replica happens in that window. Another thing that makes it hard to hit is that it's hard to get such a commit-in-progress transaction into the single item CLOG cache. Normally, if you call TransactionIdIsInProgress on such a transaction, it determines that the XID is in progress without checking the CLOG and without populating the cache. One way to prime the cache is to explicitly call pg_xact_status() on the XID. Another way is to use a lot of subtransactions, so that the subxid cache in the proc array is overflown, making TransactionIdIsInProgress rely on pg_subtrans and CLOG checks. This has been broken ever since it was introduced in 2008, but the race condition is very hard to hit, especially without synchronous replication. There were a couple of reports of the error starting from summer 2021, but no one was able to find the root cause then. TransactionIdIsKnownCompleted() is now unused. In 'master', remove it, but I left it in place in backbranches in case it's used by extensions. Also change pg_xact_status() to check TransactionIdIsInProgress(). Previously, it only checked the CLOG, and returned "committed" before the transaction was actually made visible to other queries. Note that this also means that you cannot use pg_xact_status() to reproduce the bug anymore, even if the code wasn't fixed. Report and analysis by Konstantin Knizhnik. Patch by Simon Riggs, with the pg_xact_status() change added by me. Author: Simon Riggs Reviewed-by: Andres Freund Discussion: https://www.postgresql.org/message-id/flat/4da7913d-398c-e2ad-d777-f752cf7f0bbb%40garret.ru
1 parent aa1845c commit 7ba325f

File tree

3 files changed

+32
-23
lines changed

3 files changed

+32
-23
lines changed

src/backend/access/transam/transam.c

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,15 @@ TransactionIdDidAbort(TransactionId transactionId)
226226
*
227227
* This does NOT look into pg_xact but merely probes our local cache
228228
* (and so it's not named TransactionIdDidComplete, which would be the
229-
* appropriate name for a function that worked that way). The intended
230-
* use is just to short-circuit TransactionIdIsInProgress calls when doing
231-
* repeated heapam_visibility.c checks for the same XID. If this isn't
232-
* extremely fast then it will be counterproductive.
229+
* appropriate name for a function that worked that way).
230+
*
231+
* NB: This is unused, and will be removed in v15. This was used to
232+
* short-circuit TransactionIdIsInProgress, but that was wrong for a
233+
* transaction that was known to be marked as committed in CLOG but not
234+
* yet removed from the proc array. This is kept in backbranches just in
235+
* case it is still used by extensions. However, extensions doing
236+
* something similar to tuple visibility checks should also be careful to
237+
* check the proc array first!
233238
*
234239
* Note:
235240
* Assumes transaction identifier is valid.

src/backend/storage/ipc/procarray.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ static ProcArrayStruct *procArray;
104104
static PGPROC *allProcs;
105105
static PGXACT *allPgXact;
106106

107+
/*
108+
* Cache to reduce overhead of repeated calls to TransactionIdIsInProgress()
109+
*/
110+
static TransactionId cachedXidIsNotInProgress = InvalidTransactionId;
111+
107112
/*
108113
* Bookkeeping for tracking emulated transactions in recovery
109114
*/
@@ -1027,7 +1032,7 @@ TransactionIdIsInProgress(TransactionId xid)
10271032
* already known to be completed, we can fall out without any access to
10281033
* shared memory.
10291034
*/
1030-
if (TransactionIdIsKnownCompleted(xid))
1035+
if (TransactionIdEquals(cachedXidIsNotInProgress, xid))
10311036
{
10321037
xc_by_known_xact_inc();
10331038
return false;
@@ -1177,6 +1182,7 @@ TransactionIdIsInProgress(TransactionId xid)
11771182
if (nxids == 0)
11781183
{
11791184
xc_no_overflow_inc();
1185+
cachedXidIsNotInProgress = xid;
11801186
return false;
11811187
}
11821188

@@ -1191,7 +1197,10 @@ TransactionIdIsInProgress(TransactionId xid)
11911197
xc_slow_answer_inc();
11921198

11931199
if (TransactionIdDidAbort(xid))
1200+
{
1201+
cachedXidIsNotInProgress = xid;
11941202
return false;
1203+
}
11951204

11961205
/*
11971206
* It isn't aborted, so check whether the transaction tree it belongs to
@@ -1209,6 +1218,7 @@ TransactionIdIsInProgress(TransactionId xid)
12091218
}
12101219
}
12111220

1221+
cachedXidIsNotInProgress = xid;
12121222
return false;
12131223
}
12141224

src/backend/utils/adt/xid8funcs.c

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "miscadmin.h"
3737
#include "postmaster/postmaster.h"
3838
#include "storage/lwlock.h"
39+
#include "storage/procarray.h"
3940
#include "utils/builtins.h"
4041
#include "utils/memutils.h"
4142
#include "utils/snapmgr.h"
@@ -677,29 +678,22 @@ pg_xact_status(PG_FUNCTION_ARGS)
677678
{
678679
Assert(TransactionIdIsValid(xid));
679680

680-
if (TransactionIdIsCurrentTransactionId(xid))
681+
/*
682+
* Like when doing visiblity checks on a row, check whether the
683+
* transaction is still in progress before looking into the CLOG.
684+
* Otherwise we would incorrectly return "committed" for a transaction
685+
* that is committing and has already updated the CLOG, but hasn't
686+
* removed its XID from the proc array yet. (See comment on that race
687+
* condition at the top of heapam_visibility.c)
688+
*/
689+
if (TransactionIdIsInProgress(xid))
681690
status = "in progress";
682691
else if (TransactionIdDidCommit(xid))
683692
status = "committed";
684-
else if (TransactionIdDidAbort(xid))
685-
status = "aborted";
686693
else
687694
{
688-
/*
689-
* The xact is not marked as either committed or aborted in clog.
690-
*
691-
* It could be a transaction that ended without updating clog or
692-
* writing an abort record due to a crash. We can safely assume
693-
* it's aborted if it isn't committed and is older than our
694-
* snapshot xmin.
695-
*
696-
* Otherwise it must be in-progress (or have been at the time we
697-
* checked commit/abort status).
698-
*/
699-
if (TransactionIdPrecedes(xid, GetActiveSnapshot()->xmin))
700-
status = "aborted";
701-
else
702-
status = "in progress";
695+
/* it must have aborted or crashed */
696+
status = "aborted";
703697
}
704698
}
705699
else

0 commit comments

Comments
 (0)