delay_execution.o
ISOLATION = partition-addition \
- partition-removal-1 \
- cached-plan-inval
+ partition-removal-1
ifdef USE_PGXS
PG_CONFIG = pg_config
/*-------------------------------------------------------------------------
*
* delay_execution.c
- * Test module to introduce delay at various points during execution of a
- * query to test that execution proceeds safely in light of concurrent
- * changes.
+ * Test module to allow delay between parsing and execution of a query.
*
* The delay is implemented by taking and immediately releasing a specified
* advisory lock. If another process has previously taken that lock, the
* current process will be blocked until the lock is released; otherwise,
* there's no effect. This allows an isolationtester script to reliably
- * test behaviors where some specified action happens in another backend in
- * a couple of cases: 1) between parsing and execution of any desired query
- * when using the planner_hook, 2) between RevalidateCachedQuery() and
- * ExecutorStart() when using the ExecutorStart_hook.
+ * test behaviors where some specified action happens in another backend
+ * between parsing and execution of any desired query.
*
* Copyright (c) 2020-2025, PostgreSQL Global Development Group
*
#include <limits.h>
-#include "executor/executor.h"
#include "optimizer/planner.h"
#include "utils/fmgrprotos.h"
#include "utils/guc.h"
/* GUC: advisory lock ID to use. Zero disables the feature. */
static int post_planning_lock_id = 0;
-static int executor_start_lock_id = 0;
-/* Save previous hook users to be a good citizen */
+/* Save previous planner hook user to be a good citizen */
static planner_hook_type prev_planner_hook = NULL;
-static ExecutorStart_hook_type prev_ExecutorStart_hook = NULL;
/* planner_hook function to provide the desired delay */
return result;
}
-/* ExecutorStart_hook function to provide the desired delay */
-static bool
-delay_execution_ExecutorStart(QueryDesc *queryDesc, int eflags)
-{
- bool plan_valid;
-
- /* If enabled, delay by taking and releasing the specified lock */
- if (executor_start_lock_id != 0)
- {
- DirectFunctionCall1(pg_advisory_lock_int8,
- Int64GetDatum((int64) executor_start_lock_id));
- DirectFunctionCall1(pg_advisory_unlock_int8,
- Int64GetDatum((int64) executor_start_lock_id));
-
- /*
- * Ensure that we notice any pending invalidations, since the advisory
- * lock functions don't do this.
- */
- AcceptInvalidationMessages();
- }
-
- /* Now start the executor, possibly via a previous hook user */
- if (prev_ExecutorStart_hook)
- plan_valid = prev_ExecutorStart_hook(queryDesc, eflags);
- else
- plan_valid = standard_ExecutorStart(queryDesc, eflags);
-
- if (executor_start_lock_id != 0)
- elog(NOTICE, "Finished ExecutorStart(): CachedPlan is %s",
- plan_valid ? "valid" : "not valid");
-
- return plan_valid;
-}
-
/* Module load function */
void
_PG_init(void)
{
- /* Set up GUCs to control which lock is used */
+ /* Set up the GUC to control which lock is used */
DefineCustomIntVariable("delay_execution.post_planning_lock_id",
"Sets the advisory lock ID to be locked/unlocked after planning.",
"Zero disables the delay.",
NULL,
NULL);
- DefineCustomIntVariable("delay_execution.executor_start_lock_id",
- "Sets the advisory lock ID to be locked/unlocked before starting execution.",
- "Zero disables the delay.",
- &executor_start_lock_id,
- 0,
- 0, INT_MAX,
- PGC_USERSET,
- 0,
- NULL,
- NULL,
- NULL);
MarkGUCPrefixReserved("delay_execution");
- /* Install our hooks. */
+ /* Install our hook */
prev_planner_hook = planner_hook;
planner_hook = delay_execution_planner;
- prev_ExecutorStart_hook = ExecutorStart_hook;
- ExecutorStart_hook = delay_execution_ExecutorStart;
}
+++ /dev/null
-Parsed test spec with 2 sessions
-
-starting permutation: s1prep s2lock s1exec s2dropi s2unlock
-step s1prep: SET plan_cache_mode = force_generic_plan;
- PREPARE q AS SELECT * FROM foov WHERE a = $1 FOR UPDATE;
- EXPLAIN (COSTS OFF) EXECUTE q (1);
-QUERY PLAN
------------------------------------------------------
-LockRows
- -> Append
- Subplans Removed: 2
- -> Index Scan using foo1_1_a on foo1_1 foo_1
- Index Cond: (a = $1)
-(5 rows)
-
-step s2lock: SELECT pg_advisory_lock(12345);
-pg_advisory_lock
-----------------
-
-(1 row)
-
-step s1exec: LOAD 'delay_execution';
- SET delay_execution.executor_start_lock_id = 12345;
- EXPLAIN (COSTS OFF) EXECUTE q (1); <waiting ...>
-step s2dropi: DROP INDEX foo1_1_a;
-step s2unlock: SELECT pg_advisory_unlock(12345);
-pg_advisory_unlock
-------------------
-t
-(1 row)
-
-step s1exec: <... completed>
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is not valid
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is valid
-QUERY PLAN
-------------------------------------
-LockRows
- -> Append
- Subplans Removed: 2
- -> Seq Scan on foo1_1 foo_1
- Filter: (a = $1)
-(5 rows)
-
-
-starting permutation: s1prep2 s2lock s1exec2 s2dropi s2unlock
-step s1prep2: SET plan_cache_mode = force_generic_plan;
- PREPARE q2 AS SELECT * FROM foov WHERE a = one() or a = two();
- EXPLAIN (COSTS OFF) EXECUTE q2;
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is valid
-QUERY PLAN
----------------------------------------------------
-Append
- Subplans Removed: 1
- -> Index Scan using foo1_1_a on foo1_1 foo_1
- Index Cond: (a = ANY (ARRAY[one(), two()]))
- -> Seq Scan on foo1_2 foo_2
- Filter: ((a = one()) OR (a = two()))
-(6 rows)
-
-step s2lock: SELECT pg_advisory_lock(12345);
-pg_advisory_lock
-----------------
-
-(1 row)
-
-step s1exec2: LOAD 'delay_execution';
- SET delay_execution.executor_start_lock_id = 12345;
- EXPLAIN (COSTS OFF) EXECUTE q2; <waiting ...>
-step s2dropi: DROP INDEX foo1_1_a;
-step s2unlock: SELECT pg_advisory_unlock(12345);
-pg_advisory_unlock
-------------------
-t
-(1 row)
-
-step s1exec2: <... completed>
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is not valid
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is valid
-QUERY PLAN
---------------------------------------------
-Append
- Subplans Removed: 1
- -> Seq Scan on foo1_1 foo_1
- Filter: ((a = one()) OR (a = two()))
- -> Seq Scan on foo1_2 foo_2
- Filter: ((a = one()) OR (a = two()))
-(6 rows)
-
-
-starting permutation: s1prep3 s2lock s1exec3 s2dropi s2unlock
-step s1prep3: SET plan_cache_mode = force_generic_plan;
- PREPARE q3 AS UPDATE foov SET a = a WHERE a = one() or a = two();
- EXPLAIN (COSTS OFF) EXECUTE q3;
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is valid
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is valid
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is valid
-QUERY PLAN
----------------------------------------------------------------
-Nested Loop
- -> Append
- Subplans Removed: 1
- -> Index Only Scan using foo1_1_a on foo1_1 foo_1
- Index Cond: (a = ANY (ARRAY[one(), two()]))
- -> Seq Scan on foo1_2 foo_2
- Filter: ((a = one()) OR (a = two()))
- -> Materialize
- -> Append
- Subplans Removed: 1
- -> Seq Scan on bar1 bar_1
- Filter: (a = one())
-
-Update on bar
- Update on bar1 bar_1
- -> Nested Loop
- -> Append
- Subplans Removed: 1
- -> Index Scan using foo1_1_a on foo1_1 foo_1
- Index Cond: (a = ANY (ARRAY[one(), two()]))
- -> Seq Scan on foo1_2 foo_2
- Filter: ((a = one()) OR (a = two()))
- -> Materialize
- -> Append
- Subplans Removed: 1
- -> Seq Scan on bar1 bar_1
- Filter: (a = one())
-
-Update on foo
- Update on foo1_1 foo_1
- Update on foo1_2 foo_2
- -> Append
- Subplans Removed: 1
- -> Index Scan using foo1_1_a on foo1_1 foo_1
- Index Cond: (a = ANY (ARRAY[one(), two()]))
- -> Seq Scan on foo1_2 foo_2
- Filter: ((a = one()) OR (a = two()))
-(37 rows)
-
-step s2lock: SELECT pg_advisory_lock(12345);
-pg_advisory_lock
-----------------
-
-(1 row)
-
-step s1exec3: LOAD 'delay_execution';
- SET delay_execution.executor_start_lock_id = 12345;
- EXPLAIN (COSTS OFF) EXECUTE q3; <waiting ...>
-step s2dropi: DROP INDEX foo1_1_a;
-step s2unlock: SELECT pg_advisory_unlock(12345);
-pg_advisory_unlock
-------------------
-t
-(1 row)
-
-step s1exec3: <... completed>
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is not valid
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is valid
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is valid
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is valid
-QUERY PLAN
---------------------------------------------------------
-Nested Loop
- -> Append
- Subplans Removed: 1
- -> Seq Scan on foo1_1 foo_1
- Filter: ((a = one()) OR (a = two()))
- -> Seq Scan on foo1_2 foo_2
- Filter: ((a = one()) OR (a = two()))
- -> Materialize
- -> Append
- Subplans Removed: 1
- -> Seq Scan on bar1 bar_1
- Filter: (a = one())
-
-Update on bar
- Update on bar1 bar_1
- -> Nested Loop
- -> Append
- Subplans Removed: 1
- -> Seq Scan on foo1_1 foo_1
- Filter: ((a = one()) OR (a = two()))
- -> Seq Scan on foo1_2 foo_2
- Filter: ((a = one()) OR (a = two()))
- -> Materialize
- -> Append
- Subplans Removed: 1
- -> Seq Scan on bar1 bar_1
- Filter: (a = one())
-
-Update on foo
- Update on foo1_1 foo_1
- Update on foo1_2 foo_2
- -> Append
- Subplans Removed: 1
- -> Seq Scan on foo1_1 foo_1
- Filter: ((a = one()) OR (a = two()))
- -> Seq Scan on foo1_2 foo_2
- Filter: ((a = one()) OR (a = two()))
-(37 rows)
-
-
-starting permutation: s1prep4 s2lock s1exec4 s2dropi s2unlock
-step s1prep4: SET plan_cache_mode = force_generic_plan;
- PREPARE q4 AS SELECT * FROM generate_series(1, 1) WHERE EXISTS (SELECT * FROM foov WHERE a = $1 FOR UPDATE);
- EXPLAIN (COSTS OFF) EXECUTE q4 (1);
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is valid
-QUERY PLAN
--------------------------------------------------------------
-Result
- One-Time Filter: (InitPlan 1).col1
- InitPlan 1
- -> LockRows
- -> Append
- Subplans Removed: 2
- -> Index Scan using foo1_1_a on foo1_1 foo_1
- Index Cond: (a = $1)
- -> Function Scan on generate_series
-(9 rows)
-
-step s2lock: SELECT pg_advisory_lock(12345);
-pg_advisory_lock
-----------------
-
-(1 row)
-
-step s1exec4: LOAD 'delay_execution';
- SET delay_execution.executor_start_lock_id = 12345;
- EXPLAIN (COSTS OFF) EXECUTE q4 (1); <waiting ...>
-step s2dropi: DROP INDEX foo1_1_a;
-step s2unlock: SELECT pg_advisory_unlock(12345);
-pg_advisory_unlock
-------------------
-t
-(1 row)
-
-step s1exec4: <... completed>
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is not valid
-s1: NOTICE: Finished ExecutorStart(): CachedPlan is valid
-QUERY PLAN
---------------------------------------------
-Result
- One-Time Filter: (InitPlan 1).col1
- InitPlan 1
- -> LockRows
- -> Append
- Subplans Removed: 2
- -> Seq Scan on foo1_1 foo_1
- Filter: (a = $1)
- -> Function Scan on generate_series
-(9 rows)
-
'specs': [
'partition-addition',
'partition-removal-1',
- 'cached-plan-inval',
],
},
}
+++ /dev/null
-# Test to check that invalidation of cached generic plans during ExecutorStart
-# is correctly detected causing an updated plan to be re-executed.
-
-setup
-{
- CREATE TABLE foo (a int, b text) PARTITION BY RANGE (a);
- CREATE TABLE foo1 PARTITION OF foo FOR VALUES FROM (MINVALUE) TO (3) PARTITION BY RANGE (a);
- CREATE TABLE foo1_1 PARTITION OF foo1 FOR VALUES FROM (MINVALUE) TO (2);
- CREATE TABLE foo1_2 PARTITION OF foo1 FOR VALUES FROM (2) TO (3);
- CREATE INDEX foo1_1_a ON foo1_1 (a);
- CREATE TABLE foo2 PARTITION OF foo FOR VALUES FROM (3) TO (MAXVALUE);
- INSERT INTO foo SELECT generate_series(-1000, 1000);
- CREATE VIEW foov AS SELECT * FROM foo;
- CREATE FUNCTION one () RETURNS int AS $$ BEGIN RETURN 1; END; $$ LANGUAGE PLPGSQL STABLE;
- CREATE FUNCTION two () RETURNS int AS $$ BEGIN RETURN 2; END; $$ LANGUAGE PLPGSQL STABLE;
- CREATE TABLE bar (a int, b text) PARTITION BY LIST(a);
- CREATE TABLE bar1 PARTITION OF bar FOR VALUES IN (1);
- CREATE INDEX ON bar1(a);
- CREATE TABLE bar2 PARTITION OF bar FOR VALUES IN (2);
- CREATE RULE update_foo AS ON UPDATE TO foo DO ALSO UPDATE bar SET a = a WHERE a = one();
- CREATE RULE update_bar AS ON UPDATE TO bar DO ALSO SELECT 1;
- ANALYZE;
-}
-
-teardown
-{
- DROP VIEW foov;
- DROP RULE update_foo ON foo;
- DROP TABLE foo, bar;
- DROP FUNCTION one(), two();
-}
-
-session "s1"
-step "s1prep" { SET plan_cache_mode = force_generic_plan;
- PREPARE q AS SELECT * FROM foov WHERE a = $1 FOR UPDATE;
- EXPLAIN (COSTS OFF) EXECUTE q (1); }
-
-step "s1prep2" { SET plan_cache_mode = force_generic_plan;
- PREPARE q2 AS SELECT * FROM foov WHERE a = one() or a = two();
- EXPLAIN (COSTS OFF) EXECUTE q2; }
-
-step "s1prep3" { SET plan_cache_mode = force_generic_plan;
- PREPARE q3 AS UPDATE foov SET a = a WHERE a = one() or a = two();
- EXPLAIN (COSTS OFF) EXECUTE q3; }
-
-step "s1prep4" { SET plan_cache_mode = force_generic_plan;
- PREPARE q4 AS SELECT * FROM generate_series(1, 1) WHERE EXISTS (SELECT * FROM foov WHERE a = $1 FOR UPDATE);
- EXPLAIN (COSTS OFF) EXECUTE q4 (1); }
-
-step "s1exec" { LOAD 'delay_execution';
- SET delay_execution.executor_start_lock_id = 12345;
- EXPLAIN (COSTS OFF) EXECUTE q (1); }
-step "s1exec2" { LOAD 'delay_execution';
- SET delay_execution.executor_start_lock_id = 12345;
- EXPLAIN (COSTS OFF) EXECUTE q2; }
-step "s1exec3" { LOAD 'delay_execution';
- SET delay_execution.executor_start_lock_id = 12345;
- EXPLAIN (COSTS OFF) EXECUTE q3; }
-step "s1exec4" { LOAD 'delay_execution';
- SET delay_execution.executor_start_lock_id = 12345;
- EXPLAIN (COSTS OFF) EXECUTE q4 (1); }
-
-session "s2"
-step "s2lock" { SELECT pg_advisory_lock(12345); }
-step "s2unlock" { SELECT pg_advisory_unlock(12345); }
-step "s2dropi" { DROP INDEX foo1_1_a; }
-
-# In all permutations below, while "s1exec", "s1exec2", etc. wait to
-# acquire the advisory lock, "s2drop" drops the index being used in the
-# cached plan. When "s1exec" and others are unblocked and begin initializing
-# the plan, including acquiring necessary locks on partitions, the concurrent
-# index drop is detected. This causes plan initialization to be aborted,
-# prompting the caller to retry with a new plan.
-
-# Case with runtime pruning using EXTERN parameter
-permutation "s1prep" "s2lock" "s1exec" "s2dropi" "s2unlock"
-
-# Case with runtime pruning using stable function
-permutation "s1prep2" "s2lock" "s1exec2" "s2dropi" "s2unlock"
-
-# Case with a rule adding another query causing the CachedPlan to contain
-# multiple PlannedStmts
-permutation "s1prep3" "s2lock" "s1exec3" "s2dropi" "s2unlock"
-
-# Case with run-time pruning inside a subquery
-permutation "s1prep4" "s2lock" "s1exec4" "s2dropi" "s2unlock"