Skip to content

Commit b808dbf

Browse files
committed
Avoid unnecessary plancache revalidation of utility statements.
Revalidation of a plancache entry (after a cache invalidation event) requires acquiring a snapshot. Normally that is harmless, but not if the cached statement is one that needs to run without acquiring a snapshot. We were already aware of that for TransactionStmts, but for some reason hadn't extrapolated to the other statements that PlannedStmtRequiresSnapshot() knows mustn't set a snapshot. This can lead to unexpected failures of commands such as SET TRANSACTION ISOLATION LEVEL. We can fix it in the same way, by excluding those command types from revalidation. However, we can do even better than that: there is no need to revalidate for any statement type for which parse analysis, rewrite, and plan steps do nothing interesting, which is nearly all utility commands. To mechanize this, invent a parser function stmt_requires_parse_analysis() that tells whether parse analysis does anything beyond wrapping a CMD_UTILITY Query around the raw parse tree. If that's what it does, then rewrite and plan will just skip the Query, so that it is not possible for the same raw parse tree to produce a different plan tree after cache invalidation. stmt_requires_parse_analysis() is basically equivalent to the existing function analyze_requires_snapshot(), except that for obscure reasons that function omits ReturnStmt and CallStmt. It is unclear whether those were oversights or intentional. I have not been able to demonstrate a bug from not acquiring a snapshot while analyzing these commands, but at best it seems mighty fragile. It seems safer to acquire a snapshot for parse analysis of these commands too, which allows making stmt_requires_parse_analysis and analyze_requires_snapshot equivalent. In passing this fixes a second bug, which is that ResetPlanCache would exclude ReturnStmts and CallStmts from revalidation. That's surely *not* safe, since they contain parsable expressions. Per bug #18059 from Pavel Kulakov. Back-patch to all supported branches. Discussion: https://postgr.es/m/18059-79c692f036b25346@postgresql.org
1 parent 2607380 commit b808dbf

File tree

5 files changed

+107
-49
lines changed

5 files changed

+107
-49
lines changed

src/backend/parser/analyze.c

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
274274
}
275275
#endif /* RAW_EXPRESSION_COVERAGE_TEST */
276276

277+
/*
278+
* Caution: when changing the set of statement types that have non-default
279+
* processing here, see also stmt_requires_parse_analysis() and
280+
* analyze_requires_snapshot().
281+
*/
277282
switch (nodeTag(parseTree))
278283
{
279284
/*
@@ -347,14 +352,22 @@ transformStmt(ParseState *pstate, Node *parseTree)
347352
}
348353

349354
/*
350-
* analyze_requires_snapshot
351-
* Returns true if a snapshot must be set before doing parse analysis
352-
* on the given raw parse tree.
355+
* stmt_requires_parse_analysis
356+
* Returns true if parse analysis will do anything non-trivial
357+
* with the given raw parse tree.
358+
*
359+
* Generally, this should return true for any statement type for which
360+
* transformStmt() does more than wrap a CMD_UTILITY Query around it.
361+
* When it returns false, the caller can assume that there is no situation
362+
* in which parse analysis of the raw statement could need to be re-done.
353363
*
354-
* Classification here should match transformStmt().
364+
* Currently, since the rewriter and planner do nothing for CMD_UTILITY
365+
* Queries, a false result means that the entire parse analysis/rewrite/plan
366+
* pipeline will never need to be re-done. If that ever changes, callers
367+
* will likely need adjustment.
355368
*/
356369
bool
357-
analyze_requires_snapshot(RawStmt *parseTree)
370+
stmt_requires_parse_analysis(RawStmt *parseTree)
358371
{
359372
bool result;
360373

@@ -376,19 +389,43 @@ analyze_requires_snapshot(RawStmt *parseTree)
376389
case T_DeclareCursorStmt:
377390
case T_ExplainStmt:
378391
case T_CreateTableAsStmt:
379-
/* yes, because we must analyze the contained statement */
392+
case T_CallStmt:
380393
result = true;
381394
break;
382395

383396
default:
384-
/* other utility statements don't have any real parse analysis */
397+
/* all other statements just get wrapped in a CMD_UTILITY Query */
385398
result = false;
386399
break;
387400
}
388401

389402
return result;
390403
}
391404

