8000 Fix race condition in preparing a transaction for two-phase commit. · MattPerron/postgres@045fbbd · GitHub
[go: up one dir, main page]

Skip to content

Commit 045fbbd

Browse files
committed
Fix race condition in preparing a transaction for two-phase commit.
To lock a prepared transaction's shared memory entry, we used to mark it with the XID of the backend. When the XID was no longer active according to the proc array, the entry was implicitly considered as not locked anymore. However, when preparing a transaction, the backend's proc array entry was cleared before transfering the locks (and some other state) to the prepared transaction's dummy PGPROC entry, so there was a window where another backend could finish the transaction before it was in fact fully prepared. To fix, rewrite the locking mechanism of global transaction entries. Instead of an XID, just have simple locked-or-not flag in each entry (we store the locking backend's backend id rather than a simple boolean, but that's just for debugging purposes). The backend is responsible for explicitly unlocking the entry, and to make sure that that happens, install a callback to unlock it on abort or process exit. Backpatch to all supported versions.
1 parent 52d2f88 commit 045fbbd

File tree

3 files changed

+144
-47
lines changed

3 files changed

+144
-47
lines changed

src/backend/access/transam/twophase.c

Lines changed: 125 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
#include "pg_trace.h"
5757
#include "pgstat.h"
5858
#include "storage/fd.h"
59+
#include "storage/ipc.h"
5960
#include "storage/procarray.h"
6061
#include "storage/smgr.h"
6162
#include "utils/builtins.h"
@@ -81,25 +82,25 @@ int max_prepared_xacts = 0;
8182
*
8283
* The lifecycle of a global transaction is:
8384
*
84-
* 1. After checking that the requested GID is not in use, set up an
85-
* entry in the TwoPhaseState->prepXacts array with the correct XID and GID,
86-
* with locking_xid = my own XID and valid = false.
85+
* 1. After checking that the requested GID is not in use, set up an entry in
86+
* the TwoPhaseState->prepXacts array with the correct GID and valid = false,
87+
* and mark it as locked by my backend.
8788
*
8889
* 2. After successfully completing prepare, set valid = true and enter the
8990
* contained PGPROC into the global ProcArray.
9091
*
91-
* 3. To begin COMMIT PREPARED or ROLLBACK PREPARED, check that the entry
92-
* is valid and its locking_xid is no longer active, then store my current
93-
* XID into locking_xid. This prevents concurrent attempts to commit or
94-
* rollback the same prepared xact.
92+
* 3. To begin COMMIT PREPARED or ROLLBACK PREPARED, check that the entry is
93+
* valid and not locked, then mark the entry as locked by storing my current
94+
* backend ID into locking_backend. This prevents concurrent attempts to
95+
* commit or rollback the same prepared xact.
9596
*
9697
* 4. On completion of COMMIT PREPARED or ROLLBACK PREPARED, remove the entry
9798
* from the ProcArray and the TwoPhaseState->prepXacts array and return it to
9899
* the freelist.
99100
*
100101
* Note that if the preparing transaction fails between steps 1 and 2, the
101-
* entry will remain in prepXacts until recycled. We can detect recyclable
102-
* entries by checking for valid = false and locking_xid no longer active.
102+
* entry must be removed so that the GID and the GlobalTransaction struct
103+
* can be reused. See AtAbort_Twophase().
103104
*
104105
* typedef struct GlobalTransactionData *GlobalTransaction appears in
105106
* twophase.h
@@ -113,8 +114,8 @@ typedef struct GlobalTransactionData
113114
TimestampTz prepared_at; /* time of preparation */
114115
XLogRecPtr prepare_lsn; /* XLOG offset of prepare record */
115116
Oid owner; /* ID of user that executed the xact */
116-
TransactionId locking_xid; /* top-level XID of backend working on xact */
117-
bool valid; /* TRUE if fully prepared */
117+
BackendId locking_backend; /* backend currently working on the xact */
118+
bool valid; /* TRUE if PGPROC entry is in proc array */
118119
char gid[GIDSIZE]; /* The GID assigned to the prepared xact */
119120
} GlobalTransactionData;
120121

