Make release of 2PC identifier and locks consistent in COMMIT PREPARED

When preparing a transaction in two-phase commit, a dummy PGPROC entry
holding the GID used for the transaction is registered, which gets
released once COMMIT PREPARED is run.  Prior releasing its shared memory
state, all the locks taken in the prepared transaction are released
using a dedicated set of callbacks (pgstat and multixact having similar
callbacks), which may cause the locks to be released before the GID is
set free.

Hence, there is a small window where lock conflicts could happen, for
example:
- Transaction A releases its locks, still holding its GID in shared
memory.
- Transaction B held a lock which conflicted with locks of transaction
A.
- Transaction B continues its processing, reusing the same GID as
transaction A.
- Transaction B fails because of a conflicting GID, already in use by
transaction A.

This commit changes the shared memory state release so as post-commit
callbacks and predicate lock cleanup happen consistently with the shared
memory state cleanup for the dummy PGPROC entry.  The race window is
small and 2PC had this issue from the start, so no backpatch is done.
On top if that fixes discussed involved ABI breakages, which are not
welcome in stable branches.

Reported-by: Oleksii Kliukin, Ildar Musin
Diagnosed-by: Oleksii Kliukin, Ildar Musin
Author: Michael Paquier
Reviewed-by: Masahiko Sawada, Oleksii Kliukin
Discussion: https://postgr.es/m/BF9B38A4-2BFF-46E8-BA87-A2D00A8047A6@hintbits.com
This commit is contained in:
Michael Paquier 2019-02-25 14:19:34 +09:00
parent 29ddb548f6
commit effe7d9552
4 changed files with 44 additions and 19 deletions

View File

@ -1713,7 +1713,7 @@ PostPrepare_MultiXact(TransactionId xid)
myOldestMember = OldestMemberMXactId[MyBackendId];
if (MultiXactIdIsValid(myOldestMember))
{
BackendId dummyBackendId = TwoPhaseGetDummyBackendId(xid);
BackendId dummyBackendId = TwoPhaseGetDummyBackendId(xid, false);
/*
* Even though storing MultiXactId is atomic, acquire lock to make
@ -1755,7 +1755,7 @@ void
multixact_twophase_recover(TransactionId xid, uint16 info,
void *recdata, uint32 len)
{
BackendId dummyBackendId = TwoPhaseGetDummyBackendId(xid);
BackendId dummyBackendId = TwoPhaseGetDummyBackendId(xid, false);
MultiXactId oldestMember;
/*
@ -1776,7 +1776,7 @@ void
multixact_twophase_postcommit(TransactionId xid, uint16 info,
void *recdata, uint32 len)
{
BackendId dummyBackendId = TwoPhaseGetDummyBackendId(xid);
BackendId dummyBackendId = TwoPhaseGetDummyBackendId(xid, true);
Assert(len == sizeof(MultiXactId));

View File

@ -801,9 +801,12 @@ pg_prepared_xact(PG_FUNCTION_ARGS)
* TwoPhaseGetGXact
* Get the GlobalTransaction struct for a prepared transaction
* specified by XID
*
* If lock_held is set to true, TwoPhaseStateLock will not be taken, so the
* caller had better hold it.
*/
static GlobalTransaction
TwoPhaseGetGXact(TransactionId xid)
TwoPhaseGetGXact(TransactionId xid, bool lock_held)
{
GlobalTransaction result = NULL;
int i;
@ -811,6 +814,8 @@ TwoPhaseGetGXact(TransactionId xid)
static TransactionId cached_xid = InvalidTransactionId;
static GlobalTransaction cached_gxact = NULL;
Assert(!lock_held || LWLockHeldByMe(TwoPhaseStateLock));
/*
* During a recovery, COMMIT PREPARED, or ABORT PREPARED, we'll be called
* repeatedly for the same XID. We can save work with a simple cache.
@ -818,7 +823,8 @@ TwoPhaseGetGXact(TransactionId xid)
if (xid == cached_xid)
return cached_gxact;
LWLockAcquire(TwoPhaseStateLock, LW_SHARED);
if (!lock_held)
LWLockAcquire(TwoPhaseStateLock, LW_SHARED);
for (i = 0; i < TwoPhaseState->numPrepXacts; i++)
{
@ -832,7 +838,8 @@ TwoPhaseGetGXact(TransactionId xid)
}
}
LWLockRelease(TwoPhaseStateLock);
if (!lock_held)
LWLockRelease(TwoPhaseStateLock);
if (result == NULL) /* should not happen */
elog(ERROR, "failed to find GlobalTransaction for xid %u", xid);
@ -849,12 +856,13 @@ TwoPhaseGetGXact(TransactionId xid)
*
* Dummy backend IDs are similar to real backend IDs of real backends.
* They start at MaxBackends + 1, and are unique across all currently active
* real backends and prepared transactions.
* real backends and prepared transactions. If lock_held is set to true,
* TwoPhaseStateLock will not be taken, so the caller had better hold it.
*/
BackendId
TwoPhaseGetDummyBackendId(TransactionId xid)
TwoPhaseGetDummyBackendId(TransactionId xid, bool lock_held)
{
GlobalTransaction gxact = TwoPhaseGetGXact(xid);
GlobalTransaction gxact = TwoPhaseGetGXact(xid, lock_held);
return gxact->dummyBackendId;
}
@ -862,11 +870,14 @@ TwoPhaseGetDummyBackendId(TransactionId xid)
/*
* TwoPhaseGetDummyProc
* Get the PGPROC that represents a prepared transaction specified by XID
*
* If lock_held is set to true, TwoPhaseStateLock will not be taken, so the
* caller had better hold it.
*/
PGPROC *
TwoPhaseGetDummyProc(TransactionId xid)
TwoPhaseGetDummyProc(TransactionId xid, bool lock_held)
{
GlobalTransaction gxact = TwoPhaseGetGXact(xid);
GlobalTransaction gxact = TwoPhaseGetGXact(xid, lock_held);
return &ProcGlobal->allProcs[gxact->pgprocno];
}
@ -1560,6 +1571,14 @@ FinishPreparedTransaction(const char *gid, bool isCommit)
if (hdr->initfileinval)
RelationCacheInitFilePostInvalidate();
/*
* Acquire the two-phase lock. We want to work on the two-phase callbacks
* while holding it to avoid potential conflicts with other transactions
* attempting to use the same GID, so the lock is released once the shared
* memory state is cleared.
*/
LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);
/* And now do the callbacks */
if (isCommit)
ProcessRecords(bufptr, xid, twophase_postcommit_callbacks);
@ -1568,6 +1587,15 @@ FinishPreparedTransaction(const char *gid, bool isCommit)
PredicateLockTwoPhaseFinish(xid, isCommit);
/* Clear shared memory state */
RemoveGXact(gxact);
/*
* Release the lock as all callbacks are called and shared memory cleanup
* is done.
*/
LWLockRelease(TwoPhaseStateLock);
/* Count the prepared xact as committed or aborted */
AtEOXact_PgStat(isCommit);
@ -1577,9 +1605,6 @@ FinishPreparedTransaction(const char *gid, bool isCommit)
if (gxact->ondisk)
RemoveTwoPhaseFile(xid, true);
LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);
RemoveGXact(gxact);
LWLockRelease(TwoPhaseStateLock);
MyLockedGxact = NULL;
RESUME_INTERRUPTS();

