From d67c6d671cfb26198bebca52820d68986f7b27fb Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Fri, 13 Mar 2026 12:18:50 -0400 Subject: [PATCH 05/12] Global temporary tables: disable parallel query and autovacuum GTTs use per-session local buffers that parallel workers cannot access, for the same reason as regular temporary tables. Disable parallel query for GTT scans in set_rel_consider_parallel() and disable parallel CREATE INDEX in plan_create_index_workers(). Also skip GTTs in autovacuum's table scanning loops, since autovacuum runs in a separate backend and cannot access any session's local GTT storage. The planner-side exclusion only covers GTTs used as parallel baserels; a function mislabeled PARALLEL SAFE can still reach a GTT from inside a worker. GttInitSessionStorage therefore errors out cleanly in parallel workers (mirroring the temp-table check in InitLocalBuffers), and in the parallel-mode leader when first-time storage initialization would be required, instead of tripping RelationCreateStorage's assertion or scribbling on the leader's temp namespace from a worker. --- src/backend/catalog/storage_gtt.c | 39 +++++++++++++++++++++++++++ src/backend/optimizer/path/allpaths.c | 24 ++++++++++------- src/backend/optimizer/plan/planner.c | 7 ++--- src/backend/optimizer/util/clauses.c | 20 ++++++++++++++ src/backend/postmaster/autovacuum.c | 13 +++++++-- 5 files changed, 89 insertions(+), 14 deletions(-) diff --git a/src/backend/catalog/storage_gtt.c b/src/backend/catalog/storage_gtt.c index 3aa1d20f156..bd4540b59b1 100644 --- a/src/backend/catalog/storage_gtt.c +++ b/src/backend/catalog/storage_gtt.c @@ -22,6 +22,7 @@ #include "postgres.h" #include "access/amapi.h" +#include "access/parallel.h" #include "access/relation.h" #include "access/table.h" #include "access/tableam.h" @@ -184,6 +185,33 @@ GttInitSessionStorage(Relation relation) bool found; Oid relid = RelationGetRelid(relation); + /* + * A parallel worker must never create or register per-session storage: + * the storage map is backend-local (a worker cannot see the leader's + * entries or dirty local buffers), so any worker-side materialization + * would corrupt or duplicate the leader's session state. But a worker + * may legitimately need only the relation's catalog metadata -- e.g. + * pg_get_expr() opens the relation to deparse a column default while + * pg_dump runs under debug_parallel_query. Point the relcache entry at + * the session-local file in the leader's temp namespace, exactly as an + * untouched GTT looks in the leader, but without recording anything in + * the storage map. No file is created here; if the leader never + * materialized the relation, reads short-circuit to empty. The planner + * never makes a GTT a parallel baserel and DML is never parallelized, so + * a worker never actually scans or writes one. + */ + if (IsParallelWorker()) + { + if (OidIsValid(relation->rd_rel->reltablespace)) + relation->rd_locator.spcOid = relation->rd_rel->reltablespace; + else + relation->rd_locator.spcOid = MyDatabaseTableSpace; + relation->rd_locator.dbOid = MyDatabaseId; + relation->rd_locator.relNumber = relation->rd_rel->relfilenode; + relation->rd_backend = ProcNumberForTempRelations(); + return; + } + ensure_gtt_hash(); entry = (GttStorageEntry *) hash_search(gtt_storage_hash, @@ -339,6 +367,17 @@ GttEnsureSessionStorage(Relation relation) if (entry->storage_created) return; + /* + * RelationCreateStorage cannot run in parallel mode (it couldn't update + * pendingSyncHash), and a worker must never materialize state in the + * leader's temp namespace; relcache builds in workers are already + * rejected in GttInitSessionStorage. + */ + if (IsInParallelMode()) + ereport(ERROR, + errcode(ERRCODE_INVALID_TRANSACTION_STATE), + errmsg("cannot initialize global temporary table storage during a parallel operation")); + RelationCreateStorage(entry->locator, RELPERSISTENCE_GLOBAL_TEMP, true); entry->storage_created = true; entry->storage_subid = GetCurrentSubTransactionId(); diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index c134594a21a..c40b71c902d 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -664,16 +664,22 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel, /* * Currently, parallel workers can't access the leader's temporary - * tables. We could possibly relax this if we wrote all of its - * local buffers at the start of the query and made no changes - * thereafter (maybe we could allow hint bit changes), and if we - * taught the workers to read them. Writing a large number of - * temporary buffers could be expensive, though, and we don't have - * the rest of the necessary infrastructure right now anyway. So - * for now, bail out if we see a temporary table. + * tables, nor a global temporary table's per-session data. We + * could possibly relax this if we wrote all of its local buffers + * at the start of the query and made no changes thereafter (maybe + * we could allow hint bit changes), and if we taught the workers + * to read them. Writing a large number of temporary buffers + * could be expensive, though, and we don't have the rest of the + * necessary infrastructure right now anyway. So for now, bail + * out if we see a temporary or global temporary table. */ - if (get_rel_persistence(rte->relid) == RELPERSISTENCE_TEMP) - return; + { + char relpersist = get_rel_persistence(rte->relid); + + if (relpersist == RELPERSISTENCE_TEMP || + relpersist == RELPERSISTENCE_GLOBAL_TEMP) + return; + } /* * Table sampling can be pushed down to workers if the sample diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index f4689e7c9f8..964dc4a0fa0 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -7347,11 +7347,12 @@ plan_create_index_workers(Oid tableOid, Oid indexOid) /* * Determine if it's safe to proceed. * - * Currently, parallel workers can't access the leader's temporary tables. - * Furthermore, any index predicate or index expressions must be parallel - * safe. + * Currently, parallel workers can't access the leader's temporary tables, + * nor a global temporary table's per-session data. Furthermore, any + * index predicate or index expressions must be parallel safe. */ if (heap->rd_rel->relpersistence == RELPERSISTENCE_TEMP || + RelationIsGlobalTemp(heap) || !is_parallel_safe(root, (Node *) RelationGetIndexExpressions(index)) || !is_parallel_safe(root, (Node *) RelationGetIndexPredicate(index))) { diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 01997e22266..8d98d5f8b43 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -965,6 +965,7 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) else if (IsA(node, Query)) { Query *query = (Query *) node; + ListCell *lc; /* SELECT FOR UPDATE/SHARE must be treated as unsafe */ if (query->rowMarks != NULL) @@ -973,6 +974,25 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) return true; } + /* + * A global temporary table has per-session, backend-local storage + * that parallel workers cannot see, and that storage may have to be + * created on first access -- which is impossible in parallel mode. + * Referencing one therefore makes the query parallel-unsafe, so that + * parallel mode is never imposed on it (e.g. by debug_parallel_query). + */ + foreach(lc, query->rtable) + { + RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc); + + if (rte->rtekind == RTE_RELATION && + get_rel_persistence(rte->relid) == RELPERSISTENCE_GLOBAL_TEMP) + { + context->max_hazard = PROPARALLEL_UNSAFE; + return true; + } + } + /* Recurse into subselects */ return query_tree_walker(query, max_parallel_hazard_walker, diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index e9aaf24c1be..7452518c386 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -2073,6 +2073,13 @@ do_autovacuum(void) continue; } + /* + * Global temporary tables have per-session local storage that + * autovacuum cannot access. Skip them. + */ + if (classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP) + continue; + /* Fetch reloptions and the pgstat entry for this table */ relopts = extract_autovac_opts(tuple, pg_class_desc); @@ -2147,9 +2154,11 @@ do_autovacuum(void) AutoVacuumScores scores; /* - * We cannot safely process other backends' temp tables, so skip 'em. + * We cannot safely process other backends' temp tables or GTTs (which + * have per-session local storage), so skip them. */ - if (classForm->relpersistence == RELPERSISTENCE_TEMP) + if (classForm->relpersistence == RELPERSISTENCE_TEMP || + classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP) continue; relid = classForm->oid; -- 2.43.0