@@ -139,6 +140,12 @@ typedef struct TwoPhaseStateData
139140

140141
static TwoPhaseStateData *TwoPhaseState;
141142

143+
/*
144+
* Global transaction entry currently locked by us, if any.
145+
*/
146+
static GlobalTransaction MyLockedGxact = NULL;
147+
148+
static bool twophaseExitRegistered = false;
142149

143150
static void RecordTransactionCommitPrepared(TransactionId xid,
144151
int nchildren,
@@ -152,6 +159,7 @@ static void RecordTransactionAbortPrepared(TransactionId xid,
152159
RelFileNode *rels);
153160
static void ProcessRecords(char *bufptr, TransactionId xid,
154161
const TwoPhaseCallback callbacks[]);
162+
static void RemoveGXact(GlobalTransaction gxact);
155163

156164

157165
/*
@@ -221,6 +229,74 @@ TwoPhaseShmemInit(void)
221229
Assert(found);
222230
}
223231

232+
/*
233+
* Exit hook to unlock the global transaction entry we're working on.
234+
*/
235+
static void
236+
AtProcExit_Twophase(int code, Datum arg)
237+
{
238+
/* same logic as abort */
239+
AtAbort_Twophase();
240+
}
241+
242+
/*
243+
* Abort hook to unlock the global transaction entry we're working on.
244+
*/
245+
void
246+
AtAbort_Twophase(void)
247+
{
248+
if (MyLockedGxact == NULL)
249+
return;
250+
251+
/*
252+
* What to do with the locked global transaction entry? If we were in
253+
* the process of preparing the transaction, but haven't written the WAL
254+
* record and state file yet, the transaction must not be considered as
255+
* prepared. Likewise, if we are in the process of finishing an
256+
* already-prepared transaction, and fail after having already written
257+
* the 2nd phase commit or rollback record to the WAL, the transaction
258+
* should not be considered as prepared anymore. In those cases, just
259+
* remove the entry from shared memory.
260+
*
261+
* Otherwise, the entry must be left in place so that the transaction
262+
* can be finished later, so just unlock it.
263+
*
264+
* If we abort during prepare, after having written the WAL record, we
265+
* might not have transfered all locks and other state to the prepared
266+
* transaction yet. Likewise, if we abort during commit or rollback,
267+
* after having written the WAL record, we might not have released
268+
* all the resources held by the transaction yet. In those cases, the
269+
* in-memory state can be wrong, but it's too late to back out.
270+
*/
271+
if (!MyLockedGxact->valid)
272+
{
273+
RemoveGXact(MyLockedGxact);
274+
}
275+
else
276+
{
277+
LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);
278+
279+
MyLockedGxact->locking_backend = InvalidBackendId;
280+
281+
LWLockRelease(TwoPhaseStateLock);
282+
}
283+
MyLockedGxact = NULL;
284+
}
285+
286+
/*
287+
* This is called after we have finished transfering state to the prepared
288+
* PGXACT entry.
289+
*/
290+
void
291+
PostPrepare_Twophase()
292+
{
293+
LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);
294+
MyLockedGxact->locking_backend = InvalidBackendId;
295+
LWLockRelease(TwoPhaseStateLock);
296+
297+
MyLockedGxact = NULL;
298+
}
299+
224300

