Preserve CurrentMemoryContext across Start/CommitTransactionCommand.

Up to now, committing a transaction has caused CurrentMemoryContext to
get set to TopMemoryContext.  Most callers did not pay any particular
heed to this, which is problematic because TopMemoryContext is a
long-lived context that never gets reset.  If the caller assumes it
can leak memory because it's running in a limited-lifespan context,
that behavior translates into a session-lifespan memory leak.

The first-reported instance of this involved ProcessIncomingNotify,
which is called from the main processing loop that normally runs in
MessageContext.  That outer-loop code assumes that whatever it
allocates will be cleaned up when we're done processing the current
client message --- but if we service a notify interrupt, then whatever
gets allocated before the next switch to MessageContext will be
permanently leaked in TopMemoryContext.  sinval catchup interrupts
have a similar problem, and I strongly suspect that some places in
logical replication do too.

To fix this in a generic way, let's redefine the behavior as
"CommitTransactionCommand restores the memory context that was current
at entry to StartTransactionCommand".  This clearly fixes the issue
for the notify and sinval cases, and it seems to match the mental
model that's in use in the logical replication code, to the extent
that anybody thought about it there at all.

For consistency, likewise make subtransaction exit restore the context
that was current at subtransaction start (rather than always selecting
the CurTransactionContext of the parent transaction level).  This case
has less risk of resulting in a permanent leak than the outer-level
behavior has, but it would not meet the principle of least surprise
for some CommitTransactionCommand calls to restore the previous
context while others don't.

While we're here, also change xact.c so that we reset
TopTransactionContext at transaction exit and then re-use it in later
transactions, rather than dropping and recreating it in each cycle.
This probably doesn't save a lot given the context recycling mechanism
in aset.c, but it should save a little bit.  Per suggestion from David
Rowley.  (Parenthetically, the text in src/backend/utils/mmgr/README
implies that this is how I'd planned to implement it as far back as
commit 1aebc3618 --- but the code actually added in that commit just
drops and recreates it each time.)

This commit also cleans up a few places outside xact.c that were
needlessly making TopMemoryContext current, and thus risking more
leaks of the same kind.  I don't think any of them represent
significant leak risks today, but let's deal with them while the
issue is top-of-mind.

Per bug #18512 from wizardbrony.  Commit to HEAD only, as this change
seems to have some risk of breaking things for some callers.  We'll
apply a narrower fix for the known-broken cases in the back branches.

Discussion: https://postgr.es/m/3478884.1718656625@sss.pgh.pa.us
This commit is contained in:
Tom Lane 2024-07-01 11:55:19 -04:00
parent 6e16b1e420
commit 1afe31f03c
5 changed files with 82 additions and 42 deletions

View File