405+
/*
406+
* analyze_requires_snapshot
407+
* Returns true if a snapshot must be set before doing parse analysis
408+
* on the given raw parse tree.
409+
*/
410+
bool
411+
analyze_requires_snapshot(RawStmt *parseTree)
412+
{
413+
/*
414+
* Currently, this should return true in exactly the same cases that
415+
* stmt_requires_parse_analysis() does, so we just invoke that function
416+
* rather than duplicating it. We keep the two entry points separate for
417+
* clarity of callers, since from the callers' standpoint these are
418+
* different conditions.
419+
*
420+
* While there may someday be a statement type for which transformStmt()
421+
* does something nontrivial and yet no snapshot is needed for that
422+
* processing, it seems likely that making such a choice would be fragile.
423+
* If you want to install an exception, document the reasoning for it in a
424+
* comment.
425+
*/
426+
return stmt_requires_parse_analysis(parseTree);
427+
}
428+
392429
/*
393430
* transformDeleteStmt -
394431
* transforms a Delete Statement

src/backend/utils/cache/plancache.c

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,15 @@
7777

7878
/*
7979
* We must skip "overhead" operations that involve database access when the
80-
* cached plan's subject statement is a transaction control command.
81-
* For the convenience of postgres.c, treat empty statements as control
82-
* commands too.
80+
* cached plan's subject statement is a transaction control command or one
81+
* that requires a snapshot not to be set yet (such as SET or LOCK). More
82+
* generally, statements that do not require parse analysis/rewrite/plan
83+
* activity never need to be revalidated, so we can treat them all like that.
84+
* For the convenience of postgres.c, treat empty statements that way too.
8385
*/
84-
#define IsTransactionStmtPlan(plansource) \
85-
((plansource)->raw_parse_tree == NULL || \
86-
IsA((plansource)->raw_parse_tree->stmt, TransactionStmt))
86+
#define StmtPlanRequiresRevalidation(plansource) \
87+
((plansource)->raw_parse_tree != NULL && \
88+
stmt_requires_parse_analysis((plansource)->raw_parse_tree))
8789