225301
/*
226302
* MarkAsPreparing
@@ -250,29 +326,15 @@ MarkAsPreparing(TransactionId xid, const char *gid,
250326
errmsg("prepared transactions are disabled"),
251327
errhint("Set max_prepared_transactions to a nonzero value.")));
252328

253-
LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);
254-
255-
/*
256-
* First, find and recycle any gxacts that failed during prepare. We do
257-
* this partly to ensure we don't mistakenly say their GIDs are still
258-
* reserved, and partly so we don't fail on out-of-slots unnecessarily.
259-
*/
260-
for (i = 0; i < TwoPhaseState->numPrepXacts; i++)
329+
/* on first call, register the exit hook */
330+
if (!twophaseExitRegistered)
261331
{
262-
gxact = TwoPhaseState->prepXacts[i];
263-
if (!gxact->valid && !TransactionIdIsActive(gxact->locking_xid))
264-
{
265-
/* It's dead Jim ... remove from the active array */
266-
TwoPhaseState->numPrepXacts--;
267-
TwoPhaseState->prepXacts[i] = TwoPhaseState->prepXacts[TwoPhaseState->numPrepXacts];
268-
/* and put it back in the freelist */
269-
gxact->proc.links.next = (SHM_QUEUE *) TwoPhaseState->freeGXacts;
270-
TwoPhaseState->freeGXacts = gxact;
271-
/* Back up index count too, so we don't miss scanning one */
272-
i--;
273-
}
332+
on_shmem_exit(AtProcExit_Twophase, 0);
333+
twophaseExitRegistered = true;
274334
}
275335

336+
LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);
337+
276338
/* Check for conflicting GID */
277339
for (i = 0; i < TwoPhaseState->numPrepXacts; i++)
278340
{
@@ -326,14 +388,20 @@ MarkAsPreparing(TransactionId xid, const char *gid,
326388
gxact->prepare_lsn.xlogid = 0;
327389
gxact->prepare_lsn.xrecoff = 0;
328390
gxact->owner = owner;
329-
gxact->locking_xid = xid;
391+
gxact->locking_backend = MyBackendId;
330392
gxact->valid = false;
331393
strcpy(gxact->gid, gid);
332394

333395
/* And insert it into the active array */
334396
Assert(TwoPhaseState->numPrepXacts < max_prepared_xacts);
335397
TwoPhaseState->prepXacts[TwoPhaseState->numPrepXacts++] = gxact;
336398

399+
/*
400+
* Remember that we have this GlobalTransaction entry locked for us.
401+
* If we abort after this, we must release it.
402+
*/
403+
MyLockedGxact = gxact;
404+
337405
LWLockRelease(TwoPhaseStateLock);
338406

339407
return gxact;
@@ -393,6 +461,13 @@ LockGXact(const char *gid, Oid user)
393461
{
394462
int i;
395463

464+
/* on first call, register the exit hook */
465+
if (!twophaseExitRegistered)
466+
{
467+
on_shmem_exit(AtProcExit_Twophase, 0);
468+
twophaseExitRegistered = true;
469+
}
470+
396471
LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);
397472

398473
for (i = 0; i < TwoPhaseState->numPrepXacts; i++)
@@ -406,15 +481,11 @@ LockGXact(const char *gid, Oid user)
406481
continue;
407482

408483
/* Found it, but has someone else got it locked? */
409-
if (TransactionIdIsValid(gxact->locking_xid))
410-
{
411-
if (TransactionIdIsActive(gxact->locking_xid))
412-
ereport(ERROR,
413-
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
414-
errmsg("prepared transaction with identifier \"%s\" is busy",
415-
gid)));
416-
gxact->locking_xid = InvalidTransactionId;
417-
}
484+
if (gxact->locking_backend != InvalidBackendId)
485+
ereport(ERROR,
486+
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
487+
errmsg("prepared transaction with identifier \"%s\" is busy",
488+
gid)));
418489

419490
if (user != gxact->owner && !superuser_arg(user))
420491
ereport(ERROR,
@@ -435,7 +506,8 @@ LockGXact(const char *gid, Oid user)
435506
errhint("Connect to the database where the transaction was prepared to finish it.")));
436507

437508
/* OK for me to lock it */
438-
gxact->locking_xid = GetTopTransactionId();
509+
gxact->locking_backend = MyBackendId;
510+
MyLockedGxact = gxact;
439511

440512
LWLockRelease(TwoPhaseStateLock);
441513