@ -200,6 +200,7 @@ typedef struct TransactionStateData
int gucNestLevel; /* GUC context nesting depth */ int gucNestLevel; /* GUC context nesting depth */
MemoryContext curTransactionContext; /* my xact-lifetime context */ MemoryContext curTransactionContext; /* my xact-lifetime context */
ResourceOwner curTransactionOwner; /* my query resources */ ResourceOwner curTransactionOwner; /* my query resources */
MemoryContext priorContext; /* CurrentMemoryContext before xact started */
TransactionId *childXids; /* subcommitted child XIDs, in XID order */ TransactionId *childXids; /* subcommitted child XIDs, in XID order */
int nChildXids; /* # of subcommitted child XIDs */ int nChildXids; /* # of subcommitted child XIDs */
int maxChildXids; /* allocated size of childXids[] */ int maxChildXids; /* allocated size of childXids[] */
@ -1174,6 +1175,11 @@ AtStart_Memory(void)
{ {
TransactionState s = CurrentTransactionState; TransactionState s = CurrentTransactionState;
/*
* Remember the memory context that was active prior to transaction start.
*/
s->priorContext = CurrentMemoryContext;
/* /*
* If this is the first time through, create a private context for * If this is the first time through, create a private context for
* AbortTransaction to work in. By reserving some space now, we can * AbortTransaction to work in. By reserving some space now, we can
@ -1190,17 +1196,15 @@ AtStart_Memory(void)
32 * 1024); 32 * 1024);
/* /*
* We shouldn't have a transaction context already. * Likewise, if this is the first time through, create a top-level context
* for transaction-local data. This context will be reset at transaction
* end, and then re-used in later transactions.
*/ */
Assert(TopTransactionContext == NULL); if (TopTransactionContext == NULL)
TopTransactionContext =
/* AllocSetContextCreate(TopMemoryContext,
* Create a toplevel context for the transaction. "TopTransactionContext",
*/ ALLOCSET_DEFAULT_SIZES);
TopTransactionContext =
AllocSetContextCreate(TopMemoryContext,
"TopTransactionContext",
ALLOCSET_DEFAULT_SIZES);
/* /*
* In a top-level transaction, CurTransactionContext is the same as * In a top-level transaction, CurTransactionContext is the same as
@ -1251,6 +1255,11 @@ AtSubStart_Memory(void)
Assert(CurTransactionContext != NULL); Assert(CurTransactionContext != NULL);
/*
* Remember the context that was active prior to subtransaction start.
*/
s->priorContext = CurrentMemoryContext;
/* /*
* Create a CurTransactionContext, which will be used to hold data that * Create a CurTransactionContext, which will be used to hold data that
* survives subtransaction commit but disappears on subtransaction abort. * survives subtransaction commit but disappears on subtransaction abort.
@ -1576,20 +1585,30 @@ AtCCI_LocalCache(void)
static void static void
AtCommit_Memory(void) AtCommit_Memory(void)
{ {
/* TransactionState s = CurrentTransactionState;
* Now that we're "out" of a transaction, have the system allocate things
* in the top memory context instead of per-transaction contexts.
*/
MemoryContextSwitchTo(TopMemoryContext);
/* /*
* Release all transaction-local memory. * Return to the memory context that was current before we started the
* transaction. (In principle, this could not be any of the contexts we
* are about to delete. If it somehow is, assertions in mcxt.c will
* complain.)
*/
MemoryContextSwitchTo(s->priorContext);
/*
* Release all transaction-local memory. TopTransactionContext survives
* but becomes empty; any sub-contexts go away.
*/ */
Assert(TopTransactionContext != NULL); Assert(TopTransactionContext != NULL);
MemoryContextDelete(TopTransactionContext); MemoryContextReset(TopTransactionContext);
TopTransactionContext = NULL;
/*
* Clear these pointers as a pro-forma matter. (Notionally, while
* TopTransactionContext still exists, it's currently not associated with
* this TransactionState struct.)
*/
CurTransactionContext = NULL; CurTransactionContext = NULL;
CurrentTransactionState->curTransactionContext = NULL; s->curTransactionContext = NULL;
} }
/* ---------------------------------------------------------------- /* ----------------------------------------------------------------
@ -1942,13 +1961,18 @@ AtSubAbort_childXids(void)
static void static void
AtCleanup_Memory(void) AtCleanup_Memory(void)
{ {
Assert(CurrentTransactionState->parent == NULL); TransactionState s = CurrentTransactionState;
/* Should be at top level */
Assert(s->parent == NULL);
/* /*
* Now that we're "out" of a transaction, have the system allocate things * Return to the memory context that was current before we started the
* in the top memory context instead of per-transaction contexts. * transaction. (In principle, this could not be any of the contexts we
* are about to delete. If it somehow is, assertions in mcxt.c will
* complain.)
*/ */
MemoryContextSwitchTo(TopMemoryContext); MemoryContextSwitchTo(s->priorContext);
/* /*
* Clear the special abort context for next time. * Clear the special abort context for next time.
@ -1957,13 +1981,20 @@ AtCleanup_Memory(void)
MemoryContextReset(TransactionAbortContext); MemoryContextReset(TransactionAbortContext);
/* /*
* Release all transaction-local memory. * Release all transaction-local memory, the same as in AtCommit_Memory,
* except we must cope with the possibility that we didn't get as far as
* creating TopTransactionContext.
*/ */
if (TopTransactionContext != NULL) if (TopTransactionContext != NULL)
MemoryContextDelete(TopTransactionContext); MemoryContextReset(TopTransactionContext);
TopTransactionContext = NULL;
/*
* Clear these pointers as a pro-forma matter. (Notionally, while
* TopTransactionContext still exists, it's currently not associated with
* this TransactionState struct.)
*/
CurTransactionContext = NULL; CurTransactionContext = NULL;
CurrentTransactionState->curTransactionContext = NULL; s->curTransactionContext = NULL;
} }
@ -1982,8 +2013,15 @@ AtSubCleanup_Memory(void)
Assert(s->parent != NULL); Assert(s->parent != NULL);
/* Make sure we're not in an about-to-be-deleted context */ /*
MemoryContextSwitchTo(s->parent->curTransactionContext); * Return to the memory context that was current before we started the
* subtransaction. (In principle, this could not be any of the contexts
* we are about to delete. If it somehow is, assertions in mcxt.c will
* complain.)
*/
MemoryContextSwitchTo(s->priorContext);
/* Update CurTransactionContext (might not be same as priorContext) */
CurTransactionContext = s->parent->curTransactionContext; CurTransactionContext = s->parent->curTransactionContext;
/* /*
@ -4904,8 +4942,13 @@ AbortOutOfAnyTransaction(void)
/* Should be out of all subxacts now */ /* Should be out of all subxacts now */
Assert(s->parent == NULL); Assert(s->parent == NULL);
/* If we didn't actually have anything to do, revert to TopMemoryContext */ /*
AtCleanup_Memory(); * Revert to TopMemoryContext, to ensure we exit in a well-defined state
* whether there were any transactions to close or not. (Callers that
* don't intend to exit soon should switch to some other context to avoid
* long-term memory leaks.)
*/
MemoryContextSwitchTo(TopMemoryContext);
} }
/* /*

View File

@ -440,12 +440,10 @@ IdentifySystem(void)
/* syscache access needs a transaction env. */ /* syscache access needs a transaction env. */
StartTransactionCommand(); StartTransactionCommand();
/* make dbname live outside TX context */
MemoryContextSwitchTo(cur);
dbname = get_database_name(MyDatabaseId); dbname = get_database_name(MyDatabaseId);
/* copy dbname out of TX context */
dbname = MemoryContextStrdup(cur, dbname);
CommitTransactionCommand(); CommitTransactionCommand();
/* CommitTransactionCommand switches to TopMemoryContext */
MemoryContextSwitchTo(cur);
} }
dest = CreateDestReceiver(DestRemoteSimple); dest = CreateDestReceiver(DestRemoteSimple);

