Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit f0e4ec7

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 651bdbc commit f0e4ec7

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

21352135
/*
21362136
* exec_stmt_call
2137+
*
2138+
* NOTE: this is used for both CALL and DO statements.
21372139
*/
21382140
static int
21392141
exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
21402142
{
21412143
PLpgSQL_expr *expr = stmt->expr;
2144+
SPIPlanPtr orig_plan = expr->plan;
2145+
bool local_plan;
2146+
PLpgSQL_variable *volatile cur_target = stmt->target;
21422147
volatile LocalTransactionId before_lxid;
21432148
LocalTransactionId after_lxid;
21442149
volatile bool pushed_active_snap = false;
21452150
volatile int rc;
21462151

2152+
/*
2153+
* If not in atomic context, we make a local plan that we'll just use for
2154+
* this invocation, and will free at the end. Otherwise, transaction ends
2155+
* would cause errors about plancache leaks.
2156+
*
2157+
* XXX This would be fixable with some plancache/resowner surgery
2158+
* elsewhere, but for now we'll just work around this here.
2159+
*/
2160+
local_plan = !estate->atomic;
2161+
21472162
/* PG_TRY to ensure we clear the plan link, if needed, on failure */
21482163
PG_TRY();
21492164
{
21502165
SPIPlanPtr plan = expr->plan;
21512166
ParamListInfo paramLI;
21522167

2153-
if (plan == NULL)
2168+
/*
2169+
* Make a plan if we don't have one, or if we need a local one. Note
2170+
* that we'll overwrite expr->plan either way; the PG_TRY block will
2171+
* ensure we undo that on the way out, if the plan is local.
2172+
*/
2173+
if (plan == NULL || local_plan)
21542174
{
2175+
/* Don't let SPI save the plan if it's going to be local */
2176+
exec_prepare_plan(estate, expr, 0, !local_plan);
2177+
plan = expr->plan;
21552178

21562179
/*
2157-
* Don't save the plan if not in atomic context. Otherwise,
2158-
* transaction ends would cause errors about plancache leaks.
2159-
*
2160-
* XXX This would be fixable with some plancache/resowner surgery
2161-
* elsewhere, but for now we'll just work around this here.
2180+
* A CALL or DO can never be a simple expression. (If it could
2181+
* be, we'd have to worry about saving/restoring the previous
2182+
* values of the related expr fields, not just expr->plan.)
21622183
*/
2163-
exec_prepare_plan(estate, expr, 0, estate->atomic);
2184+
Assert(!expr->expr_simple_expr);
21642185

21652186
/*
21662187
* The procedure call could end transactions, which would upset
21672188
* the snapshot management in SPI_execute*, so don't let it do it.
21682189
* Instead, we set the snapshots ourselves below.
21692190
*/
2170-
plan = expr->plan;
21712191
plan->no_snapshots = true;
21722192

21732193
/*
21742194
* Force target to be recalculated whenever the plan changes, in
21752195
* case the procedure's argument list has changed.
21762196
*/
21772197
stmt->target = NULL;
2198+
cur_target = NULL;
21782199
}
21792200

21802201
/*
21812202
* We construct a DTYPE_ROW datum representing the plpgsql variables
21822203
* associated with the procedure's output arguments. Then we can use
21832204
* exec_move_row() to do the assignments.
2205+
*
2206+
* If we're using a local plan, also make a local target; otherwise,
2207+
* since the above code will force a new plan each time through, we'd
2208+
* repeatedly leak the memory for the target. (Note: we also leak the
2209+
* target when a plan change is forced, but that isn't so likely to
2210+
* cause excessive memory leaks.)
21842211
*/
2185-
if (stmt->is_call && stmt->target == NULL)
2212+
if (stmt->is_call && cur_target == NULL)
21862213
{
21872214
Node *node;
21882215
FuncExpr *funcexpr;
@@ -2197,6 +2224,9 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
21972224
int i;
21982225
ListCell *lc;
21992226

2227+
/* Use eval_mcontext for any cruft accumulated here */
2228+
oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
2229+
22002230
/*
22012231
* Get the parsed CallStmt, and look up the called procedure
22022232
*/
@@ -2228,17 +2258,20 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
22282258
ReleaseSysCache(func_tuple);
22292259

22302260
/*
2231-
* Begin constructing row Datum
2261+
* Begin constructing row Datum; keep it in fn_cxt if it's to be
2262+
* long-lived.
22322263
*/
2233-
oldcontext = MemoryContextSwitchTo(estate->func->fn_cxt);
2264+
if (!local_plan)
2265+
MemoryContextSwitchTo(estate->func->fn_cxt);
22342266

22352267
row = (PLpgSQL_row *) palloc0(sizeof(PLpgSQL_row));
22362268
row->dtype = PLPGSQL_DTYPE_ROW;
22372269
row->refname = "(unnamed row)";
22382270
row->lineno = -1;
22392271
row->varnos = (int *) palloc(sizeof(int) * list_length(funcargs));
22402272

2241-
MemoryContextSwitchTo(oldcontext);
2273+
if (!local_plan)
2274+
MemoryContextSwitchTo(get_eval_mcontext(estate));
22422275

22432276
/*
22442277
* Examine procedure's argument list. Each output arg position
@@ -2282,7 +2315,13 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
22822315

22832316
row->nfields = nfields;
22842317

2285-
stmt->target = (PLpgSQL_variable *) row;
2318+
cur_target = (PLpgSQL_variable *) row;
2319+
2320+
/* We can save and re-use the target datum, if it's not local */
2321+
if (!local_plan)
2322+
stmt->target = cur_target;
2323+
2324+
MemoryContextSwitchTo(oldcontext);
22862325
}
22872326

22882327
paramLI = setup_param_list(estate, expr);
@@ -2305,17 +2344,27 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
23052344
PG_CATCH();
23062345
{
23072346
/*
2308-
* If we aren't saving the plan, unset the pointer. Note that it
2309-
* could have been unset already, in case of a recursive call.
2347+
* If we are using a local plan, restore the old plan link.
23102348
*/
2311-
if (expr->plan && !expr->plan->saved)
2312-
expr->plan = NULL;
2349+
if (local_plan)
2350+
expr->plan = orig_plan;
23132351
PG_RE_THROW();
23142352
}
23152353
PG_END_TRY();
23162354

2317-
if (expr->plan && !expr->plan->saved)
2318-
expr->plan = NULL;
2355+
/*
2356+
* If we are using a local plan, restore the old plan link; then free the
2357+
* local plan to avoid memory leaks. (Note that the error exit path above
2358+
* just clears the link without risking calling SPI_freeplan; we expect
2359+
* that xact cleanup will take care of the mess in that case.)
2360+
*/
2361+
if (local_plan)
2362+
{
2363+
SPIPlanPtr plan = expr->plan;
2364+
2365+
expr->plan = orig_plan;
2366+
SPI_freeplan(plan);
2367+
}
23192368

23202369
if (rc < 0)
23212370
elog(ERROR, "SPI_execute_plan_with_paramlist failed executing query \"%s\": %s",
@@ -2352,10 +2401,10 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
23522401
{
23532402
SPITupleTable *tuptab = SPI_tuptable;
23542403

2355-
if (!stmt->target)
2404+
if (!cur_target)
23562405
elog(ERROR, "DO statement returned a row");
23572406

2358-
exec_move_row(estate, stmt->target, tuptab->vals[0], tuptab->tupdesc);
2407+
exec_move_row(estate, cur_target, tuptab->vals[0], tuptab->tupdesc);
23592408
}
23602409
else if (SPI_processed > 1)
23612410
elog(ERROR, "procedure call returned more than one row");

0 commit comments

Comments
 (0)