@@ -1041,6 +1113,13 @@ EndPrepare(GlobalTransaction gxact)
10411113
*/
10421114
MyProc->inCommit = false;
10431115

1116+
/*
1117+
* Remember that we have this GlobalTransaction entry locked for us. If
1118+
* we crash after this point, it's too late to abort, but we must unlock
1119+
* it so that the prepared transaction can be committed or rolled back.
1120+
*/
1121+
MyLockedGxact = gxact;
1122+
10441123
END_CRIT_SECTION();
10451124

10461125
records.tail = records.head = NULL;
@@ -1240,8 +1319,9 @@ FinishPreparedTransaction(const char *gid, bool isCommit)
12401319

12411320
/*
12421321
* In case we fail while running the callbacks, mark the gxact invalid so
1243-
* no one else will try to commit/rollback, and so it can be recycled
1244-
* properly later. It is still locked by our XID so it won't go away yet.
1322+
* no one else will try to commit/rollback, and so it will be recycled
1323+
* if we fail after this point. It is still locked by our backend so it
1324+
* won't go away yet.
12451325
*
12461326
* (We assume it's safe to do this without taking TwoPhaseStateLock.)
12471327
*/
@@ -1291,6 +1371,7 @@ FinishPreparedTransaction(const char *gid, bool isCommit)
12911371
RemoveTwoPhaseFile(xid, true);
12921372

12931373
RemoveGXact(gxact);
1374+
MyLockedGxact = NULL;
12941375

12951376
pfree(buf);
12961377
}

src/backend/access/transam/xact.c

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,9 +1912,13 @@ PrepareTransaction(void)
19121912
ProcArrayClearTransaction(MyProc);
19131913

19141914
/*
1915-
* This is all post-transaction cleanup. Note that if an error is raised
1916-
* here, it's too late to abort the transaction. This should be just
1917-
* noncritical resource releasing. See notes in CommitTransaction.
1915+
* In normal commit-processing, this is all non-critical post-transaction
1916+
* cleanup. When the transaction is prepared, however, it's important that
1917+
* the locks and other per-backend resources are transfered to the
1918+
* prepared transaction's PGPROC entry. Note that if an error is raised
1919+
* here, it's too late to abort the transaction. XXX: This probably should
1920+
* be in a critical section, to force a PANIC if any of this fails, but
1921+
* that cure could be worse than the disease.
19181922
*/
19191923

19201924
CallXactCallbacks(XACT_EVENT_PREPARE);
@@ -1951,6 +1955,14 @@ PrepareTransaction(void)
19511955
RESOURCE_RELEASE_AFTER_LOCKS,
19521956
true, true);
19531957

1958+
/*
1959+
* Allow another backend to finish the transaction. After
1960+
* PostPrepare_Twophase(), the transaction is completely detached from
1961+
* our backend. The rest is just non-critical cleanup of backend-local
1962+
* state.
1963+
*/
1964+
PostPrepare_Twophase();
1965+
19541966
/* Check we've released all catcache entries */
19551967
AtEOXact_CatCache(true);
19561968

@@ -2061,6 +2073,7 @@ AbortTransaction(void)
20612073
AtEOXact_LargeObject(false); /* 'false' means it's abort */
20622074
AtAbort_Notify();
20632075
AtEOXact_UpdateFlatFiles(false);
2076+
AtAbort_Twophase();
20642077

20652078
/*
20662079
* Advertise the fact that we aborted in pg_clog (assuming that we got as

src/include/access/twophase.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ extern int max_prepared_xacts;
3131
extern Size TwoPhaseShmemSize(void);
3232
extern void TwoPhaseShmemInit(void);
3333

34+
extern void AtAbort_Twophase(void);
35+
extern void PostPrepare_Twophase(void);
36+
3437
extern PGPROC *TwoPhaseGetDummyProc(TransactionId xid);
3538
extern BackendId TwoPhaseGetDummyBackendId(TransactionId xid);
3639

0 commit comments

Comments
 (0)
0