On Sat, Sep 12, 2026 at 8:04 AM Nikolay Samokhvalov <[email protected]> wrote:
> I'd keep the dead list for now. No runtime issue found in v2.

Hi Amit,

I kept iterating with our new PostgresAI harness and found one more
issue while testing v2.

`ri_HashCompareOp()` can process a cast invalidation inside
`fmgr_info_cxt()`, before publishing `entry->info`. The callback sees
NULL and has nothing to detach, so the outer call publishes the old
cast information afterward.

In the affected backend, a direct cast maps 2 to 102, but RI still
looks for parent 2. A fresh backend uses the new cast. This can occur
when function initialization processes a pending cast invalidation
during cache construction.

Reproduced on master and PG19. The same comparison-cache construction
sequence is present in the PG14–18 v2 patches.

Function initialization can load a C library and run its `_PG_init()`.
Nested RI from there can populate the same cache entry. The outer call
then overwrites it, leaking the comparison context; fast-path metadata
has the same problem.

In case helpful, attached is an incremental fix for master/PG19, on
top of v2. It retries after invalidation and keeps an entry already
populated by a nested call. The fast-path check uses a per-constraint
generation, since a nested reload can set `valid` back to true before
the outer call resumes. PG14–18 need only the comparison-cache
changes.

The fix passes checks for invalidation during construction, nested
initialization, and both together. On master, regression, isolation,
and injection-point suites pass with the earlier memory-test
correction applied.

Thanks,
Nik
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index e2ca06b9..d60198bf 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -123,6 +123,7 @@ typedef struct RI_ConstraintInfo
 {
 	Oid			constraint_id;	/* OID of pg_constraint entry (hash key) */
 	bool		valid;			/* successfully initialized? */
+	uint64		cache_generation; /* increments when entry is invalidated */
 	Oid			constraint_root_id; /* OID of topmost ancestor constraint;
 									 * same as constraint_id if not inherited */
 	uint32		oidHashValue;	/* hash value of constraint_id */
@@ -338,6 +339,9 @@ static FastPathMeta *ri_fpmeta_dead_list = NULL;
 /* Comparison call information detached by InvalidateCastCacheCallBack(). */
 static RI_CompareInfo *ri_compare_dead_list = NULL;
 
+/* Incremented whenever the comparison cache is invalidated. */
+static uint64 ri_compare_cache_generation = 0;
+
 /*
  * Local function prototypes
  */
@@ -2511,7 +2515,10 @@ ri_LoadConstraintInfo(Oid constraintOid)
 											   &constraintOid,
 											   HASH_ENTER, &found);
 	if (!found)
+	{
 		riinfo->valid = false;
+		riinfo->cache_generation = 0;
+	}
 	else if (riinfo->valid)
 		return riinfo;
 
