Skip to content

Commit 13a1901

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 462ff79 commit 13a1901

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
@@ -2102,55 +2102,82 @@ exec_stmt_perform(PLpgSQL_execstate *estate, PLpgSQL_stmt_perform *stmt)
21022102

21032103
/*
21042104
* exec_stmt_call
2105+
*
2106+
* NOTE: this is used for both CALL and DO statements.
21052107
*/
21062108
static int
21072109
exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
21082110
{
21092111
PLpgSQL_expr *expr = stmt->expr;
2112+
SPIPlanPtr orig_plan = expr->plan;
2113+
bool local_plan;
2114+
PLpgSQL_variable *volatile cur_target = stmt->target;
21102115
volatile LocalTransactionId before_lxid;
21112116
LocalTransactionId after_lxid;
21122117
volatile bool pushed_active_snap = false;
21132118
volatile int rc;
21142119

2120+
/*
2121+
* If not in atomic context, we make a local plan that we'll just use for
2122+
* this invocation, and will free at the end. Otherwise, transaction ends
2123+
* would cause errors about plancache leaks.
2124+
*
2125+
* XXX This would be fixable with some plancache/resowner surgery
2126+
* elsewhere, but for now we'll just work around this here.
2127+
*/
2128+
local_plan = !estate->atomic;
2129+
21152130
/* PG_TRY to ensure we clear the plan link, if needed, on failure */
21162131
PG_TRY();
21172132
{
21182133
SPIPlanPtr plan = expr->plan;
21192134
ParamListInfo paramLI;
21202135

2121-
if (plan == NULL)
2136+
/*
2137+
* Make a plan if we don't have one, or if we need a local one. Note
2138+
* that we'll overwrite expr->plan either way; the PG_TRY block will
2139+
* ensure we undo that on the way out, if the plan is local.
2140+
*/
2141+
if (plan == NULL || local_plan)
21222142
{
2143+
/* Don't let SPI save the plan if it's going to be local */
2144+
exec_prepare_plan(estate, expr, 0, !local_plan);
2145+
plan = expr->plan;
21232146

21242147
/*
2125-
* Don't save the plan if not in atomic context. Otherwise,
2126-
* transaction ends would cause errors about plancache leaks.
2127-
*
2128-
* XXX This would be fixable with some plancache/resowner surgery
2129-
* elsewhere, but for now we'll just work around this here.
2148+
* A CALL or DO can never be a simple expression. (If it could
2149+
* be, we'd have to worry about saving/restoring the previous
2150+
* values of the related expr fields, not just expr->plan.)
21302151
*/
2131-
exec_prepare_plan(estate, expr, 0, estate->atomic);
2152+
Assert(!expr->expr_simple_expr);
21322153

21332154
/*
21342155
* The procedure call could end transactions, which would upset
21352156
* the snapshot management in SPI_execute*, so don't let it do it.
21362157
* Instead, we set the snapshots ourselves below.
21372158
*/
2138-
plan = expr->plan;
21392159
plan->no_snapshots = true;
21402160

21412161
/*
21422162
* Force target to be recalculated whenever the plan changes, in
21432163
* case the procedure's argument list has changed.
21442164
*/
21452165
stmt->target = NULL;
2166+
cur_target = NULL;
21462167
}
21472168

21482169
/*
21492170
* We construct a DTYPE_ROW datum representing the plpgsql variables
21502171
* associated with the procedure's output arguments. Then we can use
21512172
* exec_move_row() to do the assignments.
2173+
*
2174+
* If we're using a local plan, also make a local target; otherwise,
2175+
* since the above code will force a new plan each time through, we'd
2176+
* repeatedly leak the memory for the target. (Note: we also leak the
2177+
* target when a plan change is forced, but that isn't so likely to
2178+
* cause excessive memory leaks.)
21522179
*/
2153-
if (stmt->is_call && stmt->target == NULL)
2180+
if (stmt->is_call && cur_target == NULL)
21542181
{
21552182
Node *node;
21562183
FuncExpr *funcexpr;
@@ -2165,6 +2192,9 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
21652192
int i;
21662193
ListCell *lc;
21672194

2195+
/* Use eval_mcontext for any cruft accumulated here */
2196+
oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
2197+
21682198
/*
21692199
* Get the parsed CallStmt, and look up the called procedure
21702200
*/
@@ -2196,17 +2226,20 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
21962226
ReleaseSysCache(func_tuple);
21972227

21982228
/*
2199-
* Begin constructing row Datum
2229+
* Begin constructing row Datum; keep it in fn_cxt if it's to be
2230+
* long-lived.
22002231
*/
2201-
oldcontext = MemoryContextSwitchTo(estate->func->fn_cxt);
2232+
if (!local_plan)
2233+
MemoryContextSwitchTo(estate->func->fn_cxt);
22022234

22032235
row = (PLpgSQL_row *) palloc0(sizeof(PLpgSQL_row));
22042236
row->dtype = PLPGSQL_DTYPE_ROW;
22052237
row->refname = "(unnamed row)";
22062238
row->lineno = -1;
22072239
row->varnos = (int *) palloc(sizeof(int) * list_length(funcargs));
22082240

2209-
MemoryContextSwitchTo(oldcontext);
2241+
if (!local_plan)
2242+
MemoryContextSwitchTo(get_eval_mcontext(estate));
22102243

22112244
/*
22122245
* Examine procedure's argument list. Each output arg position
@@ -2250,7 +2283,13 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
22502283

22512284
row->nfields = nfields;
22522285

2253-
stmt->target = (PLpgSQL_variable *) row;
2286+
cur_target = (PLpgSQL_variable *) row;
2287+
2288+
/* We can save and re-use the target datum, if it's not local */
2289+
if (!local_plan)
2290+
stmt->target = cur_target;
2291+
2292+
MemoryContextSwitchTo(oldcontext);
22542293
}
22552294

22562295
paramLI = setup_param_list(estate, expr);
@@ -2273,17 +2312,27 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
22732312
PG_CATCH();
22742313
{
22752314
/*
2276-
* If we aren't saving the plan, unset the pointer. Note that it
2277-
* could have been unset already, in case of a recursive call.
2315+
* If we are using a local plan, restore the old plan link.
22782316
*/
2279-
if (expr->plan && !expr->plan->saved)
2280-
expr->plan = NULL;
2317+
if (local_plan)
2318+
expr->plan = orig_plan;
22812319
PG_RE_THROW();
22822320
}
22832321
PG_END_TRY();
22842322

2285-
if (expr->plan && !expr->plan->saved)
2286-
expr->plan = NULL;
2323+
/*
2324+
* If we are using a local plan, restore the old plan link; then free the
2325+
* local plan to avoid memory leaks. (Note that the error exit path above
2326+
* just clears the link without risking calling SPI_freeplan; we expect
2327+
* that xact cleanup will take care of the mess in that case.)
2328+
*/
2329+
if (local_plan)
2330+
{
2331+
SPIPlanPtr plan = expr->plan;
2332+
2333+
expr->plan = orig_plan;
2334+
SPI_freeplan(plan);
2335+
}
22872336

22882337
if (rc < 0)
22892338
elog(ERROR, "SPI_execute_plan_with_paramlist failed executing query \"%s\": %s",
@@ -2319,10 +2368,10 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
23192368
{
23202369
SPITupleTable *tuptab = SPI_tuptable;
23212370

2322-
if (!stmt->target)
2371+
if (!cur_target)
23232372
elog(ERROR, "DO statement returned a row");
23242373

2325-
exec_move_row(estate, stmt->target, tuptab->vals[0], tuptab->tupdesc);
2374+
exec_move_row(estate, cur_target, tuptab->vals[0], tuptab->tupdesc);
23262375
}
23272376
else if (SPI_processed > 1)
23282377
elog(ERROR, "procedure call returned more than one row");

0 commit comments

Comments
 (0)