Skip to content

Commit 2785073

Browse files
committed
Add injection-point test for the prune-before-lock relock fallback
PortalRunMulti() reuses a cached generic plan's Bind-time partition pruning only while the command counter has not advanced; otherwise it discards the prepared state and re-locks conservatively, failing with serialization_failure if the plan was invalidated in between. That fallback is only reachable in a pipelined transaction, so add a "cached-plan-relock" injection point that both diverts an ordinary EXECUTE into the fallback and provides a wait in the re-lock window, and an isolation test covering the two outcomes: the fallback running to completion when the plan is still valid, and serialization_failure when a concurrent session invalidates it during the wait.
1 parent 96ef126 commit 2785073

5 files changed

Lines changed: 139 additions & 2 deletions

File tree

src/backend/tcop/pquery.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "pg_trace.h"
2626
#include "tcop/pquery.h"
2727
#include "tcop/utility.h"
28+
#include "utils/injection_point.h"
2829
#include "utils/memutils.h"
2930
#include "utils/snapmgr.h"
3031

@@ -1363,7 +1364,8 @@ PortalRunMulti(Portal portal,
13631364
Assert(pstmt->canSetTag);
13641365
Assert(!active_snapshot_set);
13651366

1366-
if (prep_qd->snapshot->curcid == GetCurrentCommandId(false))
1367+
if (prep_qd->snapshot->curcid == GetCurrentCommandId(false) &&
1368+
!IS_INJECTION_POINT_ATTACHED("cached-plan-relock"))
13671369
{
13681370
PushActiveSnapshot(prep_qd->snapshot);
13691371
ProcessQuery(pstmt,
@@ -1399,6 +1401,16 @@ PortalRunMulti(Portal portal,
13991401
*/
14001402
PortalDisposePrepQueryDesc(portal);
14011403

1404+
/*
1405+
* Test hook: a concurrent invalidation must be able to land
1406+
* here, between discarding the Bind-time prep and the
1407+
* conservative re-lock, to exercise the serialization_failure
1408+
* path below. Attaching this point also forces the fast path
1409+
* above to be skipped, so an ordinary (non-pipelined) EXECUTE
1410+
* reaches this branch. Compiles out without injection points.
1411+
*/
1412+
INJECTION_POINT("cached-plan-relock", NULL);
1413+
14021414
/*
14031415
* Note that this locks all partitions instead of redoing the
14041416
* initial pruning as ExecutorPrepAndLock() would have at

src/test/modules/injection_points/Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ ISOLATION = basic \
1919
repack_temporal_multirange \
2020
repack_toast \
2121
syscache-update-pruned \
22-
heap_lock_update
22+
heap_lock_update \
23+
cached_plan_relock
2324

2425
# some isolation tests require wal_level=replica
2526
ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
Parsed test spec with 2 sessions
2+
3+
starting permutation: arm_notice exec
4+
injection_points_set_local
5+
--------------------------
6+
7+
(1 row)
8+
9+
step arm_notice: SELECT injection_points_attach('cached-plan-relock', 'notice');
10+
injection_points_attach
11+
-----------------------
12+
13+
(1 row)
14+
15+
s1: NOTICE: notice triggered for injection point cached-plan-relock
16+
step exec: EXECUTE p (1);
17+
a
18+
-
19+
1
20+
(1 row)
21+
22+
injection_points_detach
23+
-----------------------
24+
25+
(1 row)
26+
27+
28+
starting permutation: arm_wait exec invalidate wakeup noop
29+
injection_points_set_local
30+
--------------------------
31+
32+
(1 row)
33+
34+
step arm_wait: SELECT injection_points_attach('cached-plan-relock', 'wait');
35+
injection_points_attach
36+
-----------------------
37+
38+
(1 row)
39+
40+
step exec: EXECUTE p (1); <waiting ...>
41+
step invalidate: CREATE INDEX cpr3_a_idx ON cpr3 (a);
42+
step wakeup: SELECT injection_points_wakeup('cached-plan-relock');
43+
injection_points_wakeup
44+
-----------------------
45+
46+
(1 row)
47+
48+
step exec: <... completed>
49+
ERROR: cached plan was invalidated during execution setup
50+
step noop:
51+
injection_points_detach
52+
-----------------------
53+
54+
(1 row)
55+

src/test/modules/injection_points/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ tests += {
5151
'repack_toast',
5252
'syscache-update-pruned',
5353
'heap_lock_update',
54+
'cached_plan_relock',
5455
],
5556
'runningcheck': false, # see syscache-update-pruned
5657
# Some tests wait for all snapshots, so avoid parallel execution
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Exercise the fallback in PortalRunMulti() taken when a reused generic
2+
# plan's Bind-time partition-pruning survivor set may be stale (the
3+
# command counter advanced between Bind and Execute). That fallback
4+
# discards the prepared state and re-locks conservatively; if the plan
5+
# was invalidated in the meantime the statement fails with
6+
# serialization_failure.
7+
#
8+
# The fallback is normally only reachable in a pipelined transaction
9+
# (named portals executed out of Bind order). The "cached-plan-relock"
10+
# injection point stands in for that: while it is attached, an ordinary
11+
# EXECUTE is diverted into the fallback, and the point also provides a
12+
# wait so a concurrent invalidation can be placed precisely in the
13+
# re-lock window.
14+
15+
setup
16+
{
17+
CREATE EXTENSION injection_points;
18+
19+
CREATE TABLE cpr (a int, b int) PARTITION BY LIST (a);
20+
CREATE TABLE cpr1 PARTITION OF cpr FOR VALUES IN (1);
21+
CREATE TABLE cpr2 PARTITION OF cpr FOR VALUES IN (2);
22+
CREATE TABLE cpr3 PARTITION OF cpr FOR VALUES IN (3);
23+
INSERT INTO cpr SELECT g, 0 FROM generate_series(1, 3) g;
24+
}
25+
26+
teardown
27+
{
28+
DROP TABLE cpr;
29+
DROP EXTENSION injection_points;
30+
}
31+
32+
session s1
33+
setup
34+
{
35+
SET plan_cache_mode = force_generic_plan;
36+
-- session setup runs at the start of every permutation on the same
37+
-- connection, so clear any prepared statement left by a prior one
38+
DEALLOCATE ALL;
39+
PREPARE p (int) AS UPDATE cpr SET b = b + 1 WHERE a = $1 RETURNING a;
40+
-- first EXECUTE builds the generic plan (not yet "reused", so it does
41+
-- not take the prune-before-lock path); the plan is reused afterwards
42+
EXECUTE p (1);
43+
SELECT injection_points_set_local();
44+
}
45+
# attach as 'notice': force the fallback but let it run through
46+
step arm_notice { SELECT injection_points_attach('cached-plan-relock', 'notice'); }
47+
# attach as 'wait': force the fallback and park in the re-lock window
48+
step arm_wait { SELECT injection_points_attach('cached-plan-relock', 'wait'); }
49+
step exec { EXECUTE p (1); }
50+
step noop { }
51+
teardown { SELECT injection_points_detach('cached-plan-relock'); }
52+
53+
session s2
54+
# invalidate the cached plan on a partition s1 does not hold locked at
55+
# Bind (it pruned to cpr1), so this does not block on s1
56+
step invalidate { CREATE INDEX cpr3_a_idx ON cpr3 (a); }
57+
step wakeup { SELECT injection_points_wakeup('cached-plan-relock'); }
58+
59+
# Fallback runs to completion when the plan is still valid: the reused
60+
# generic plan is discarded, re-locked conservatively, and executed,
61+
# returning the correct row (a = 1).
62+
permutation arm_notice exec
63+
64+
# Fallback meets a concurrent invalidation: s1 parks at the re-lock
65+
# point, s2 invalidates the plan and wakes s1, whose conservative
66+
# AcquireExecutorLocks() then reports the plan invalid, raising
67+
# serialization_failure. The noop keeps detach ordered after the wait.
68+
permutation arm_wait exec invalidate wakeup noop

0 commit comments

Comments
 (0)