While the approach in the previous email does pass the tests, I think (but couldn't find a test case to prove) it does so coincidentally, not because it is correct. If I make the test for "detached exist" use the cached omits-partitions-partdesc, it does fail, because we had previously cached one that was not yet omitting the partition. So what I said earlier in the thread stands: the set of partitions that are considered detached changes depending on what the active snapshot is, and therefore we *must not* cache any such descriptor.
So I backtracked to my previous proposal, which saves in relcache only the partdesc that includes all partitions. If any partdesc is built that omits partitions being detached, that one must be rebuilt afresh each time. And to avoid potentially saving a lot of single-use partdescs in CacheMemoryContext, in the attached second patch (which I attach separately only to make it more obvious to review) I store such partdescs in PortalContext. Barring objections, I will get this pushed early tomorrow. -- Álvaro Herrera Valdivia, Chile "Just treat us the way you want to be treated + some extra allowance for ignorance." (Michael Brusser)
>From a6e316fd12e5afe4dbb736e27ac83f688c7138ab Mon Sep 17 00:00:00 2001 From: Alvaro Herrera <alvhe...@alvh.no-ip.org> Date: Tue, 20 Apr 2021 18:01:18 -0400 Subject: [PATCH 1/2] Fix relcache hazard with detached partitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During queries coming from ri_triggers.c, we need to omit partitions that are marked pending detach -- otherwise, the RI query is tricked into allowing a row into the referencing table whose corresponding row is in the detached partition. Which is bogus: once the detach operation completes, the row becomes an orphan. However, the code was not doing that in repeatable-read transactions, because relcache kept a copy of the partition descriptor that included the partition, and used it in the RI query. This commit changes the partdesc cache code to only keep descriptors that aren't dependent on a snapshot (namely: those where no detached partition exist, and those where detached partitions are included). When a partdesc-without- detached-partitions is requested, we create one afresh each time. find_inheritance_children gets a new output *detached_exist boolean, which indicates whether any partition marked pending-detach is found. Its "include_detached" input flag is changed to "omit_detached", because that captures desired the semantics more naturally. This was noticed because a buildfarm member that runs with relcache clobbering, which would not keep the improperly cached partdesc, broke one test, which led us to realize that the expected output of that test was bogus. This commit also corrects that expected output. Author: Amit Langote <amitlangot...@gmail.com> Author: Álvaro Herrera <alvhe...@alvh.no-ip.org> Discussion: https://postgr.es/m/3269784.1617215...@sss.pgh.pa.us --- src/backend/catalog/pg_inherits.c | 64 +++++++++++-------- src/backend/commands/tablecmds.c | 22 +++---- src/backend/commands/trigger.c | 4 +- src/backend/executor/execPartition.c | 15 ++--- src/backend/partitioning/partdesc.c | 30 +++++---- src/include/catalog/pg_inherits.h | 4 +- src/include/partitioning/partdesc.h | 10 ++- .../detach-partition-concurrently-4.out | 1 + 8 files changed, 88 insertions(+), 62 deletions(-) diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c index bb8b2249b1..bb6b1686ee 100644 --- a/src/backend/catalog/pg_inherits.c +++ b/src/backend/catalog/pg_inherits.c @@ -52,13 +52,19 @@ typedef struct SeenRelsEntry * then no locks are acquired, but caller must beware of race conditions * against possible DROPs of child relations. * - * include_detached says to include all partitions, even if they're marked - * detached. Passing it as false means they might or might not be included, - * depending on the visibility of the pg_inherits row for the active snapshot. + * If a partition's pg_inherits row is marked "detach pending", + * *detached_exist (if not null) is set true, otherwise it is set false. + * + * If omit_detached is true and there is an active snapshot (not the same as + * the catalog snapshot used to scan pg_inherits!) and a pg_inherits tuple + * marked "detach pending" is visible to that snapshot, then that partition is + * omitted from the output list. This makes partitions invisible depending on + * whether the transaction that marked those partitions as detached appears + * committed to the active snapshot. */ List * -find_inheritance_children(Oid parentrelId, bool include_detached, - LOCKMODE lockmode) +find_inheritance_children(Oid parentrelId, bool omit_detached, + LOCKMODE lockmode, bool *detached_exist) { List *list = NIL; Relation relation; @@ -78,6 +84,9 @@ find_inheritance_children(Oid parentrelId, bool include_detached, if (!has_subclass(parentrelId)) return NIL; + if (detached_exist) + *detached_exist = false; + /* * Scan pg_inherits and build a working array of subclass OIDs. */ @@ -99,29 +108,34 @@ find_inheritance_children(Oid parentrelId, bool include_detached, { /* * Cope with partitions concurrently being detached. When we see a - * partition marked "detach pending", we only include it in the set of - * visible partitions if caller requested all detached partitions, or - * if its pg_inherits tuple's xmin is still visible to the active - * snapshot. + * partition marked "detach pending", we omit it from the returned + * descriptor if caller requested that and the tuple's xmin does not + * appear in progress to the active snapshot. (If there's no active + * snapshot set, that means we're not running a user query, so it's OK + * to always include detached partitions in that case; if the xmin is + * still running to the active snapshot, then the partition has not + * been detached yet and so we include it.) * - * The reason for this check is that we want to avoid seeing the + * The reason for this hack is that we want to avoid seeing the * partition as alive in RI queries during REPEATABLE READ or - * SERIALIZABLE transactions. (If there's no active snapshot set, - * that means we're not running a user query, so it's OK to always - * include detached partitions in that case.) + * SERIALIZABLE transactions. */ - if (((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhdetachpending && - !include_detached && - ActiveSnapshotSet()) + if (((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhdetachpending) { - TransactionId xmin; - Snapshot snap; + if (detached_exist) + *detached_exist = true; - xmin = HeapTupleHeaderGetXmin(inheritsTuple->t_data); - snap = GetActiveSnapshot(); + if (omit_detached && ActiveSnapshotSet()) + { + TransactionId xmin; + Snapshot snap; - if (!XidInMVCCSnapshot(xmin, snap)) - continue; + xmin = HeapTupleHeaderGetXmin(inheritsTuple->t_data); + snap = GetActiveSnapshot(); + + if (!XidInMVCCSnapshot(xmin, snap)) + continue; + } } inhrelid = ((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhrelid; @@ -235,8 +249,8 @@ find_all_inheritors(Oid parentrelId, LOCKMODE lockmode, List **numparents) ListCell *lc; /* Get the direct children of this rel */ - currentchildren = find_inheritance_children(currentrel, false, - lockmode); + currentchildren = find_inheritance_children(currentrel, true, + lockmode, NULL); /* * Add to the queue only those children not already seen. This avoids @@ -524,7 +538,7 @@ DeleteInheritsTuple(Oid inhrelid, Oid inhparent, bool expect_detach_pending, parent = ((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhparent; if (!OidIsValid(inhparent) || parent == inhparent) { - bool detach_pending; + bool detach_pending; detach_pending = ((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhdetachpending; diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 096a6f2891..31da913d2b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -3507,7 +3507,7 @@ renameatt_internal(Oid myrelid, * expected_parents will only be 0 if we are not already recursing. */ if (expected_parents == 0 && - find_inheritance_children(myrelid, false, NoLock) != NIL) + find_inheritance_children(myrelid, true, NoLock, NULL) != NIL) ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("inherited column \"%s\" must be renamed in child tables too", @@ -3706,7 +3706,7 @@ rename_constraint_internal(Oid myrelid, else { if (expected_parents == 0 && - find_inheritance_children(myrelid, false, NoLock) != NIL) + find_inheritance_children(myrelid, true, NoLock, NULL) != NIL) ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("inherited constraint \"%s\" must be renamed in child tables too", @@ -6580,7 +6580,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, */ if (colDef->identity && recurse && - find_inheritance_children(myrelid, false, NoLock) != NIL) + find_inheritance_children(myrelid, true, NoLock, NULL) != NIL) ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("cannot recursively add identity column to table that has child tables"))); @@ -6826,7 +6826,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, * use find_all_inheritors to do it in one pass. */ children = - find_inheritance_children(RelationGetRelid(rel), false, lockmode); + find_inheritance_children(RelationGetRelid(rel), true, lockmode, NULL); /* * If we are told not to recurse, there had better not be any child @@ -7689,7 +7689,7 @@ ATPrepDropExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recurs * resulting state can be properly dumped and restored. */ if (!recurse && - find_inheritance_children(RelationGetRelid(rel), false, lockmode)) + find_inheritance_children(RelationGetRelid(rel), true, lockmode, NULL)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("ALTER TABLE / DROP EXPRESSION must be applied to child tables too"))); @@ -8297,7 +8297,7 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName, * use find_all_inheritors to do it in one pass. */ children = - find_inheritance_children(RelationGetRelid(rel), false, lockmode); + find_inheritance_children(RelationGetRelid(rel), true, lockmode, NULL); if (children) { @@ -8785,7 +8785,7 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, * use find_all_inheritors to do it in one pass. */ children = - find_inheritance_children(RelationGetRelid(rel), false, lockmode); + find_inheritance_children(RelationGetRelid(rel), true, lockmode, NULL); /* * Check if ONLY was specified with ALTER TABLE. If so, allow the @@ -11318,8 +11318,8 @@ ATExecDropConstraint(Relation rel, const char *constrName, * use find_all_inheritors to do it in one pass. */ if (!is_no_inherit_constraint) - children = - find_inheritance_children(RelationGetRelid(rel), false, lockmode); + children = find_inheritance_children(RelationGetRelid(rel), true, + lockmode, NULL); else children = NIL; @@ -11703,8 +11703,8 @@ ATPrepAlterColumnType(List **wqueue, } } else if (!recursing && - find_inheritance_children(RelationGetRelid(rel), false, - NoLock) != NIL) + find_inheritance_children(RelationGetRelid(rel), true, + NoLock, NULL) != NIL) ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("type of inherited column \"%s\" must be changed in child tables too", diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 3421014e47..2dce62999d 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -1141,8 +1141,8 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, ListCell *l; List *idxs = NIL; - idxs = find_inheritance_children(indexOid, false, - ShareRowExclusiveLock); + idxs = find_inheritance_children(indexOid, true, + ShareRowExclusiveLock, NULL); foreach(l, idxs) childTbls = lappend_oid(childTbls, IndexGetRelation(lfirst_oid(l), diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c index 99780ebb96..bbfc9ef3fa 100644 --- a/src/backend/executor/execPartition.c +++ b/src/backend/executor/execPartition.c @@ -991,14 +991,11 @@ ExecInitPartitionDispatchInfo(EState *estate, /* * For data modification, it is better that executor does not include - * partitions being detached, except in snapshot-isolation mode. This - * means that a read-committed transaction immediately gets a "no - * partition for tuple" error when a tuple is inserted into a partition - * that's being detached concurrently, but a transaction in repeatable- - * read mode can still use the partition. Note that because partition - * detach uses ShareLock on the partition (which conflicts with DML), - * we're certain that the detach won't be able to complete until any - * inserting transaction is done. + * partitions being detached, except when running in snapshot-isolation + * mode. This means that a read-committed transaction immediately gets a + * "no partition for tuple" error when a tuple is inserted into a + * partition that's being detached concurrently, but a transaction in + * repeatable-read mode can still use such a partition. */ if (estate->es_partition_directory == NULL) estate->es_partition_directory = @@ -1571,7 +1568,7 @@ ExecCreatePartitionPruneState(PlanState *planstate, ListCell *lc; int i; - /* Executor must always include detached partitions */ + /* For data reading, executor always includes detached partitions */ if (estate->es_partition_directory == NULL) estate->es_partition_directory = CreatePartitionDirectory(estate->es_query_cxt, true); diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c index 58570fecfd..7d019a72be 100644 --- a/src/backend/partitioning/partdesc.c +++ b/src/backend/partitioning/partdesc.c @@ -47,7 +47,8 @@ typedef struct PartitionDirectoryEntry PartitionDesc pd; } PartitionDirectoryEntry; -static void RelationBuildPartitionDesc(Relation rel, bool include_detached); +static PartitionDesc RelationBuildPartitionDesc(Relation rel, + bool include_detached); /* @@ -64,14 +65,13 @@ static void RelationBuildPartitionDesc(Relation rel, bool include_detached); PartitionDesc RelationGetPartitionDesc(Relation rel, bool include_detached) { - if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) - return NULL; + Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); - if (unlikely(rel->rd_partdesc == NULL || - rel->rd_partdesc->includes_detached != include_detached)) - RelationBuildPartitionDesc(rel, include_detached); + if (likely(rel->rd_partdesc && + (!rel->rd_partdesc->detached_exist || include_detached))) + return rel->rd_partdesc; - return rel->rd_partdesc; + return RelationBuildPartitionDesc(rel, include_detached); } /* @@ -89,7 +89,7 @@ RelationGetPartitionDesc(Relation rel, bool include_detached) * that some of our callees allocate memory on their own which would be leaked * permanently. */ -static void +static PartitionDesc RelationBuildPartitionDesc(Relation rel, bool include_detached) { PartitionDesc partdesc; @@ -98,6 +98,7 @@ RelationBuildPartitionDesc(Relation rel, bool include_detached) PartitionBoundSpec **boundspecs = NULL; Oid *oids = NULL; bool *is_leaf = NULL; + bool detached_exist; ListCell *cell; int i, nparts; @@ -112,8 +113,8 @@ RelationBuildPartitionDesc(Relation rel, bool include_detached) * concurrently, whatever this function returns will be accurate as of * some well-defined point in time. */ - inhoids = find_inheritance_children(RelationGetRelid(rel), include_detached, - NoLock); + inhoids = find_inheritance_children(RelationGetRelid(rel), !include_detached, + NoLock, &detached_exist); nparts = list_length(inhoids); /* Allocate working arrays for OIDs, leaf flags, and boundspecs. */ @@ -234,6 +235,7 @@ RelationBuildPartitionDesc(Relation rel, bool include_detached) partdesc = (PartitionDescData *) MemoryContextAllocZero(new_pdcxt, sizeof(PartitionDescData)); partdesc->nparts = nparts; + partdesc->detached_exist = detached_exist; /* If there are no partitions, the rest of the partdesc can stay zero */ if (nparts > 0) { @@ -241,7 +243,6 @@ RelationBuildPartitionDesc(Relation rel, bool include_detached) partdesc->boundinfo = partition_bounds_copy(boundinfo, key); partdesc->oids = (Oid *) palloc(nparts * sizeof(Oid)); partdesc->is_leaf = (bool *) palloc(nparts * sizeof(bool)); - partdesc->includes_detached = include_detached; /* * Assign OIDs from the original array into mapped indexes of the @@ -276,7 +277,12 @@ RelationBuildPartitionDesc(Relation rel, bool include_detached) if (rel->rd_pdcxt != NULL) MemoryContextSetParent(rel->rd_pdcxt, new_pdcxt); rel->rd_pdcxt = new_pdcxt; - rel->rd_partdesc = partdesc; + + /* Store it into relcache, but only if no detached partitions exist */ + if (!detached_exist) + rel->rd_partdesc = partdesc; + + return partdesc; } /* diff --git a/src/include/catalog/pg_inherits.h b/src/include/catalog/pg_inherits.h index 6d07e1b302..4d28ede5a6 100644 --- a/src/include/catalog/pg_inherits.h +++ b/src/include/catalog/pg_inherits.h @@ -50,8 +50,8 @@ DECLARE_INDEX(pg_inherits_parent_index, 2187, on pg_inherits using btree(inhpare #define InheritsParentIndexId 2187 -extern List *find_inheritance_children(Oid parentrelId, bool include_detached, - LOCKMODE lockmode); +extern List *find_inheritance_children(Oid parentrelId, bool omit_detached, + LOCKMODE lockmode, bool *detached_exist); extern List *find_all_inheritors(Oid parentrelId, LOCKMODE lockmode, List **parents); extern bool has_subclass(Oid relationId); diff --git a/src/include/partitioning/partdesc.h b/src/include/partitioning/partdesc.h index 7f03ff4271..41485c5b10 100644 --- a/src/include/partitioning/partdesc.h +++ b/src/include/partitioning/partdesc.h @@ -17,11 +17,19 @@ /* * Information about partitions of a partitioned table. + * + * For partitioned tables where detached partitions exist, we only cache the + * descriptor that includes all partitions, including detached; when we're + * requested a descriptor without the detached partitions, we create one + * afresh each time. (The reason for this is that the set of detached + * partitions that are visible to each caller depends on the snapshot it has, + * so it's pretty much impossible to evict a descriptor from cache at the + * right time.) */ typedef struct PartitionDescData { int nparts; /* Number of partitions */ - bool includes_detached; /* Does it include detached partitions */ + bool detached_exist; /* Are there any detached partitions? */ Oid *oids; /* Array of 'nparts' elements containing * partition OIDs in order of the their bounds */ bool *is_leaf; /* Array of 'nparts' elements storing whether diff --git a/src/test/isolation/expected/detach-partition-concurrently-4.out b/src/test/isolation/expected/detach-partition-concurrently-4.out index 90a75cb077..2167675374 100644 --- a/src/test/isolation/expected/detach-partition-concurrently-4.out +++ b/src/test/isolation/expected/detach-partition-concurrently-4.out @@ -324,6 +324,7 @@ a 1 2 step s1insert: insert into d4_fk values (1); +ERROR: insert or update on table "d4_fk" violates foreign key constraint "d4_fk_a_fkey" step s1c: commit; starting permutation: s2snitch s1b s1s s2detach s1cancel s3vacfreeze s1s s1insert s1c -- 2.20.1
>From 1b1106cace29025ef96b364e5bcd5bc7930c2eb7 Mon Sep 17 00:00:00 2001 From: Alvaro Herrera <alvhe...@alvh.no-ip.org> Date: Wed, 21 Apr 2021 15:42:03 -0400 Subject: [PATCH 2/2] Make partitions-omitted partdescs have stmt lifetime --- src/backend/partitioning/partdesc.c | 50 ++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c index 7d019a72be..399390b42c 100644 --- a/src/backend/partitioning/partdesc.c +++ b/src/backend/partitioning/partdesc.c @@ -88,6 +88,12 @@ RelationGetPartitionDesc(Relation rel, bool include_detached) * context the current context except in very brief code sections, out of fear * that some of our callees allocate memory on their own which would be leaked * permanently. + * + * As a special case, partition descriptors that are requested to omit + * partitions being detached (and which contain such partitions) are transient + * and are not to be associated with the relcache entry. Such descriptors only + * last through the requesting Portal, so we use the corresponding memory + * context for them. */ static PartitionDesc RelationBuildPartitionDesc(Relation rel, bool include_detached) @@ -262,25 +268,39 @@ RelationBuildPartitionDesc(Relation rel, bool include_detached) } /* - * We have a fully valid partdesc ready to store into the relcache. - * Reparent it so it has the right lifespan. + * We have a fully valid partdesc. Reparent it so that it has the right + * lifespan, and if appropriate put it into the relation's relcache entry. */ - MemoryContextSetParent(new_pdcxt, CacheMemoryContext); + if (!include_detached && detached_exist) + { + /* + * A transient partition descriptor is only good for the current + * statement, so make it a child of the current portal's context. + */ + MemoryContextSetParent(new_pdcxt, PortalContext); + } + else + { + /* + * This partdesc goes into relcache. + */ - /* - * But first, a kluge: if there's an old rd_pdcxt, it contains an old - * partition descriptor that may still be referenced somewhere. Preserve - * it, while not leaking it, by reattaching it as a child context of the - * new rd_pdcxt. Eventually it will get dropped by either RelationClose - * or RelationClearRelation. - */ - if (rel->rd_pdcxt != NULL) - MemoryContextSetParent(rel->rd_pdcxt, new_pdcxt); - rel->rd_pdcxt = new_pdcxt; + MemoryContextSetParent(new_pdcxt, CacheMemoryContext); - /* Store it into relcache, but only if no detached partitions exist */ - if (!detached_exist) + /* + * But first, a kluge: if there's an old rd_pdcxt, it contains an old + * partition descriptor that may still be referenced somewhere. + * Preserve it, while not leaking it, by reattaching it as a child + * context of the new rd_pdcxt. Eventually it will get dropped by + * either RelationClose or RelationClearRelation. + */ + if (rel->rd_pdcxt != NULL) + MemoryContextSetParent(rel->rd_pdcxt, new_pdcxt); + rel->rd_pdcxt = new_pdcxt; + + /* Store it into relcache */ rel->rd_partdesc = partdesc; + } return partdesc; } -- 2.20.1