8890
/*
8991
* This is the head of the backend's list of "saved" CachedPlanSources (i.e.,
@@ -381,13 +383,13 @@ CompleteCachedPlan(CachedPlanSource *plansource,
381383
plansource->query_context = querytree_context;
382384
plansource->query_list = querytree_list;
383385

384-
if (!plansource->is_oneshot && !IsTransactionStmtPlan(plansource))
386+
if (!plansource->is_oneshot && StmtPlanRequiresRevalidation(plansource))
385387
{
386388
/*
387389
* Use the planner machinery to extract dependencies. Data is saved
388390
* in query_context. (We assume that not a lot of extra cruft is
389391
* created by this call.) We can skip this for one-shot plans, and
390-
* transaction control commands have no such dependencies anyway.
392+
* plans not needing revalidation have no such dependencies anyway.
391393
*/
392394
extract_query_dependencies((Node *) querytree_list,
393395
&plansource->relationOids,
@@ -566,11 +568,11 @@ RevalidateCachedQuery(CachedPlanSource *plansource,
566568
/*
567569
* For one-shot plans, we do not support revalidation checking; it's
568570
* assumed the query is parsed, planned, and executed in one transaction,
569-
* so that no lock re-acquisition is necessary. Also, there is never any
570-
* need to revalidate plans for transaction control commands (and we
571-
* mustn't risk any catalog accesses when handling those).
571+
* so that no lock re-acquisition is necessary. Also, if the statement
572+
* type can't require revalidation, we needn't do anything (and we mustn't
573+
* risk catalog accesses when handling, eg, transaction control commands).
572574
*/
573-
if (plansource->is_oneshot || IsTransactionStmtPlan(plansource))
575+
if (plansource->is_oneshot || !StmtPlanRequiresRevalidation(plansource))
574576
{
575577
Assert(plansource->is_valid);
576578
return NIL;
@@ -1026,8 +1028,8 @@ choose_custom_plan(CachedPlanSource *plansource, ParamListInfo boundParams)
10261028
/* Otherwise, never any point in a custom plan if there's no parameters */
10271029
if (boundParams == NULL)
10281030
return false;
1029-
/* ... nor for transaction control statements */
1030-
if (IsTransactionStmtPlan(plansource))
1031+
/* ... nor when planning would be a no-op */
1032+
if (!StmtPlanRequiresRevalidation(plansource))
10311033
return false;
10321034

10331035
/* Let settings force the decision */
@@ -1778,8 +1780,8 @@ PlanCacheRelCallback(Datum arg, Oid relid)
17781780
if (!plansource->is_valid)
17791781
continue;
17801782

1781-
/* Never invalidate transaction control commands */
1782-
if (IsTransactionStmtPlan(plansource))
1783+
/* Never invalidate if parse/plan would be a no-op anyway */
1784+
if (!StmtPlanRequiresRevalidation(plansource))
17831785
continue;
17841786

17851787
/*
@@ -1863,8 +1865,8 @@ PlanCacheObjectCallback(Datum arg, int cacheid, uint32 hashvalue)
18631865
if (!plansource->is_valid)
18641866
continue;
18651867

1866-
/* Never invalidate transaction control commands */
1867-
if (IsTransactionStmtPlan(plansource))
1868+
/* Never invalidate if parse/plan would be a no-op anyway */
1869+
if (!StmtPlanRequiresRevalidation(plansource))
18681870
continue;
18691871

18701872
/*
@@ -1973,7 +1975,6 @@ ResetPlanCache(void)
19731975
{
19741976
CachedPlanSource *plansource = dlist_container(CachedPlanSource,
19751977
node, iter.cur);
1976-
ListCell *lc;
19771978

19781979
Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
19791980

@@ -1985,32 +1986,16 @@ ResetPlanCache(void)
19851986
* We *must not* mark transaction control statements as invalid,
19861987
* particularly not ROLLBACK, because they may need to be executed in
19871988
* aborted transactions when we can't revalidate them (cf bug #5269).
1989+
* In general there's no point in invalidating statements for which a
1990+
* new parse analysis/rewrite/plan cycle would certainly give the same
1991+
* results.
19881992
*/
1989-
if (IsTransactionStmtPlan(plansource))
1993+
if (!StmtPlanRequiresRevalidation(plansource))
19901994
continue;
19911995

1992-
/*
1993-
* In general there is no point in invalidating utility statements
1994-
* since they have no plans anyway. So invalidate it only if it
1995-
* contains at least one non-utility statement, or contains a utility
1996-
* statement that contains a pre-analyzed query (which could have
1997-
* dependencies.)
1998-
*/
1999-
foreach(lc, plansource->query_list)
2000-
{
2001-
Query *query = lfirst_node(Query, lc);
2002-
2003-
if (query->commandType != CMD_UTILITY ||
2004-
UtilityContainsQuery(query->utilityStmt))
2005-
{
2006-
/* non-utility statement, so invalidate */
2007-
plansource->is_valid = false;
2008-
if (plansource->gplan)
2009-
plansource->gplan->is_valid = false;
2010-
/* no need to look further */
2011-
break;
2012-
}
2013-
}
1996+
plansource->is_valid = false;
1997+
if (plansource->gplan)
1998+
plansource->gplan->is_valid = false;
20141999
}
20152000

20162001
/* Likewise invalidate cached expressions */

src/include/parser/analyze.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ extern Query *parse_sub_analyze(Node *parseTree, ParseState *parentParseState,
3535
extern Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree);
3636
extern Query *transformStmt(ParseState *pstate, Node *parseTree);
3737

38+
extern bool stmt_requires_parse_analysis(RawStmt *parseTree);
3839
extern bool analyze_requires_snapshot(RawStmt *parseTree);
3940

4041
extern const char *LCS_asString(LockClauseStrength strength);

src/pl/plpgsql/src/expected/plpgsql_call.out

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ SELECT * FROM test1;
3535
55
3636
(1 row)
3737

38+
-- Check that plan revalidation doesn't prevent setting transaction properties
39+
-- (bug #18059). This test must include the first temp-object creation in
40+
-- this script, or it won't exercise the bug scenario. Hence we put it early.
41+
CREATE PROCEDURE test_proc3a()
42+
LANGUAGE plpgsql
43+
AS $$
44+
BEGIN
45+
COMMIT;
46+
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
47+
RAISE NOTICE 'done';
48+
END;
49+
$$;
50+
CALL test_proc3a();
51+
NOTICE: done
52+
CREATE TEMP TABLE tt1(f1 int);
53+
CALL test_proc3a();
54+
NOTICE: done
3855
-- nested CALL
3956
TRUNCATE TABLE test1;
4057
CREATE PROCEDURE test_proc4(y int)

src/pl/plpgsql/src/sql/plpgsql_call.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@ CALL test_proc3(55);
3838
SELECT * FROM test1;
3939

4040

41+
-- Check that plan revalidation doesn't prevent setting transaction properties
42+
-- (bug #18059). This test must include the first temp-object creation in
43+
-- this script, or it won't exercise the bug scenario. Hence we put it early.
44+
CREATE PROCEDURE test_proc3a()
45+
LANGUAGE plpgsql
46+
AS $$
47+
BEGIN
48+
COMMIT;
49+
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
50+
RAISE NOTICE 'done';
51+
END;
52+
$$;
53+
54+
CALL test_proc3a();
55+
CREATE TEMP TABLE tt1(f1 int);
56+
CALL test_proc3a();
57+
58+
4159
-- nested CALL
4260
TRUNCATE TABLE test1;
4361

0 commit comments

Comments
 (0)