View File

@ -196,9 +196,8 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
* Save remote_host and remote_port in port structure (after this, they * Save remote_host and remote_port in port structure (after this, they
* will appear in log_line_prefix data for log messages). * will appear in log_line_prefix data for log messages).
*/ */
oldcontext = MemoryContextSwitchTo(TopMemoryContext); port->remote_host = MemoryContextStrdup(TopMemoryContext, remote_host);
port->remote_host = pstrdup(remote_host); port->remote_port = MemoryContextStrdup(TopMemoryContext, remote_port);
port->remote_port = pstrdup(remote_port);
/* And now we can issue the Log_connections message, if wanted */ /* And now we can issue the Log_connections message, if wanted */
if (Log_connections) if (Log_connections)
@ -230,9 +229,8 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
strspn(remote_host, "0123456789.") < strlen(remote_host) && strspn(remote_host, "0123456789.") < strlen(remote_host) &&
strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host)) strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
{ {
port->remote_hostname = pstrdup(remote_host); port->remote_hostname = MemoryContextStrdup(TopMemoryContext, remote_host);
} }
MemoryContextSwitchTo(oldcontext);
/* /*
* Ready to begin client interaction. We will give up and _exit(1) after * Ready to begin client interaction. We will give up and _exit(1) after

View File

@ -4418,7 +4418,7 @@ PostgresMain(const char *dbname, const char *username)
* Now return to normal top-level context and clear ErrorContext for * Now return to normal top-level context and clear ErrorContext for
* next time. * next time.
*/ */
MemoryContextSwitchTo(TopMemoryContext); MemoryContextSwitchTo(MessageContext);
FlushErrorState(); FlushErrorState();
/* /*

View File

@ -1148,7 +1148,8 @@ MemoryContextAllocationFailure(MemoryContext context, Size size, int flags)
{ {
if ((flags & MCXT_ALLOC_NO_OOM) == 0) if ((flags & MCXT_ALLOC_NO_OOM) == 0)
{ {
MemoryContextStats(TopMemoryContext); if (TopMemoryContext)
MemoryContextStats(TopMemoryContext);
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY), (errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of memory"), errmsg("out of memory"),