Skip to content

Commit a6b1f53

Browse files
committed
Fix memory leak in plpgsql's CALL processing.
When executing a CALL or DO in a non-atomic context (i.e., not inside a function or query), plpgsql creates a new plan each time through, as a rather hacky solution to some resource management issues. But it failed to free this plan until exit of the current procedure or DO block, resulting in serious memory bloat in procedures that called other procedures many times. Fix by remembering to free the plan, and by being more honest about restoring the previous state (otherwise, recursive procedure calls have a problem). There was also a smaller leak associated with recalculation of the "target" list of output variables. Fix that by using the statement- lifespan context to hold non-permanent values. Back-patch to v11 where procedures were introduced. Pavel Stehule and Tom Lane Discussion: https://postgr.es/m/CAFj8pRDiiU1dqym+_P4_GuTWm76knJu7z9opWayBJTC0nQGUUA@mail.gmail.com
1 parent 927d9ab commit a6b1f53

File tree

1 file changed

+70
-21
lines changed

1 file changed

+70
-21
lines changed

src/pl/plpgsql/src/pl_exec.c

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2145,55 +2145,82 @@ exec_stmt_perform(PLpgSQL_execstate *estate, PLpgSQL_stmt_perform *stmt)
21452145

21462146
/*
21472147
* exec_stmt_call
2148+
*
2149+
* NOTE: this is used for both CALL and DO statements.
21482150
*/
21492151
static int
21502152
exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
21512153
{
21522154
PLpgSQL_expr *expr = stmt->expr;
2155+
SPIPlanPtr orig_plan = expr->plan;
2156+
bool local_plan;
2157+
PLpgSQL_variable *volatile cur_target = stmt->target;
21532158
volatile LocalTransactionId before_lxid;
21542159
LocalTransactionId after_lxid;
21552160
volatile bool pushed_active_snap = false;
21562161
volatile int rc;
21572162

2163+
/*
2164+
* If not in atomic context, we make a local plan that we'll just use for
2165+
* this invocation, and will free at the end. Otherwise, transaction ends
2166+
* would cause errors about plancache leaks.
2167+
*
2168+
* XXX This would be fixable with some plancache/resowner surgery
2169+
* elsewhere, but for now we'll just work around this here.
2170+
*/
2171+
local_plan = !estate->atomic;
2172+
21582173
/* PG_TRY to ensure we clear the plan link, if needed, on failure */
21592174
PG_TRY();
21602175
{
21612176
SPIPlanPtr plan = expr->plan;
21622177
ParamListInfo paramLI;
21632178

2164-
if (plan == NULL)
2179+
/*
2180+
* Make a plan if we don't have one, or if we need a local one. Note
2181+
* that we'll overwrite expr->plan either way; the PG_TRY block will
2182+
* ensure we undo that on the way out, if the plan is local.
2183+
*/
2184+
if (plan == NULL || local_plan)
21652185
{
2186+
/* Don't let SPI save the plan if it's going to be local */
2187+
exec_prepare_plan(estate, expr, 0, !local_plan);
2188+
plan = expr->plan;
21662189

21672190
/*
2168-
* Don't save the plan if not in atomic context. Otherwise,
2169-
* transaction ends would cause errors about plancache leaks.
2170-
*
2171-
* XXX This would be fixable with some plancache/resowner surgery
2172-
* elsewhere, but for now we'll just work around this here.
2191+
* A CALL or DO can never be a simple expression. (If it could
2192+
* be, we'd have to worry about saving/restoring the previous
2193+
* values of the related expr fields, not just expr->plan.)
21732194
*/
2174-
exec_prepare_plan(estate, expr, 0, estate->atomic);
2195+
Assert(!expr->expr_simple_expr);
21752196

21762197
/*
21772198
* The procedure call could end transactions, which would upset
21782199
* the snapshot management in SPI_execute*, so don't let it do it.
21792200
* Instead, we set the snapshots ourselves below.
21802201
*/
2181-
plan = expr->plan;
21822202
plan->no_snapshots = true;
21832203

21842204
/*
21852205
* Force target to be recalculated whenever the plan changes, in
21862206
* case the procedure's argument list has changed.
21872207
*/
21882208
stmt->target = NULL;
2209+
cur_target = NULL;
21892210
}
21902211

21912212
/*
21922213
* We construct a DTYPE_ROW datum representing the plpgsql variables
21932214
* associated with the procedure's output arguments. Then we can use
21942215
* exec_move_row() to do the assignments.
2216+
*
2217+
* If we're using a local plan, also make a local target; otherwise,
2218+
* since the above code will force a new plan each time through, we'd
2219+
* repeatedly leak the memory for the target. (Note: we also leak the
2220+
* target when a plan change is forced, but that isn't so likely to
2221+
* cause excessive memory leaks.)
21952222
*/
2196-
if (stmt->is_call && stmt->target == NULL)
2223+
if (stmt->is_call && cur_target == NULL)
21972224
{
21982225
Node *node;
21992226
FuncExpr *funcexpr;
@@ -2208,6 +2235,9 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
22082235
int i;
22092236
ListCell *lc;
22102237

2238+
/* Use eval_mcontext for any cruft accumulated here */
2239+
oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
2240+
22112241
/*
22122242
* Get the parsed CallStmt, and look up the called procedure
22132243
*/
@@ -2239,17 +2269,20 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
22392269
ReleaseSysCache(func_tuple);
22402270

22412271
/*
2242-
* Begin constructing row Datum
2272+
* Begin constructing row Datum; keep it in fn_cxt if it's to be
2273+
* long-lived.
22432274
*/
2244-
oldcontext = MemoryContextSwitchTo(estate->func->fn_cxt);
2275+
if (!local_plan)
2276+
MemoryContextSwitchTo(estate->func->fn_cxt);
22452277

22462278
row = (PLpgSQL_row *) palloc0(sizeof(PLpgSQL_row));
22472279
row->dtype = PLPGSQL_DTYPE_ROW;
22482280
row->refname = "(unnamed row)";
22492281
row->lineno = -1;
22502282
row->varnos = (int *) palloc(sizeof(int) * list_length(funcargs));
22512283

2252-
MemoryContextSwitchTo(oldcontext);
2284+
if (!local_plan)
2285+
MemoryContextSwitchTo(get_eval_mcontext(estate));
22532286

22542287
/*
22552288
* Examine procedure's argument list. Each output arg position
@@ -2293,7 +2326,13 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
22932326

22942327
row->nfields = nfields;
22952328

2296-
stmt->target = (PLpgSQL_variable *) row;
2329+
cur_target = (PLpgSQL_variable *) row;
2330+
2331+
/* We can save and re-use the target datum, if it's not local */
2332+
if (!local_plan)
2333+
stmt->target = cur_target;
2334+
2335+
MemoryContextSwitchTo(oldcontext);
22972336
}
22982337

22992338
paramLI = setup_param_list(estate, expr);
@@ -2316,17 +2355,27 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
23162355
PG_CATCH();
23172356
{
23182357
/*
2319-
* If we aren't saving the plan, unset the pointer. Note that it
2320-
* could have been unset already, in case of a recursive call.
2358+
* If we are using a local plan, restore the old plan link.
23212359
*/
2322-
if (expr->plan && !expr->plan->saved)
2323-
expr->plan = NULL;
2360+
if (local_plan)
2361+
expr->plan = orig_plan;
23242362
PG_RE_THROW();
23252363
}
23262364
PG_END_TRY();
23272365

2328-
if (expr->plan && !expr->plan->saved)
2329-
expr->plan = NULL;
2366+
/*
2367+
* If we are using a local plan, restore the old plan link; then free the
2368+
* local plan to avoid memory leaks. (Note that the error exit path above
2369+
* just clears the link without risking calling SPI_freeplan; we expect
2370+
* that xact cleanup will take care of the mess in that case.)
2371+
*/
2372+
if (local_plan)
2373+
{
2374+
SPIPlanPtr plan = expr->plan;
2375+
2376+
expr->plan = orig_plan;
2377+
SPI_freeplan(plan);
2378+
}
23302379

23312380
if (rc < 0)
23322381
elog(ERROR, "SPI_execute_plan_with_paramlist failed executing query \"%s\": %s",
@@ -2363,10 +2412,10 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
23632412
{
23642413
SPITupleTable *tuptab = SPI_tuptable;
23652414

2366-
if (!stmt->target)
2415+
if (!cur_target)
23672416
elog(ERROR, "DO statement returned a row");
23682417

2369-
exec_move_row(estate, stmt->target, tuptab->vals[0], tuptab->tupdesc);
2418+
exec_move_row(estate, cur_target, tuptab->vals[0], tuptab->tupdesc);
23702419
}
23712420
else if (SPI_processed > 1)
23722421
elog(ERROR, "procedure call returned more than one row");

0 commit comments

Comments
 (0)