@@ -2665,6 +2672,7 @@ InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid,
 			riinfo->oidHashValue == hashvalue ||
 			riinfo->rootHashValue == hashvalue)
 		{
+			riinfo->cache_generation++;
 			riinfo->valid = false;
 
 			/*
@@ -2704,6 +2712,7 @@ InvalidateCastCacheCallBack(Datum arg, SysCacheIdentifier cacheid,
 	HASH_SEQ_STATUS status;
 	RI_CompareHashEntry *entry;
 
+	ri_compare_cache_generation++;
 	hash_seq_init(&status, ri_compare_cache);
 	while ((entry = hash_seq_search(&status)) != NULL)
 	{
@@ -3785,56 +3794,79 @@ ri_populate_fastpath_metadata(RI_ConstraintInfo *riinfo,
 {
 	FastPathMeta *fpmeta;
 	MemoryContext context;
+	uint64		generation;
 
-	Assert(riinfo != NULL && riinfo->valid);
-	Assert(riinfo->fpmeta == NULL);
-
-	/* Keep incomplete metadata subject to normal error cleanup. */
-	context = AllocSetContextCreate(CurTransactionContext,
-									"RI fast-path finfo scratch",
-									ALLOCSET_SMALL_SIZES);
-	fpmeta = MemoryContextAllocZero(context, sizeof(FastPathMeta));
-	fpmeta->scratch_cxt = context;
-	for (int i = 0; i < riinfo->nkeys; i++)
+	for (;;)
 	{
-		Oid			eq_opr = riinfo->pf_eq_oprs[i];
-		Oid			typeid = RIAttType(fk_rel, riinfo->fk_attnums[i]);
-		Oid			lefttype;
-		RI_CompareInfo *entry = ri_HashCompareOp(eq_opr, typeid);
-		int			idx_col;
+		Assert(riinfo != NULL && riinfo->valid);
+		if (riinfo->fpmeta != NULL)
+			return;
+		generation = riinfo->cache_generation;
 
-		/*
-		 * Find the index column position for this constraint key.  The FK
-		 * constraint may reference columns in a different order than they
-		 * appear in the PK index, so we must map pk_attnums[i] to the
-		 * corresponding index column position.
-		 */
-		for (idx_col = 0; idx_col < riinfo->nkeys; idx_col++)
+		/* Keep incomplete metadata subject to normal error cleanup. */
+		context = AllocSetContextCreate(CurTransactionContext,
+										"RI fast-path finfo scratch",
+										ALLOCSET_SMALL_SIZES);
+		fpmeta = MemoryContextAllocZero(context, sizeof(FastPathMeta));
+		fpmeta->scratch_cxt = context;
+		for (int i = 0; i < riinfo->nkeys; i++)
 		{
-			if (idx_rel->rd_index->indkey.values[idx_col] == riinfo->pk_attnums[i])
-				break;
+			Oid			eq_opr = riinfo->pf_eq_oprs[i];
+			Oid			typeid = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+			Oid			lefttype;
+			RI_CompareInfo *entry = ri_HashCompareOp(eq_opr, typeid);
+			int			idx_col;
+
+			/*
+			 * Find the index column position for this constraint key.  The FK
+			 * constraint may reference columns in a different order than they
+			 * appear in the PK index, so we must map pk_attnums[i] to the
+			 * corresponding index column position.
+			 */
+			for (idx_col = 0; idx_col < riinfo->nkeys; idx_col++)
+			{
+				if (idx_rel->rd_index->indkey.values[idx_col] == riinfo->pk_attnums[i])
+					break;
+			}
+			Assert(idx_col < riinfo->nkeys);
+
+			/* 1-based attribute number */
+			fpmeta->index_attnos[i] = idx_col + 1;
+
+			fmgr_info_copy(&fpmeta->cast_func_finfo[i], &entry->cast_func_finfo,
+						   fpmeta->scratch_cxt);
+			fmgr_info_copy(&fpmeta->eq_opr_finfo[i], &entry->eq_opr_finfo,
+						   fpmeta->scratch_cxt);
+			fpmeta->regops[i] = get_opcode(eq_opr);
+
+			get_op_opfamily_properties(eq_opr,
+									   idx_rel->rd_opfamily[idx_col],
+									   false,
+									   &fpmeta->strats[i],
+									   &lefttype,
+									   &fpmeta->subtypes[i]);
 		}
-		Assert(idx_col < riinfo->nkeys);
-
-		/* 1-based attribute number */
-		fpmeta->index_attnos[i] = idx_col + 1;
-
-		fmgr_info_copy(&fpmeta->cast_func_finfo[i], &entry->cast_func_finfo,
-					   fpmeta->scratch_cxt);
-		fmgr_info_copy(&fpmeta->eq_opr_finfo[i], &entry->eq_opr_finfo,
-					   fpmeta->scratch_cxt);
-		fpmeta->regops[i] = get_opcode(eq_opr);
-
-		get_op_opfamily_properties(eq_opr,
-								   idx_rel->rd_opfamily[idx_col],
-								   false,
-								   &fpmeta->strats[i],
-								   &lefttype,
-								   &fpmeta->subtypes[i]);
-	}
 
-	MemoryContextSetParent(context, TopMemoryContext);
-	riinfo->fpmeta = fpmeta;
+		if (generation != riinfo->cache_generation || !riinfo->valid)
+		{
+			Oid			constraint_id = riinfo->constraint_id;
+
+			MemoryContextDelete(context);
+			riinfo = ri_LoadConstraintInfo(constraint_id);
+			continue;
+		}
+
+		/* Nested RI may have populated the same entry while we built ours. */
+		if (riinfo->fpmeta != NULL)
+		{
+			MemoryContextDelete(context);
+			return;
+		}
+
+		MemoryContextSetParent(context, TopMemoryContext);
+		riinfo->fpmeta = fpmeta;
+		return;
+	}
 }
 
 /*
@@ -4371,7 +4403,7 @@ ri_HashCompareOp(Oid eq_opr, Oid typeid)
 	 * If not already initialized, build a new generation of call information.
 	 * Use a separate context so invalidation cannot affect active callers.
 	 */
-	if (entry->info == NULL)
+	while (entry->info == NULL)
 	{
 		Oid			lefttype,
 					righttype,
@@ -4379,6 +4411,7 @@ ri_HashCompareOp(Oid eq_opr, Oid typeid)
 		CoercionPathType pathtype;
 		MemoryContext context;
 		RI_CompareInfo *info;
+		uint64		generation = ri_compare_cache_generation;
 
 		/*
 		 * If we chose to use a cast from FK to PK type, we may have to apply
@@ -4435,6 +4468,17 @@ ri_HashCompareOp(Oid eq_opr, Oid typeid)
 			fmgr_info_cxt(castfunc, &info->cast_func_finfo, context);
 		else
 			info->cast_func_finfo.fn_oid = InvalidOid;
+
+		/*
+		 * Discard this copy if invalidation was processed or a nested call
+		 * populated the same entry while we were constructing it.
+		 */
+		if (generation != ri_compare_cache_generation || entry->info != NULL)
+		{
+			MemoryContextDelete(context);
+			continue;
+		}
+
 		MemoryContextSetParent(context, TopMemoryContext);
 		entry->info = info;
 	}

Reply via email to