View File

@ -3243,7 +3243,7 @@ AtPrepare_Locks(void)
void
PostPrepare_Locks(TransactionId xid)
{
PGPROC *newproc = TwoPhaseGetDummyProc(xid);
PGPROC *newproc = TwoPhaseGetDummyProc(xid, false);
HASH_SEQ_STATUS status;
LOCALLOCK *locallock;
LOCK *lock;
@ -4034,7 +4034,7 @@ lock_twophase_recover(TransactionId xid, uint16 info,
void *recdata, uint32 len)
{
TwoPhaseLockRecord *rec = (TwoPhaseLockRecord *) recdata;
PGPROC *proc = TwoPhaseGetDummyProc(xid);
PGPROC *proc = TwoPhaseGetDummyProc(xid, false);
LOCKTAG *locktag;
LOCKMODE lockmode;
LOCKMETHODID lockmethodid;
@ -4247,7 +4247,7 @@ lock_twophase_postcommit(TransactionId xid, uint16 info,
void *recdata, uint32 len)
{
TwoPhaseLockRecord *rec = (TwoPhaseLockRecord *) recdata;
PGPROC *proc = TwoPhaseGetDummyProc(xid);
PGPROC *proc = TwoPhaseGetDummyProc(xid, true);
LOCKTAG *locktag;
LOCKMETHODID lockmethodid;
LockMethod lockMethodTable;

View File

@ -34,8 +34,8 @@ extern void TwoPhaseShmemInit(void);
extern void AtAbort_Twophase(void);
extern void PostPrepare_Twophase(void);
extern PGPROC *TwoPhaseGetDummyProc(TransactionId xid);
extern BackendId TwoPhaseGetDummyBackendId(TransactionId xid);
extern PGPROC *TwoPhaseGetDummyProc(TransactionId xid, bool lock_held);
extern BackendId TwoPhaseGetDummyBackendId(TransactionId xid, bool lock_held);
extern GlobalTransaction MarkAsPreparing(TransactionId xid, const char *gid,
TimestampTz prepared_at,