[18] Fix a few issues with the collation cache

Started by Jeff Davisover 1 year ago6 messages
#1Jeff Davis
pgsql@j-davis.com
5 attachment(s)

The collation cache, which maps collation oids to pg_locale_t objects,
has a few longstanding issues:

1. Using TopMemoryContext too much, with potential for leaks on error
paths.

2. Creating collators (UCollator for ICU or locale_t for libc) that can
leak on some error paths. For instance, the following will leak the
result of newlocale():

create collation c2 (provider=libc,
locale='C.utf8', version='bogus');

3. Not invalidating the cache. Collations don't change in a way that
matters during the lifetime of a backend, so the main problem is DROP
COLLATION. That can leave dangling entries in the cache until the
backend exits, and perhaps be a problem if there's OID wraparound.

The patches make use of resource owners for problems #2 and #3. There
aren't a lot of examples where resource owners are used in this way, so
I'd appreciate feedback on whether this is a reasonable thing to do or
not. Does it feel over-engineered? We can solve these problems by
rearranging the code to avoid problem #2 and iterating through the hash
table for problem #3, but using resource owners felt cleaner to me.

These problems exist in all supported branches, but the fixes are
somewhat invasive so I'm not inclined to backport them unless someone
thinks the problems are serious enough.

Regards,
Jeff Davis

Attachments:

v1-0001-Minor-refactor-of-collation-cache.patchtext/x-patch; charset=UTF-8; name=v1-0001-Minor-refactor-of-collation-cache.patchDownload
From 7a007d3573c6bda67729ab45bd58931bfcdaf702 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Tue, 6 Aug 2024 12:44:14 -0700
Subject: [PATCH v1 1/5] Minor refactor of collation cache.

Put collation cache logic in pg_newlocale_from_collation(), and
separate out the slow path for constructing a new pg_locale_t object.
---
 src/backend/utils/adt/pg_locale.c | 286 ++++++++++++++----------------
 1 file changed, 135 insertions(+), 151 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index cd3661e7279..1560209ef21 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1219,46 +1219,6 @@ IsoLocaleName(const char *winlocname)
 #endif							/* WIN32 && LC_MESSAGES */
 
 
-/*
- * Cache mechanism for collation information.
- *
- * Note that we currently lack any way to flush the cache.  Since we don't
- * support ALTER COLLATION, this is OK.  The worst case is that someone
- * drops a collation, and a useless cache entry hangs around in existing
- * backends.
- */
-static collation_cache_entry *
-lookup_collation_cache(Oid collation)
-{
-	collation_cache_entry *cache_entry;
-	bool		found;
-
-	Assert(OidIsValid(collation));
-	Assert(collation != DEFAULT_COLLATION_OID);
-
-	if (CollationCache == NULL)
-	{
-		CollationCacheContext = AllocSetContextCreate(TopMemoryContext,
-													  "collation cache",
-													  ALLOCSET_DEFAULT_SIZES);
-		CollationCache = collation_cache_create(CollationCacheContext,
-												16, NULL);
-	}
-
-	cache_entry = collation_cache_insert(CollationCache, collation, &found);
-	if (!found)
-	{
-		/*
-		 * Make sure cache entry is marked invalid, in case we fail before
-		 * setting things.
-		 */
-		cache_entry->locale = 0;
-	}
-
-	return cache_entry;
-}
-
-
 /*
  * Detect whether collation's LC_COLLATE property is C
  */
@@ -1551,149 +1511,173 @@ init_database_collation(void)
 }
 
 /*
- * Create a pg_locale_t from a collation OID.  Results are cached for the
- * lifetime of the backend.  Thus, do not free the result with freelocale().
+ * Create pg_locale_t from collation oid in TopMemoryContext.
  *
  * For simplicity, we always generate COLLATE + CTYPE even though we
  * might only need one of them.  Since this is called only once per session,
  * it shouldn't cost much.
  */
-pg_locale_t
-pg_newlocale_from_collation(Oid collid)
-{
-	collation_cache_entry *cache_entry;
+static pg_locale_t
+init_pg_locale(Oid collid)
+{
+	/* We haven't computed this yet in this session, so do it */
+	HeapTuple	tp;
+	Form_pg_collation collform;
+	struct pg_locale_struct result;
+	pg_locale_t resultp;
+	Datum		datum;
+	bool		isnull;
 
-	/* Callers must pass a valid OID */
-	Assert(OidIsValid(collid));
+	tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for collation %u", collid);
+	collform = (Form_pg_collation) GETSTRUCT(tp);
 
-	if (collid == DEFAULT_COLLATION_OID)
-		return &default_locale;
+	/* We'll fill in the result struct locally before allocating memory */
+	memset(&result, 0, sizeof(result));
+	result.provider = collform->collprovider;
+	result.deterministic = collform->collisdeterministic;
 
-	cache_entry = lookup_collation_cache(collid);
+	if (collform->collprovider == COLLPROVIDER_BUILTIN)
+	{
+		const char *locstr;
+
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
+		locstr = TextDatumGetCString(datum);
+
+		result.collate_is_c = true;
+		result.ctype_is_c = (strcmp(locstr, "C") == 0);
 
-	if (cache_entry->locale == 0)
+		builtin_validate_locale(GetDatabaseEncoding(), locstr);
+
+		result.info.builtin.locale = MemoryContextStrdup(TopMemoryContext,
+														 locstr);
+	}
+	else if (collform->collprovider == COLLPROVIDER_LIBC)
 	{
-		/* We haven't computed this yet in this session, so do it */
-		HeapTuple	tp;
-		Form_pg_collation collform;
-		struct pg_locale_struct result;
-		pg_locale_t resultp;
-		Datum		datum;
-		bool		isnull;
-
-		tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collid));
-		if (!HeapTupleIsValid(tp))
-			elog(ERROR, "cache lookup failed for collation %u", collid);
-		collform = (Form_pg_collation) GETSTRUCT(tp);
-
-		/* We'll fill in the result struct locally before allocating memory */
-		memset(&result, 0, sizeof(result));
-		result.provider = collform->collprovider;
-		result.deterministic = collform->collisdeterministic;
-
-		if (collform->collprovider == COLLPROVIDER_BUILTIN)
-		{
-			const char *locstr;
+		const char *collcollate;
+		const char *collctype;
 
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
-			locstr = TextDatumGetCString(datum);
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
+		collcollate = TextDatumGetCString(datum);
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collctype);
+		collctype = TextDatumGetCString(datum);
 
-			result.collate_is_c = true;
-			result.ctype_is_c = (strcmp(locstr, "C") == 0);
+		result.collate_is_c = (strcmp(collcollate, "C") == 0) ||
+			(strcmp(collcollate, "POSIX") == 0);
+		result.ctype_is_c = (strcmp(collctype, "C") == 0) ||
+			(strcmp(collctype, "POSIX") == 0);
 
-			builtin_validate_locale(GetDatabaseEncoding(), locstr);
+		make_libc_collator(collcollate, collctype, &result);
+	}
+	else if (collform->collprovider == COLLPROVIDER_ICU)
+	{
+		const char *iculocstr;
+		const char *icurules;
 
-			result.info.builtin.locale = MemoryContextStrdup(TopMemoryContext,
-															 locstr);
-		}
-		else if (collform->collprovider == COLLPROVIDER_LIBC)
-		{
-			const char *collcollate;
-			const char *collctype;
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
+		iculocstr = TextDatumGetCString(datum);
 
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
-			collcollate = TextDatumGetCString(datum);
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collctype);
-			collctype = TextDatumGetCString(datum);
+		result.collate_is_c = false;
+		result.ctype_is_c = false;
 
-			result.collate_is_c = (strcmp(collcollate, "C") == 0) ||
-				(strcmp(collcollate, "POSIX") == 0);
-			result.ctype_is_c = (strcmp(collctype, "C") == 0) ||
-				(strcmp(collctype, "POSIX") == 0);
+		datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collicurules, &isnull);
+		if (!isnull)
+			icurules = TextDatumGetCString(datum);
+		else
+			icurules = NULL;
 
-			make_libc_collator(collcollate, collctype, &result);
-		}
-		else if (collform->collprovider == COLLPROVIDER_ICU)
-		{
-			const char *iculocstr;
-			const char *icurules;
+		make_icu_collator(iculocstr, icurules, &result);
+	}
 
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
-			iculocstr = TextDatumGetCString(datum);
+	datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion,
+							&isnull);
+	if (!isnull)
+	{
+		char	   *actual_versionstr;
+		char	   *collversionstr;
 
-			result.collate_is_c = false;
-			result.ctype_is_c = false;
+		collversionstr = TextDatumGetCString(datum);
 
-			datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collicurules, &isnull);
-			if (!isnull)
-				icurules = TextDatumGetCString(datum);
-			else
-				icurules = NULL;
+		if (collform->collprovider == COLLPROVIDER_LIBC)
+			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
+		else
+			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
 
-			make_icu_collator(iculocstr, icurules, &result);
+		actual_versionstr = get_collation_actual_version(collform->collprovider,
+														 TextDatumGetCString(datum));
+		if (!actual_versionstr)
+		{
+			/*
+			 * This could happen when specifying a version in CREATE COLLATION
+			 * but the provider does not support versioning, or manually
+			 * creating a mess in the catalogs.
+			 */
+			ereport(ERROR,
+					(errmsg("collation \"%s\" has no actual version, but a version was recorded",
+							NameStr(collform->collname))));
 		}
 
-		datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion,
-								&isnull);
-		if (!isnull)
-		{
-			char	   *actual_versionstr;
-			char	   *collversionstr;
+		if (strcmp(actual_versionstr, collversionstr) != 0)
+			ereport(WARNING,
+					(errmsg("collation \"%s\" has version mismatch",
+							NameStr(collform->collname)),
+					 errdetail("The collation in the database was created using version %s, "
+							   "but the operating system provides version %s.",
+							   collversionstr, actual_versionstr),
+					 errhint("Rebuild all objects affected by this collation and run "
+							 "ALTER COLLATION %s REFRESH VERSION, "
+							 "or build PostgreSQL with the right library version.",
+							 quote_qualified_identifier(get_namespace_name(collform->collnamespace),
+														NameStr(collform->collname)))));
+	}
 
-			collversionstr = TextDatumGetCString(datum);
+	ReleaseSysCache(tp);
 
-			if (collform->collprovider == COLLPROVIDER_LIBC)
-				datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
-			else
-				datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
+	/* We'll keep the pg_locale_t structures in TopMemoryContext */
+	resultp = MemoryContextAlloc(TopMemoryContext, sizeof(*resultp));
+	*resultp = result;
 
-			actual_versionstr = get_collation_actual_version(collform->collprovider,
-															 TextDatumGetCString(datum));
-			if (!actual_versionstr)
-			{
-				/*
-				 * This could happen when specifying a version in CREATE
-				 * COLLATION but the provider does not support versioning, or
-				 * manually creating a mess in the catalogs.
-				 */
-				ereport(ERROR,
-						(errmsg("collation \"%s\" has no actual version, but a version was recorded",
-								NameStr(collform->collname))));
-			}
+	return resultp;
+}
 
-			if (strcmp(actual_versionstr, collversionstr) != 0)
-				ereport(WARNING,
-						(errmsg("collation \"%s\" has version mismatch",
-								NameStr(collform->collname)),
-						 errdetail("The collation in the database was created using version %s, "
-								   "but the operating system provides version %s.",
-								   collversionstr, actual_versionstr),
-						 errhint("Rebuild all objects affected by this collation and run "
-								 "ALTER COLLATION %s REFRESH VERSION, "
-								 "or build PostgreSQL with the right library version.",
-								 quote_qualified_identifier(get_namespace_name(collform->collnamespace),
-															NameStr(collform->collname)))));
-		}
+/*
+ * Retrieve a pg_locale_t from a collation OID.  Results are cached for the
+ * lifetime of the backend, thus do not free the result.
+ */
+pg_locale_t
+pg_newlocale_from_collation(Oid collid)
+{
+	collation_cache_entry *cache_entry;
+	bool		found;
 
-		ReleaseSysCache(tp);
+	/* Callers must pass a valid OID */
+	Assert(OidIsValid(collid));
 
-		/* We'll keep the pg_locale_t structures in TopMemoryContext */
-		resultp = MemoryContextAlloc(TopMemoryContext, sizeof(*resultp));
-		*resultp = result;
+	if (collid == DEFAULT_COLLATION_OID)
+		return &default_locale;
 
-		cache_entry->locale = resultp;
+	/*
+	 * Cache mechanism for collation information.
+	 *
+	 * Note that we currently lack any way to flush the cache.  Since we don't
+	 * support ALTER COLLATION, this is OK.  The worst case is that someone
+	 * drops a collation, and a useless cache entry hangs around in existing
+	 * backends.
+	 */
+	if (CollationCache == NULL)
+	{
+		CollationCacheContext = AllocSetContextCreate(TopMemoryContext,
+													  "collation cache",
+													  ALLOCSET_DEFAULT_SIZES);
+		CollationCache = collation_cache_create(CollationCacheContext,
+												16, NULL);
 	}
 
+	cache_entry = collation_cache_insert(CollationCache, collid, &found);
+	if (!found || !cache_entry->locale)
+		cache_entry->locale = init_pg_locale(collid);
+
 	return cache_entry->locale;
 }
 
-- 
2.34.1

v1-0002-Tighten-up-make_libc_collator-and-make_icu_collat.patchtext/x-patch; charset=UTF-8; name=v1-0002-Tighten-up-make_libc_collator-and-make_icu_collat.patchDownload
From 06e1cb3aec40655976fa35e17f055a3ce2057778 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 7 Aug 2024 11:05:46 -0700
Subject: [PATCH v1 2/5] Tighten up make_libc_collator() and
 make_icu_collator().

Return the result rather than using an out parameter, and make it the
caller's responsibility to copy it into the right context. Ensure that
no paths leak a collator.

The function make_icu_collator() doesn't have any external callers, so
change it to be static. Also, when re-opening with rules, close the
previous collator before there's a chance for errors.

In make_libc_collator(), if the first newlocale() succeeds and the
second one fails, close the first locale_t object.
---
 src/backend/utils/adt/pg_locale.c | 75 +++++++++++++++++++------------
 src/include/utils/pg_locale.h     |  4 --
 2 files changed, 47 insertions(+), 32 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 1560209ef21..20fa4ebe2b2 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1302,14 +1302,15 @@ report_newlocale_failure(const char *localename)
 }
 
 /*
- * Initialize the locale_t field.
+ * Create a locale_t with the given collation and ctype.
  *
- * The "C" and "POSIX" locales are not actually handled by libc, so set the
- * locale_t to zero in that case.
+ * The "C" and "POSIX" locales are not actually handled by libc, so return
+ * NULL.
+ *
+ * Ensure that no path leaks a locale_t.
  */
-static void
-make_libc_collator(const char *collate, const char *ctype,
-				   pg_locale_t result)
+static locale_t
+make_libc_collator(const char *collate, const char *ctype)
 {
 	locale_t	loc = 0;
 
@@ -1348,7 +1349,11 @@ make_libc_collator(const char *collate, const char *ctype,
 			errno = 0;
 			loc = newlocale(LC_CTYPE_MASK, ctype, loc1);
 			if (!loc)
+			{
+				if (loc1)
+					freelocale(loc1);
 				report_newlocale_failure(ctype);
+			}
 		}
 		else
 			loc = loc1;
@@ -1365,15 +1370,18 @@ make_libc_collator(const char *collate, const char *ctype,
 #endif
 	}
 
-	result->info.lt = loc;
+	return loc;
 }
 
-void
-make_icu_collator(const char *iculocstr,
-				  const char *icurules,
-				  struct pg_locale_struct *resultp)
-{
+/*
+ * Create a UCollator with the given locale string and rules.
+ *
+ * Ensure that no path leaks a UCollator.
+ */
 #ifdef USE_ICU
+static UCollator *
+make_icu_collator(const char *iculocstr, const char *icurules)
+{
 	UCollator  *collator;
 
 	collator = pg_ucol_open(iculocstr);
@@ -1391,14 +1399,14 @@ make_icu_collator(const char *iculocstr,
 		int32_t		length;
 
 		default_rules = ucol_getRules(collator, &length);
+		ucol_close(collator);
+
 		icu_to_uchar(&my_rules, icurules, strlen(icurules));
 
 		agg_rules = palloc_array(UChar, u_strlen(default_rules) + u_strlen(my_rules) + 1);
 		u_strcpy(agg_rules, default_rules);
 		u_strcat(agg_rules, my_rules);
 
-		ucol_close(collator);
-
 		status = U_ZERO_ERROR;
 		collator = ucol_openRules(agg_rules, u_strlen(agg_rules),
 								  UCOL_DEFAULT, UCOL_DEFAULT_STRENGTH, NULL, &status);
@@ -1409,16 +1417,9 @@ make_icu_collator(const char *iculocstr,
 							iculocstr, icurules, u_errorName(status))));
 	}
 
-	/* We will leak this string if the caller errors later :-( */
-	resultp->info.icu.locale = MemoryContextStrdup(TopMemoryContext, iculocstr);
-	resultp->info.icu.ucol = collator;
-#else							/* not USE_ICU */
-	/* could get here if a collation was created by a build with ICU */
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("ICU is not supported in this build")));
-#endif							/* not USE_ICU */
+	return collator;
 }
+#endif							/* not USE_ICU */
 
 
 bool
@@ -1436,7 +1437,6 @@ init_database_collation(void)
 	HeapTuple	tup;
 	Form_pg_database dbform;
 	Datum		datum;
-	bool		isnull;
 
 	/* Fetch our pg_database row normally, via syscache */
 	tup = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
@@ -1461,8 +1461,10 @@ init_database_collation(void)
 	}
 	else if (dbform->datlocprovider == COLLPROVIDER_ICU)
 	{
+#ifdef USE_ICU
 		char	   *datlocale;
 		char	   *icurules;
+		bool		isnull;
 
 		datum = SysCacheGetAttrNotNull(DATABASEOID, tup, Anum_pg_database_datlocale);
 		datlocale = TextDatumGetCString(datum);
@@ -1476,7 +1478,14 @@ init_database_collation(void)
 		else
 			icurules = NULL;
 
-		make_icu_collator(datlocale, icurules, &default_locale);
+		default_locale.info.icu.ucol = make_icu_collator(datlocale, icurules);
+		default_locale.info.icu.locale = MemoryContextStrdup(TopMemoryContext, datlocale);
+#else							/* not USE_ICU */
+		/* could get here if a collation was created by a build with ICU */
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("ICU is not supported in this build")));
+#endif							/* not USE_ICU */
 	}
 	else
 	{
@@ -1495,7 +1504,7 @@ init_database_collation(void)
 		default_locale.ctype_is_c = (strcmp(datctype, "C") == 0) ||
 			(strcmp(datctype, "POSIX") == 0);
 
-		make_libc_collator(datcollate, datctype, &default_locale);
+		default_locale.info.lt = make_libc_collator(datcollate, datctype);
 	}
 
 	default_locale.provider = dbform->datlocprovider;
@@ -1568,10 +1577,11 @@ init_pg_locale(Oid collid)
 		result.ctype_is_c = (strcmp(collctype, "C") == 0) ||
 			(strcmp(collctype, "POSIX") == 0);
 
-		make_libc_collator(collcollate, collctype, &result);
+		result.info.lt = make_libc_collator(collcollate, collctype);
 	}
 	else if (collform->collprovider == COLLPROVIDER_ICU)
 	{
+#ifdef USE_ICU
 		const char *iculocstr;
 		const char *icurules;
 
@@ -1587,7 +1597,14 @@ init_pg_locale(Oid collid)
 		else
 			icurules = NULL;
 
-		make_icu_collator(iculocstr, icurules, &result);
+		result.info.icu.ucol = make_icu_collator(iculocstr, icurules);
+		result.info.icu.locale = MemoryContextStrdup(TopMemoryContext, iculocstr);
+#else							/* not USE_ICU */
+		/* could get here if a collation was created by a build with ICU */
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("ICU is not supported in this build")));
+#endif							/* not USE_ICU */
 	}
 
 	datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion,
@@ -2530,6 +2547,8 @@ builtin_validate_locale(int encoding, const char *locale)
 /*
  * Wrapper around ucol_open() to handle API differences for older ICU
  * versions.
+ *
+ * Ensure that no path leaks a UCollator.
  */
 static UCollator *
 pg_ucol_open(const char *loc_str)
diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h
index f41d33975be..1ca0b7fa74b 100644
--- a/src/include/utils/pg_locale.h
+++ b/src/include/utils/pg_locale.h
@@ -107,10 +107,6 @@ struct pg_locale_struct
 
 typedef struct pg_locale_struct *pg_locale_t;
 
-extern void make_icu_collator(const char *iculocstr,
-							  const char *icurules,
-							  struct pg_locale_struct *resultp);
-
 extern bool pg_locale_deterministic(pg_locale_t locale);
 extern void init_database_collation(void);
 extern pg_locale_t pg_newlocale_from_collation(Oid collid);
-- 
2.34.1

v1-0003-For-collation-cache-use-CollationCacheContext-for.patchtext/x-patch; charset=UTF-8; name=v1-0003-For-collation-cache-use-CollationCacheContext-for.patchDownload
From e3985ae3574655878ae53f63390a2f4c7167fffc Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 7 Aug 2024 11:31:30 -0700
Subject: [PATCH v1 3/5] For collation cache, use CollationCacheContext for all
 memory.

Commit 005c6b833f introduced CollationCacheContext for the hash table,
but the pg_locale_t objects were still allocated in
TopMemoryContext. Change to use CollationCacheContext.

This change required a bit of refactoring to make the caller of
init_pg_locale() responsible for copying to the right context, so that
init_database_collation() can still use TopMemoryContext.
---
 src/backend/utils/adt/pg_locale.c | 79 ++++++++++++++++++++-----------
 1 file changed, 52 insertions(+), 27 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 20fa4ebe2b2..59fc99bd12b 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1520,11 +1520,11 @@ init_database_collation(void)
 }
 
 /*
- * Create pg_locale_t from collation oid in TopMemoryContext.
+ * Create pg_locale_t from collation oid.
  *
- * For simplicity, we always generate COLLATE + CTYPE even though we
- * might only need one of them.  Since this is called only once per session,
- * it shouldn't cost much.
+ * For simplicity, we always generate COLLATE + CTYPE even though we might
+ * only need one of them.  Since this is called only once per session, it
+ * shouldn't cost much.
  */
 static pg_locale_t
 init_pg_locale(Oid collid)
@@ -1532,8 +1532,7 @@ init_pg_locale(Oid collid)
 	/* We haven't computed this yet in this session, so do it */
 	HeapTuple	tp;
 	Form_pg_collation collform;
-	struct pg_locale_struct result;
-	pg_locale_t resultp;
+	pg_locale_t result;
 	Datum		datum;
 	bool		isnull;
 
@@ -1542,10 +1541,9 @@ init_pg_locale(Oid collid)
 		elog(ERROR, "cache lookup failed for collation %u", collid);
 	collform = (Form_pg_collation) GETSTRUCT(tp);
 
-	/* We'll fill in the result struct locally before allocating memory */
-	memset(&result, 0, sizeof(result));
-	result.provider = collform->collprovider;
-	result.deterministic = collform->collisdeterministic;
+	result = palloc0_object(struct pg_locale_struct);
+	result->provider = collform->collprovider;
+	result->deterministic = collform->collisdeterministic;
 
 	if (collform->collprovider == COLLPROVIDER_BUILTIN)
 	{
@@ -1554,13 +1552,12 @@ init_pg_locale(Oid collid)
 		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
 		locstr = TextDatumGetCString(datum);
 
-		result.collate_is_c = true;
-		result.ctype_is_c = (strcmp(locstr, "C") == 0);
+		result->collate_is_c = true;
+		result->ctype_is_c = (strcmp(locstr, "C") == 0);
 
 		builtin_validate_locale(GetDatabaseEncoding(), locstr);
 
-		result.info.builtin.locale = MemoryContextStrdup(TopMemoryContext,
-														 locstr);
+		result->info.builtin.locale = pstrdup(locstr);
 	}
 	else if (collform->collprovider == COLLPROVIDER_LIBC)
 	{
@@ -1572,12 +1569,12 @@ init_pg_locale(Oid collid)
 		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collctype);
 		collctype = TextDatumGetCString(datum);
 
-		result.collate_is_c = (strcmp(collcollate, "C") == 0) ||
+		result->collate_is_c = (strcmp(collcollate, "C") == 0) ||
 			(strcmp(collcollate, "POSIX") == 0);
-		result.ctype_is_c = (strcmp(collctype, "C") == 0) ||
+		result->ctype_is_c = (strcmp(collctype, "C") == 0) ||
 			(strcmp(collctype, "POSIX") == 0);
 
-		result.info.lt = make_libc_collator(collcollate, collctype);
+		result->info.lt = make_libc_collator(collcollate, collctype);
 	}
 	else if (collform->collprovider == COLLPROVIDER_ICU)
 	{
@@ -1588,8 +1585,8 @@ init_pg_locale(Oid collid)
 		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
 		iculocstr = TextDatumGetCString(datum);
 
-		result.collate_is_c = false;
-		result.ctype_is_c = false;
+		result->collate_is_c = false;
+		result->ctype_is_c = false;
 
 		datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collicurules, &isnull);
 		if (!isnull)
@@ -1597,8 +1594,8 @@ init_pg_locale(Oid collid)
 		else
 			icurules = NULL;
 
-		result.info.icu.ucol = make_icu_collator(iculocstr, icurules);
-		result.info.icu.locale = MemoryContextStrdup(TopMemoryContext, iculocstr);
+		result->info.icu.ucol = make_icu_collator(iculocstr, icurules);
+		result->info.icu.locale = pstrdup(iculocstr);
 #else							/* not USE_ICU */
 		/* could get here if a collation was created by a build with ICU */
 		ereport(ERROR,
@@ -1651,11 +1648,7 @@ init_pg_locale(Oid collid)
 
 	ReleaseSysCache(tp);
 
-	/* We'll keep the pg_locale_t structures in TopMemoryContext */
-	resultp = MemoryContextAlloc(TopMemoryContext, sizeof(*resultp));
-	*resultp = result;
-
-	return resultp;
+	return result;
 }
 
 /*
@@ -1693,7 +1686,39 @@ pg_newlocale_from_collation(Oid collid)
 
 	cache_entry = collation_cache_insert(CollationCache, collid, &found);
 	if (!found || !cache_entry->locale)
-		cache_entry->locale = init_pg_locale(collid);
+	{
+		MemoryContext oldcontext;
+		pg_locale_t tmplocale;
+		pg_locale_t locale;
+
+		tmplocale = init_pg_locale(collid);
+
+		oldcontext = MemoryContextSwitchTo(CollationCacheContext);
+
+		locale = palloc0_object(struct pg_locale_struct);
+		*locale = *tmplocale;
+
+		/* deep copy */
+		if (locale->provider == COLLPROVIDER_BUILTIN)
+		{
+			locale->info.builtin.locale = pstrdup(locale->info.builtin.locale);
+		}
+#ifdef USE_ICU
+		else if (locale->provider == COLLPROVIDER_ICU)
+		{
+			locale->info.icu.locale = pstrdup(locale->info.icu.locale);
+		}
+#endif
+		else if (locale->provider != COLLPROVIDER_LIBC)
+		{
+			/* shouldn't happen */
+			PGLOCALE_SUPPORT_ERROR(locale->provider);
+		}
+
+		MemoryContextSwitchTo(oldcontext);
+
+		cache_entry->locale = locale;
+	}
 
 	return cache_entry->locale;
 }
-- 
2.34.1

v1-0004-Use-resource-owners-to-track-locale_t-and-ICU-col.patchtext/x-patch; charset=UTF-8; name=v1-0004-Use-resource-owners-to-track-locale_t-and-ICU-col.patchDownload
From 9ef14874e3ea19edbed8c7aa3d855424ea330937 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 7 Aug 2024 12:25:37 -0700
Subject: [PATCH v1 4/5] Use resource owners to track locale_t and ICU collator
 objects.

Rather than rely on careful ordering of operations in error paths,
track collator objects with CurrentResourceOwner soon after they are
allocated. Then, move them to the new CollationCacheOwner when the
pg_locale_t is successfully allocated, right after copying the memory
to CollationCacheContext.
---
 src/backend/utils/adt/pg_locale.c | 94 ++++++++++++++++++++++++++++++-
 1 file changed, 91 insertions(+), 3 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 59fc99bd12b..2aaf11af09b 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -66,6 +66,7 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
+#include "utils/resowner.h"
 #include "utils/syscache.h"
 
 #ifdef USE_ICU
@@ -151,6 +152,16 @@ typedef struct
 static MemoryContext CollationCacheContext = NULL;
 static collation_cache_hash *CollationCache = NULL;
 
+/*
+ * Collator objects (UCollator for ICU or locale_t for libc) are allocated in
+ * an external library, so track them using a resource owner. While still
+ * initializing the pg_locale_t structure, the collators are owned by
+ * CurrentResourceOwner, so that they are released if an error is encountered
+ * partway through. After the pg_locale_t is fully initialized, the collators
+ * are transferred to CollationCacheOwner.
+ */
+static ResourceOwner CollationCacheOwner = NULL;
+
 #if defined(WIN32) && defined(LC_MESSAGES)
 static char *IsoLocaleName(const char *);
 #endif
@@ -174,6 +185,51 @@ static void icu_set_collation_attributes(UCollator *collator, const char *loc,
 										 UErrorCode *status);
 #endif
 
+static void ResOwnerReleaseLocaleT(Datum val);
+
+#ifdef USE_ICU
+static void ResOwnerReleaseICUCollator(Datum val);
+
+static const ResourceOwnerDesc ICUCollatorResourceKind =
+{
+	.name = "ICU collator reference",
+	.release_phase = RESOURCE_RELEASE_AFTER_LOCKS,
+	.release_priority = RELEASE_PRIO_LAST,
+	.ReleaseResource = ResOwnerReleaseICUCollator,
+	.DebugPrint = NULL			/* the default message is fine */
+};
+
+static void
+ResOwnerReleaseICUCollator(Datum val)
+{
+	UCollator  *ucol = (UCollator *) DatumGetPointer(val);
+
+	ucol_close(ucol);
+}
+
+#endif
+
+static const ResourceOwnerDesc LocaleTResourceKind =
+{
+	.name = "locale_t reference",
+	.release_phase = RESOURCE_RELEASE_AFTER_LOCKS,
+	.release_priority = RELEASE_PRIO_LAST,
+	.ReleaseResource = ResOwnerReleaseLocaleT,
+	.DebugPrint = NULL			/* the default message is fine */
+};
+
+static void
+ResOwnerReleaseLocaleT(Datum val)
+{
+	locale_t	loc = (locale_t) DatumGetPointer(val);
+
+#ifndef WIN32
+	freelocale(loc);
+#else
+	_free_locale(loc);
+#endif
+}
+
 /*
  * POSIX doesn't define _l-variants of these functions, but several systems
  * have them.  We provide our own replacements here.
@@ -1574,7 +1630,12 @@ init_pg_locale(Oid collid)
 		result->ctype_is_c = (strcmp(collctype, "C") == 0) ||
 			(strcmp(collctype, "POSIX") == 0);
 
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 		result->info.lt = make_libc_collator(collcollate, collctype);
+		if (result->info.lt != NULL)
+			ResourceOwnerRemember(CurrentResourceOwner,
+								  PointerGetDatum(result->info.lt),
+								  &LocaleTResourceKind);
 	}
 	else if (collform->collprovider == COLLPROVIDER_ICU)
 	{
@@ -1594,7 +1655,11 @@ init_pg_locale(Oid collid)
 		else
 			icurules = NULL;
 
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 		result->info.icu.ucol = make_icu_collator(iculocstr, icurules);
+		Assert(result->info.icu.ucol != NULL);
+		ResourceOwnerRemember(CurrentResourceOwner, PointerGetDatum(result->info.icu.ucol),
+							  &ICUCollatorResourceKind);
 		result->info.icu.locale = pstrdup(iculocstr);
 #else							/* not USE_ICU */
 		/* could get here if a collation was created by a build with ICU */
@@ -1677,6 +1742,7 @@ pg_newlocale_from_collation(Oid collid)
 	 */
 	if (CollationCache == NULL)
 	{
+		CollationCacheOwner = ResourceOwnerCreate(NULL, "collation cache");
 		CollationCacheContext = AllocSetContextCreate(TopMemoryContext,
 													  "collation cache",
 													  ALLOCSET_DEFAULT_SIZES);
@@ -1691,14 +1757,20 @@ pg_newlocale_from_collation(Oid collid)
 		pg_locale_t tmplocale;
 		pg_locale_t locale;
 
+		ResourceOwnerEnlarge(CollationCacheOwner);
+
 		tmplocale = init_pg_locale(collid);
 
+		/*
+		 * Deep copy the memory into CollationCacheContext and reassign the
+		 * collators to CollationCacheOwner.
+		 */
+
 		oldcontext = MemoryContextSwitchTo(CollationCacheContext);
 
 		locale = palloc0_object(struct pg_locale_struct);
 		*locale = *tmplocale;
 
-		/* deep copy */
 		if (locale->provider == COLLPROVIDER_BUILTIN)
 		{
 			locale->info.builtin.locale = pstrdup(locale->info.builtin.locale);
@@ -1707,13 +1779,29 @@ pg_newlocale_from_collation(Oid collid)
 		else if (locale->provider == COLLPROVIDER_ICU)
 		{
 			locale->info.icu.locale = pstrdup(locale->info.icu.locale);
+			Assert(locale->info.icu.ucol != NULL);
+			ResourceOwnerForget(CurrentResourceOwner,
+								PointerGetDatum(locale->info.icu.ucol),
+								&ICUCollatorResourceKind);
+			ResourceOwnerRemember(CollationCacheOwner,
+								  PointerGetDatum(locale->info.icu.ucol),
+								  &ICUCollatorResourceKind);
 		}
 #endif
-		else if (locale->provider != COLLPROVIDER_LIBC)
+		else if (locale->provider == COLLPROVIDER_LIBC)
 		{
+			/* may be NULL if locale is "C" or "POSIX" */
+			if (locale->info.lt != NULL)
+			{
+				ResourceOwnerForget(CurrentResourceOwner, PointerGetDatum(locale->info.lt),
+									&LocaleTResourceKind);
+				ResourceOwnerRemember(CollationCacheOwner, PointerGetDatum(locale->info.lt),
+									  &LocaleTResourceKind);
+			}
+		}
+		else
 			/* shouldn't happen */
 			PGLOCALE_SUPPORT_ERROR(locale->provider);
-		}
 
 		MemoryContextSwitchTo(oldcontext);
 
-- 
2.34.1

v1-0005-Invalidate-collation-cache-when-appropriate.patchtext/x-patch; charset=UTF-8; name=v1-0005-Invalidate-collation-cache-when-appropriate.patchDownload
From 7e04f81d825bcddf2c5ec3e7a156f30c8d1b67eb Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 7 Aug 2024 13:03:32 -0700
Subject: [PATCH v1 5/5] Invalidate collation cache when appropriate.

Previously, DROP COLLATION could leave a cache entry around. That's
not normally a problem, but can be if oid wraparound causes the same
oid to be reused for a different collation.
---
 src/backend/utils/adt/pg_locale.c | 36 ++++++++++++++++++++++---------
 1 file changed, 26 insertions(+), 10 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 2aaf11af09b..7c5abb5eca3 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -63,6 +63,7 @@
 #include "utils/builtins.h"
 #include "utils/formatting.h"
 #include "utils/guc_hooks.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
@@ -158,7 +159,8 @@ static collation_cache_hash *CollationCache = NULL;
  * initializing the pg_locale_t structure, the collators are owned by
  * CurrentResourceOwner, so that they are released if an error is encountered
  * partway through. After the pg_locale_t is fully initialized, the collators
- * are transferred to CollationCacheOwner.
+ * are transferred to CollationCacheOwner, which lasts until the next cache
+ * invalidation.
  */
 static ResourceOwner CollationCacheOwner = NULL;
 
@@ -1484,6 +1486,23 @@ pg_locale_deterministic(pg_locale_t locale)
 	return locale->deterministic;
 }
 
+static void
+CollationCacheInvalidate(Datum arg, int cacheid, uint32 hashvalue)
+{
+	/* free all memory and reset hash table */
+	MemoryContextReset(CollationCacheContext);
+	CollationCache = collation_cache_create(CollationCacheContext,
+											16, NULL);
+
+	/* release ICU collator and locale_t objects */
+#ifdef USE_ICU
+	ResourceOwnerReleaseAllOfKind(CollationCacheOwner,
+								  &ICUCollatorResourceKind);
+#endif
+	ResourceOwnerReleaseAllOfKind(CollationCacheOwner,
+								  &LocaleTResourceKind);
+}
+
 /*
  * Initialize default_locale with database locale settings.
  */
@@ -1732,14 +1751,7 @@ pg_newlocale_from_collation(Oid collid)
 	if (collid == DEFAULT_COLLATION_OID)
 		return &default_locale;
 
-	/*
-	 * Cache mechanism for collation information.
-	 *
-	 * Note that we currently lack any way to flush the cache.  Since we don't
-	 * support ALTER COLLATION, this is OK.  The worst case is that someone
-	 * drops a collation, and a useless cache entry hangs around in existing
-	 * backends.
-	 */
+	/* cache mechanism for collation information */
 	if (CollationCache == NULL)
 	{
 		CollationCacheOwner = ResourceOwnerCreate(NULL, "collation cache");
@@ -1748,6 +1760,9 @@ pg_newlocale_from_collation(Oid collid)
 													  ALLOCSET_DEFAULT_SIZES);
 		CollationCache = collation_cache_create(CollationCacheContext,
 												16, NULL);
+		CacheRegisterSyscacheCallback(COLLOID,
+									  CollationCacheInvalidate,
+									  (Datum) 0);
 	}
 
 	cache_entry = collation_cache_insert(CollationCache, collid, &found);
@@ -1763,7 +1778,8 @@ pg_newlocale_from_collation(Oid collid)
 
 		/*
 		 * Deep copy the memory into CollationCacheContext and reassign the
-		 * collators to CollationCacheOwner.
+		 * collators to CollationCacheOwner, which last until the next cache
+		 * invalidation.
 		 */
 
 		oldcontext = MemoryContextSwitchTo(CollationCacheContext);
-- 
2.34.1

#2Jeff Davis
pgsql@j-davis.com
In reply to: Jeff Davis (#1)
6 attachment(s)
Re: [18] Fix a few issues with the collation cache

On Thu, 2024-08-08 at 12:24 -0700, Jeff Davis wrote:

The collation cache, which maps collation oids to pg_locale_t
objects,
has a few longstanding issues:

Here's a patch set v2.

I changed it so that the pg_locale_t itself a resource kind, rather
than having separate locale_t and UCollator resource kinds. That
requires a bit more care to make sure that the pg_locale_t can be
initialized without leaking the locale_t or UCollator, but worked out
to be simpler overall.

A potential benefit of these changes is that, for eventual support of
multi-lib or an extension hook, improvements in the API here will make
things less error-prone.

Regards,
Jeff Davis

Attachments:

v2-0001-Minor-refactor-of-collation-cache.patchtext/x-patch; charset=UTF-8; name=v2-0001-Minor-refactor-of-collation-cache.patchDownload
From 7c5390d9cea9b8b3abb94db26337e10c60fe3756 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Tue, 6 Aug 2024 12:44:14 -0700
Subject: [PATCH v2 1/6] Minor refactor of collation cache.

Put collation cache logic in pg_newlocale_from_collation(), and
separate out the slow path for constructing a new pg_locale_t object.

Discussion: https://postgr.es/m/54d20e812bd6c3e44c10eddcd757ec494ebf1803.camel@j-davis.com
---
 src/backend/utils/adt/pg_locale.c | 286 ++++++++++++++----------------
 1 file changed, 135 insertions(+), 151 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index cd3661e7279..1668236e779 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1219,46 +1219,6 @@ IsoLocaleName(const char *winlocname)
 #endif							/* WIN32 && LC_MESSAGES */
 
 
-/*
- * Cache mechanism for collation information.
- *
- * Note that we currently lack any way to flush the cache.  Since we don't
- * support ALTER COLLATION, this is OK.  The worst case is that someone
- * drops a collation, and a useless cache entry hangs around in existing
- * backends.
- */
-static collation_cache_entry *
-lookup_collation_cache(Oid collation)
-{
-	collation_cache_entry *cache_entry;
-	bool		found;
-
-	Assert(OidIsValid(collation));
-	Assert(collation != DEFAULT_COLLATION_OID);
-
-	if (CollationCache == NULL)
-	{
-		CollationCacheContext = AllocSetContextCreate(TopMemoryContext,
-													  "collation cache",
-													  ALLOCSET_DEFAULT_SIZES);
-		CollationCache = collation_cache_create(CollationCacheContext,
-												16, NULL);
-	}
-
-	cache_entry = collation_cache_insert(CollationCache, collation, &found);
-	if (!found)
-	{
-		/*
-		 * Make sure cache entry is marked invalid, in case we fail before
-		 * setting things.
-		 */
-		cache_entry->locale = 0;
-	}
-
-	return cache_entry;
-}
-
-
 /*
  * Detect whether collation's LC_COLLATE property is C
  */
@@ -1551,149 +1511,173 @@ init_database_collation(void)
 }
 
 /*
- * Create a pg_locale_t from a collation OID.  Results are cached for the
- * lifetime of the backend.  Thus, do not free the result with freelocale().
+ * Create pg_locale_t from collation oid in TopMemoryContext.
  *
  * For simplicity, we always generate COLLATE + CTYPE even though we
  * might only need one of them.  Since this is called only once per session,
  * it shouldn't cost much.
  */
-pg_locale_t
-pg_newlocale_from_collation(Oid collid)
-{
-	collation_cache_entry *cache_entry;
+static pg_locale_t
+create_pg_locale(Oid collid)
+{
+	/* We haven't computed this yet in this session, so do it */
+	HeapTuple	tp;
+	Form_pg_collation collform;
+	struct pg_locale_struct result;
+	pg_locale_t resultp;
+	Datum		datum;
+	bool		isnull;
 
-	/* Callers must pass a valid OID */
-	Assert(OidIsValid(collid));
+	tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for collation %u", collid);
+	collform = (Form_pg_collation) GETSTRUCT(tp);
 
-	if (collid == DEFAULT_COLLATION_OID)
-		return &default_locale;
+	/* We'll fill in the result struct locally before allocating memory */
+	memset(&result, 0, sizeof(result));
+	result.provider = collform->collprovider;
+	result.deterministic = collform->collisdeterministic;
 
-	cache_entry = lookup_collation_cache(collid);
+	if (collform->collprovider == COLLPROVIDER_BUILTIN)
+	{
+		const char *locstr;
+
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
+		locstr = TextDatumGetCString(datum);
+
+		result.collate_is_c = true;
+		result.ctype_is_c = (strcmp(locstr, "C") == 0);
 
-	if (cache_entry->locale == 0)
+		builtin_validate_locale(GetDatabaseEncoding(), locstr);
+
+		result.info.builtin.locale = MemoryContextStrdup(TopMemoryContext,
+														 locstr);
+	}
+	else if (collform->collprovider == COLLPROVIDER_LIBC)
 	{
-		/* We haven't computed this yet in this session, so do it */
-		HeapTuple	tp;
-		Form_pg_collation collform;
-		struct pg_locale_struct result;
-		pg_locale_t resultp;
-		Datum		datum;
-		bool		isnull;
-
-		tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collid));
-		if (!HeapTupleIsValid(tp))
-			elog(ERROR, "cache lookup failed for collation %u", collid);
-		collform = (Form_pg_collation) GETSTRUCT(tp);
-
-		/* We'll fill in the result struct locally before allocating memory */
-		memset(&result, 0, sizeof(result));
-		result.provider = collform->collprovider;
-		result.deterministic = collform->collisdeterministic;
-
-		if (collform->collprovider == COLLPROVIDER_BUILTIN)
-		{
-			const char *locstr;
+		const char *collcollate;
+		const char *collctype;
 
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
-			locstr = TextDatumGetCString(datum);
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
+		collcollate = TextDatumGetCString(datum);
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collctype);
+		collctype = TextDatumGetCString(datum);
 
-			result.collate_is_c = true;
-			result.ctype_is_c = (strcmp(locstr, "C") == 0);
+		result.collate_is_c = (strcmp(collcollate, "C") == 0) ||
+			(strcmp(collcollate, "POSIX") == 0);
+		result.ctype_is_c = (strcmp(collctype, "C") == 0) ||
+			(strcmp(collctype, "POSIX") == 0);
 
-			builtin_validate_locale(GetDatabaseEncoding(), locstr);
+		make_libc_collator(collcollate, collctype, &result);
+	}
+	else if (collform->collprovider == COLLPROVIDER_ICU)
+	{
+		const char *iculocstr;
+		const char *icurules;
 
-			result.info.builtin.locale = MemoryContextStrdup(TopMemoryContext,
-															 locstr);
-		}
-		else if (collform->collprovider == COLLPROVIDER_LIBC)
-		{
-			const char *collcollate;
-			const char *collctype;
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
+		iculocstr = TextDatumGetCString(datum);
 
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
-			collcollate = TextDatumGetCString(datum);
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collctype);
-			collctype = TextDatumGetCString(datum);
+		result.collate_is_c = false;
+		result.ctype_is_c = false;
 
-			result.collate_is_c = (strcmp(collcollate, "C") == 0) ||
-				(strcmp(collcollate, "POSIX") == 0);
-			result.ctype_is_c = (strcmp(collctype, "C") == 0) ||
-				(strcmp(collctype, "POSIX") == 0);
+		datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collicurules, &isnull);
+		if (!isnull)
+			icurules = TextDatumGetCString(datum);
+		else
+			icurules = NULL;
 
-			make_libc_collator(collcollate, collctype, &result);
-		}
-		else if (collform->collprovider == COLLPROVIDER_ICU)
-		{
-			const char *iculocstr;
-			const char *icurules;
+		make_icu_collator(iculocstr, icurules, &result);
+	}
 
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
-			iculocstr = TextDatumGetCString(datum);
+	datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion,
+							&isnull);
+	if (!isnull)
+	{
+		char	   *actual_versionstr;
+		char	   *collversionstr;
 
-			result.collate_is_c = false;
-			result.ctype_is_c = false;
+		collversionstr = TextDatumGetCString(datum);
 
-			datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collicurules, &isnull);
-			if (!isnull)
-				icurules = TextDatumGetCString(datum);
-			else
-				icurules = NULL;
+		if (collform->collprovider == COLLPROVIDER_LIBC)
+			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
+		else
+			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
 
-			make_icu_collator(iculocstr, icurules, &result);
+		actual_versionstr = get_collation_actual_version(collform->collprovider,
+														 TextDatumGetCString(datum));
+		if (!actual_versionstr)
+		{
+			/*
+			 * This could happen when specifying a version in CREATE COLLATION
+			 * but the provider does not support versioning, or manually
+			 * creating a mess in the catalogs.
+			 */
+			ereport(ERROR,
+					(errmsg("collation \"%s\" has no actual version, but a version was recorded",
+							NameStr(collform->collname))));
 		}
 
-		datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion,
-								&isnull);
-		if (!isnull)
-		{
-			char	   *actual_versionstr;
-			char	   *collversionstr;
+		if (strcmp(actual_versionstr, collversionstr) != 0)
+			ereport(WARNING,
+					(errmsg("collation \"%s\" has version mismatch",
+							NameStr(collform->collname)),
+					 errdetail("The collation in the database was created using version %s, "
+							   "but the operating system provides version %s.",
+							   collversionstr, actual_versionstr),
+					 errhint("Rebuild all objects affected by this collation and run "
+							 "ALTER COLLATION %s REFRESH VERSION, "
+							 "or build PostgreSQL with the right library version.",
+							 quote_qualified_identifier(get_namespace_name(collform->collnamespace),
+														NameStr(collform->collname)))));
+	}
 
-			collversionstr = TextDatumGetCString(datum);
+	ReleaseSysCache(tp);
 
-			if (collform->collprovider == COLLPROVIDER_LIBC)
-				datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
-			else
-				datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
+	/* We'll keep the pg_locale_t structures in TopMemoryContext */
+	resultp = MemoryContextAlloc(TopMemoryContext, sizeof(*resultp));
+	*resultp = result;
 
-			actual_versionstr = get_collation_actual_version(collform->collprovider,
-															 TextDatumGetCString(datum));
-			if (!actual_versionstr)
-			{
-				/*
-				 * This could happen when specifying a version in CREATE
-				 * COLLATION but the provider does not support versioning, or
-				 * manually creating a mess in the catalogs.
-				 */
-				ereport(ERROR,
-						(errmsg("collation \"%s\" has no actual version, but a version was recorded",
-								NameStr(collform->collname))));
-			}
+	return resultp;
+}
 
-			if (strcmp(actual_versionstr, collversionstr) != 0)
-				ereport(WARNING,
-						(errmsg("collation \"%s\" has version mismatch",
-								NameStr(collform->collname)),
-						 errdetail("The collation in the database was created using version %s, "
-								   "but the operating system provides version %s.",
-								   collversionstr, actual_versionstr),
-						 errhint("Rebuild all objects affected by this collation and run "
-								 "ALTER COLLATION %s REFRESH VERSION, "
-								 "or build PostgreSQL with the right library version.",
-								 quote_qualified_identifier(get_namespace_name(collform->collnamespace),
-															NameStr(collform->collname)))));
-		}
+/*
+ * Retrieve a pg_locale_t from a collation OID.  Results are cached for the
+ * lifetime of the backend, thus do not free the result.
+ */
+pg_locale_t
+pg_newlocale_from_collation(Oid collid)
+{
+	collation_cache_entry *cache_entry;
+	bool		found;
 
-		ReleaseSysCache(tp);
+	/* Callers must pass a valid OID */
+	Assert(OidIsValid(collid));
 
-		/* We'll keep the pg_locale_t structures in TopMemoryContext */
-		resultp = MemoryContextAlloc(TopMemoryContext, sizeof(*resultp));
-		*resultp = result;
+	if (collid == DEFAULT_COLLATION_OID)
+		return &default_locale;
 
-		cache_entry->locale = resultp;
+	/*
+	 * Cache mechanism for collation information.
+	 *
+	 * Note that we currently lack any way to flush the cache.  Since we don't
+	 * support ALTER COLLATION, this is OK.  The worst case is that someone
+	 * drops a collation, and a useless cache entry hangs around in existing
+	 * backends.
+	 */
+	if (CollationCache == NULL)
+	{
+		CollationCacheContext = AllocSetContextCreate(TopMemoryContext,
+													  "collation cache",
+													  ALLOCSET_DEFAULT_SIZES);
+		CollationCache = collation_cache_create(CollationCacheContext,
+												16, NULL);
 	}
 
+	cache_entry = collation_cache_insert(CollationCache, collid, &found);
+	if (!found || !cache_entry->locale)
+		cache_entry->locale = create_pg_locale(collid);
+
 	return cache_entry->locale;
 }
 
-- 
2.34.1

v2-0002-Tighten-up-make_libc_collator-and-make_icu_collat.patchtext/x-patch; charset=UTF-8; name=v2-0002-Tighten-up-make_libc_collator-and-make_icu_collat.patchDownload
From a8da929f5961c169b359ad93f31f59b8baac00c8 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 7 Aug 2024 11:05:46 -0700
Subject: [PATCH v2 2/6] Tighten up make_libc_collator() and
 make_icu_collator().

Return the result rather than using an out parameter, and make it the
caller's responsibility to copy it into the right context. Ensure that
no paths leak a collator.

The function make_icu_collator() doesn't have any external callers, so
change it to be static. Also, when re-opening with rules, close the
previous collator before there's a chance for errors.

In make_libc_collator(), if the first newlocale() succeeds and the
second one fails, close the first locale_t object.
---
 src/backend/utils/adt/pg_locale.c | 75 +++++++++++++++++++------------
 src/include/utils/pg_locale.h     |  4 --
 2 files changed, 47 insertions(+), 32 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 1668236e779..5a3dccf757b 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1302,14 +1302,15 @@ report_newlocale_failure(const char *localename)
 }
 
 /*
- * Initialize the locale_t field.
+ * Create a locale_t with the given collation and ctype.
  *
- * The "C" and "POSIX" locales are not actually handled by libc, so set the
- * locale_t to zero in that case.
+ * The "C" and "POSIX" locales are not actually handled by libc, so return
+ * NULL.
+ *
+ * Ensure that no path leaks a locale_t.
  */
-static void
-make_libc_collator(const char *collate, const char *ctype,
-				   pg_locale_t result)
+static locale_t
+make_libc_collator(const char *collate, const char *ctype)
 {
 	locale_t	loc = 0;
 
@@ -1348,7 +1349,11 @@ make_libc_collator(const char *collate, const char *ctype,
 			errno = 0;
 			loc = newlocale(LC_CTYPE_MASK, ctype, loc1);
 			if (!loc)
+			{
+				if (loc1)
+					freelocale(loc1);
 				report_newlocale_failure(ctype);
+			}
 		}
 		else
 			loc = loc1;
@@ -1365,15 +1370,18 @@ make_libc_collator(const char *collate, const char *ctype,
 #endif
 	}
 
-	result->info.lt = loc;
+	return loc;
 }
 
-void
-make_icu_collator(const char *iculocstr,
-				  const char *icurules,
-				  struct pg_locale_struct *resultp)
-{
+/*
+ * Create a UCollator with the given locale string and rules.
+ *
+ * Ensure that no path leaks a UCollator.
+ */
 #ifdef USE_ICU
+static UCollator *
+make_icu_collator(const char *iculocstr, const char *icurules)
+{
 	UCollator  *collator;
 
 	collator = pg_ucol_open(iculocstr);
@@ -1391,14 +1399,14 @@ make_icu_collator(const char *iculocstr,
 		int32_t		length;
 
 		default_rules = ucol_getRules(collator, &length);
+		ucol_close(collator);
+
 		icu_to_uchar(&my_rules, icurules, strlen(icurules));
 
 		agg_rules = palloc_array(UChar, u_strlen(default_rules) + u_strlen(my_rules) + 1);
 		u_strcpy(agg_rules, default_rules);
 		u_strcat(agg_rules, my_rules);
 
-		ucol_close(collator);
-
 		status = U_ZERO_ERROR;
 		collator = ucol_openRules(agg_rules, u_strlen(agg_rules),
 								  UCOL_DEFAULT, UCOL_DEFAULT_STRENGTH, NULL, &status);
@@ -1409,16 +1417,9 @@ make_icu_collator(const char *iculocstr,
 							iculocstr, icurules, u_errorName(status))));
 	}
 
-	/* We will leak this string if the caller errors later :-( */
-	resultp->info.icu.locale = MemoryContextStrdup(TopMemoryContext, iculocstr);
-	resultp->info.icu.ucol = collator;
-#else							/* not USE_ICU */
-	/* could get here if a collation was created by a build with ICU */
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("ICU is not supported in this build")));
-#endif							/* not USE_ICU */
+	return collator;
 }
+#endif							/* not USE_ICU */
 
 
 bool
@@ -1436,7 +1437,6 @@ init_database_collation(void)
 	HeapTuple	tup;
 	Form_pg_database dbform;
 	Datum		datum;
-	bool		isnull;
 
 	/* Fetch our pg_database row normally, via syscache */
 	tup = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
@@ -1461,8 +1461,10 @@ init_database_collation(void)
 	}
 	else if (dbform->datlocprovider == COLLPROVIDER_ICU)
 	{
+#ifdef USE_ICU
 		char	   *datlocale;
 		char	   *icurules;
+		bool		isnull;
 
 		datum = SysCacheGetAttrNotNull(DATABASEOID, tup, Anum_pg_database_datlocale);
 		datlocale = TextDatumGetCString(datum);
@@ -1476,7 +1478,14 @@ init_database_collation(void)
 		else
 			icurules = NULL;
 
-		make_icu_collator(datlocale, icurules, &default_locale);
+		default_locale.info.icu.ucol = make_icu_collator(datlocale, icurules);
+		default_locale.info.icu.locale = MemoryContextStrdup(TopMemoryContext, datlocale);
+#else							/* not USE_ICU */
+		/* could get here if a collation was created by a build with ICU */
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("ICU is not supported in this build")));
+#endif							/* not USE_ICU */
 	}
 	else
 	{
@@ -1495,7 +1504,7 @@ init_database_collation(void)
 		default_locale.ctype_is_c = (strcmp(datctype, "C") == 0) ||
 			(strcmp(datctype, "POSIX") == 0);
 
-		make_libc_collator(datcollate, datctype, &default_locale);
+		default_locale.info.lt = make_libc_collator(datcollate, datctype);
 	}
 
 	default_locale.provider = dbform->datlocprovider;
@@ -1568,10 +1577,11 @@ create_pg_locale(Oid collid)
 		result.ctype_is_c = (strcmp(collctype, "C") == 0) ||
 			(strcmp(collctype, "POSIX") == 0);
 
-		make_libc_collator(collcollate, collctype, &result);
+		result.info.lt = make_libc_collator(collcollate, collctype);
 	}
 	else if (collform->collprovider == COLLPROVIDER_ICU)
 	{
+#ifdef USE_ICU
 		const char *iculocstr;
 		const char *icurules;
 
@@ -1587,7 +1597,14 @@ create_pg_locale(Oid collid)
 		else
 			icurules = NULL;
 
-		make_icu_collator(iculocstr, icurules, &result);
+		result.info.icu.ucol = make_icu_collator(iculocstr, icurules);
+		result.info.icu.locale = MemoryContextStrdup(TopMemoryContext, iculocstr);
+#else							/* not USE_ICU */
+		/* could get here if a collation was created by a build with ICU */
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("ICU is not supported in this build")));
+#endif							/* not USE_ICU */
 	}
 
 	datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion,
@@ -2530,6 +2547,8 @@ builtin_validate_locale(int encoding, const char *locale)
 /*
  * Wrapper around ucol_open() to handle API differences for older ICU
  * versions.
+ *
+ * Ensure that no path leaks a UCollator.
  */
 static UCollator *
 pg_ucol_open(const char *loc_str)
diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h
index f41d33975be..1ca0b7fa74b 100644
--- a/src/include/utils/pg_locale.h
+++ b/src/include/utils/pg_locale.h
@@ -107,10 +107,6 @@ struct pg_locale_struct
 
 typedef struct pg_locale_struct *pg_locale_t;
 
-extern void make_icu_collator(const char *iculocstr,
-							  const char *icurules,
-							  struct pg_locale_struct *resultp);
-
 extern bool pg_locale_deterministic(pg_locale_t locale);
 extern void init_database_collation(void);
 extern pg_locale_t pg_newlocale_from_collation(Oid collid);
-- 
2.34.1

v2-0003-Refactor-collation-version-check-into-new-functio.patchtext/x-patch; charset=UTF-8; name=v2-0003-Refactor-collation-version-check-into-new-functio.patchDownload
From c0671c641af3399bfc7a809153af9fa40ed166b2 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Tue, 13 Aug 2024 13:24:07 -0700
Subject: [PATCH v2 3/6] Refactor collation version check into new function.

Will make it easier, in an upcoming commit, to avoid a resource leak
if the version check raises an ERROR. Also adds a nice place for a
comment.
---
 src/backend/utils/adt/pg_locale.c | 124 +++++++++++++++++++-----------
 1 file changed, 80 insertions(+), 44 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 5a3dccf757b..db9d1c01947 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1519,12 +1519,84 @@ init_database_collation(void)
 	ReleaseSysCache(tup);
 }
 
+/*
+ * Check that the collation version in the catalog (from the time the
+ * collation was last created or refreshed) matches the version currently
+ * reported by the collation provider.
+ *
+ * A mismatch can indicate that the provider has been updated, which can lead
+ * to index inconsistencies or other problems.
+ */
+static void
+check_collation_version(Oid collid)
+{
+	/* We haven't computed this yet in this session, so do it */
+	HeapTuple	tp;
+	Form_pg_collation collform;
+	Datum		datum;
+	bool		isnull;
+	char	   *actual_versionstr;
+	char	   *collversionstr;
+
+	tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for collation %u", collid);
+	collform = (Form_pg_collation) GETSTRUCT(tp);
+
+	datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion,
+							&isnull);
+	if (isnull)
+	{
+		ReleaseSysCache(tp);
+		return;
+	}
+
+	collversionstr = TextDatumGetCString(datum);
+
+	if (collform->collprovider == COLLPROVIDER_LIBC)
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
+	else
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
+
+	actual_versionstr = get_collation_actual_version(collform->collprovider,
+													 TextDatumGetCString(datum));
+	if (!actual_versionstr)
+	{
+		/*
+		 * This could happen when specifying a version in CREATE COLLATION but
+		 * the provider does not support versioning, or manually creating a
+		 * mess in the catalogs.
+		 */
+		ereport(ERROR,
+				(errmsg("collation \"%s\" has no actual version, but a version was recorded",
+						NameStr(collform->collname))));
+	}
+
+	if (strcmp(actual_versionstr, collversionstr) != 0)
+		ereport(WARNING,
+				(errmsg("collation \"%s\" has version mismatch",
+						NameStr(collform->collname)),
+				 errdetail("The collation in the database was created using version %s, "
+						   "but the operating system provides version %s.",
+						   collversionstr, actual_versionstr),
+				 errhint("Rebuild all objects affected by this collation and run "
+						 "ALTER COLLATION %s REFRESH VERSION, "
+						 "or build PostgreSQL with the right library version.",
+						 quote_qualified_identifier(get_namespace_name(collform->collnamespace),
+													NameStr(collform->collname)))));
+
+	ReleaseSysCache(tp);
+}
+
 /*
  * Create pg_locale_t from collation oid in TopMemoryContext.
  *
  * For simplicity, we always generate COLLATE + CTYPE even though we
  * might only need one of them.  Since this is called only once per session,
  * it shouldn't cost much.
+ *
+ * Ensure that no path leaks a collator. That is, create the collator after
+ * any error paths, such as memory allocation.
  */
 static pg_locale_t
 create_pg_locale(Oid collid)
@@ -1535,7 +1607,6 @@ create_pg_locale(Oid collid)
 	struct pg_locale_struct result;
 	pg_locale_t resultp;
 	Datum		datum;
-	bool		isnull;
 
 	tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collid));
 	if (!HeapTupleIsValid(tp))
@@ -1584,6 +1655,7 @@ create_pg_locale(Oid collid)
 #ifdef USE_ICU
 		const char *iculocstr;
 		const char *icurules;
+		bool		isnull;
 
 		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
 		iculocstr = TextDatumGetCString(datum);
@@ -1607,48 +1679,6 @@ create_pg_locale(Oid collid)
 #endif							/* not USE_ICU */
 	}
 
-	datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion,
-							&isnull);
-	if (!isnull)
-	{
-		char	   *actual_versionstr;
-		char	   *collversionstr;
-
-		collversionstr = TextDatumGetCString(datum);
-
-		if (collform->collprovider == COLLPROVIDER_LIBC)
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
-		else
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
-
-		actual_versionstr = get_collation_actual_version(collform->collprovider,
-														 TextDatumGetCString(datum));
-		if (!actual_versionstr)
-		{
-			/*
-			 * This could happen when specifying a version in CREATE COLLATION
-			 * but the provider does not support versioning, or manually
-			 * creating a mess in the catalogs.
-			 */
-			ereport(ERROR,
-					(errmsg("collation \"%s\" has no actual version, but a version was recorded",
-							NameStr(collform->collname))));
-		}
-
-		if (strcmp(actual_versionstr, collversionstr) != 0)
-			ereport(WARNING,
-					(errmsg("collation \"%s\" has version mismatch",
-							NameStr(collform->collname)),
-					 errdetail("The collation in the database was created using version %s, "
-							   "but the operating system provides version %s.",
-							   collversionstr, actual_versionstr),
-					 errhint("Rebuild all objects affected by this collation and run "
-							 "ALTER COLLATION %s REFRESH VERSION, "
-							 "or build PostgreSQL with the right library version.",
-							 quote_qualified_identifier(get_namespace_name(collform->collnamespace),
-														NameStr(collform->collname)))));
-	}
-
 	ReleaseSysCache(tp);
 
 	/* We'll keep the pg_locale_t structures in TopMemoryContext */
@@ -1693,7 +1723,13 @@ pg_newlocale_from_collation(Oid collid)
 
 	cache_entry = collation_cache_insert(CollationCache, collid, &found);
 	if (!found || !cache_entry->locale)
-		cache_entry->locale = create_pg_locale(collid);
+	{
+		pg_locale_t locale = create_pg_locale(collid);
+
+		check_collation_version(collid);
+
+		cache_entry->locale = locale;
+	}
 
 	return cache_entry->locale;
 }
-- 
2.34.1

v2-0004-For-collation-cache-use-CollationCacheContext-for.patchtext/x-patch; charset=UTF-8; name=v2-0004-For-collation-cache-use-CollationCacheContext-for.patchDownload
From bc196e948a06b07369fe1623f03f1f549fc48a2c Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 7 Aug 2024 11:31:30 -0700
Subject: [PATCH v2 4/6] For collation cache, use CollationCacheContext for all
 memory.

Commit 005c6b833f introduced CollationCacheContext for the hash table,
but the pg_locale_t objects were still allocated in
TopMemoryContext. Change to use CollationCacheContext.

This change required a bit of refactoring to make the caller of
init_pg_locale() responsible for copying to the right context, so that
init_database_collation() can still use TopMemoryContext.
---
 src/backend/utils/adt/pg_locale.c | 49 +++++++++++++------------------
 1 file changed, 21 insertions(+), 28 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index db9d1c01947..c52d6989802 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1589,23 +1589,22 @@ check_collation_version(Oid collid)
 }
 
 /*
- * Create pg_locale_t from collation oid in TopMemoryContext.
+ * Create pg_locale_t from collation oid.
  *
- * For simplicity, we always generate COLLATE + CTYPE even though we
- * might only need one of them.  Since this is called only once per session,
- * it shouldn't cost much.
+ * For simplicity, we always generate COLLATE + CTYPE even though we might
+ * only need one of them.  Since this is called only once per session, it
+ * shouldn't cost much.
  *
  * Ensure that no path leaks a collator. That is, create the collator after
  * any error paths, such as memory allocation.
  */
 static pg_locale_t
-create_pg_locale(Oid collid)
+create_pg_locale(Oid collid, MemoryContext context)
 {
 	/* We haven't computed this yet in this session, so do it */
 	HeapTuple	tp;
 	Form_pg_collation collform;
-	struct pg_locale_struct result;
-	pg_locale_t resultp;
+	pg_locale_t result;
 	Datum		datum;
 
 	tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collid));
@@ -1613,10 +1612,9 @@ create_pg_locale(Oid collid)
 		elog(ERROR, "cache lookup failed for collation %u", collid);
 	collform = (Form_pg_collation) GETSTRUCT(tp);
 
-	/* We'll fill in the result struct locally before allocating memory */
-	memset(&result, 0, sizeof(result));
-	result.provider = collform->collprovider;
-	result.deterministic = collform->collisdeterministic;
+	result = MemoryContextAllocZero(context, sizeof(struct pg_locale_struct));
+	result->provider = collform->collprovider;
+	result->deterministic = collform->collisdeterministic;
 
 	if (collform->collprovider == COLLPROVIDER_BUILTIN)
 	{
@@ -1625,13 +1623,12 @@ create_pg_locale(Oid collid)
 		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
 		locstr = TextDatumGetCString(datum);
 
-		result.collate_is_c = true;
-		result.ctype_is_c = (strcmp(locstr, "C") == 0);
+		result->collate_is_c = true;
+		result->ctype_is_c = (strcmp(locstr, "C") == 0);
 
 		builtin_validate_locale(GetDatabaseEncoding(), locstr);
 
-		result.info.builtin.locale = MemoryContextStrdup(TopMemoryContext,
-														 locstr);
+		result->info.builtin.locale = MemoryContextStrdup(context, locstr);
 	}
 	else if (collform->collprovider == COLLPROVIDER_LIBC)
 	{
@@ -1643,12 +1640,12 @@ create_pg_locale(Oid collid)
 		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collctype);
 		collctype = TextDatumGetCString(datum);
 
-		result.collate_is_c = (strcmp(collcollate, "C") == 0) ||
+		result->collate_is_c = (strcmp(collcollate, "C") == 0) ||
 			(strcmp(collcollate, "POSIX") == 0);
-		result.ctype_is_c = (strcmp(collctype, "C") == 0) ||
+		result->ctype_is_c = (strcmp(collctype, "C") == 0) ||
 			(strcmp(collctype, "POSIX") == 0);
 
-		result.info.lt = make_libc_collator(collcollate, collctype);
+		result->info.lt = make_libc_collator(collcollate, collctype);
 	}
 	else if (collform->collprovider == COLLPROVIDER_ICU)
 	{
@@ -1660,8 +1657,8 @@ create_pg_locale(Oid collid)
 		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
 		iculocstr = TextDatumGetCString(datum);
 
-		result.collate_is_c = false;
-		result.ctype_is_c = false;
+		result->collate_is_c = false;
+		result->ctype_is_c = false;
 
 		datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collicurules, &isnull);
 		if (!isnull)
@@ -1669,8 +1666,8 @@ create_pg_locale(Oid collid)
 		else
 			icurules = NULL;
 
-		result.info.icu.ucol = make_icu_collator(iculocstr, icurules);
-		result.info.icu.locale = MemoryContextStrdup(TopMemoryContext, iculocstr);
+		result->info.icu.locale = MemoryContextStrdup(context, iculocstr);
+		result->info.icu.ucol = make_icu_collator(iculocstr, icurules);
 #else							/* not USE_ICU */
 		/* could get here if a collation was created by a build with ICU */
 		ereport(ERROR,
@@ -1681,11 +1678,7 @@ create_pg_locale(Oid collid)
 
 	ReleaseSysCache(tp);
 
-	/* We'll keep the pg_locale_t structures in TopMemoryContext */
-	resultp = MemoryContextAlloc(TopMemoryContext, sizeof(*resultp));
-	*resultp = result;
-
-	return resultp;
+	return result;
 }
 
 /*
@@ -1724,7 +1717,7 @@ pg_newlocale_from_collation(Oid collid)
 	cache_entry = collation_cache_insert(CollationCache, collid, &found);
 	if (!found || !cache_entry->locale)
 	{
-		pg_locale_t locale = create_pg_locale(collid);
+		pg_locale_t locale = create_pg_locale(collid, CollationCacheContext);
 
 		check_collation_version(collid);
 
-- 
2.34.1

v2-0005-Use-resource-owners-to-track-locale_t-and-ICU-col.patchtext/x-patch; charset=UTF-8; name=v2-0005-Use-resource-owners-to-track-locale_t-and-ICU-col.patchDownload
From 994d970c648b3ab13bd9120e03a97823628a5255 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 7 Aug 2024 12:25:37 -0700
Subject: [PATCH v2 5/6] Use resource owners to track locale_t and ICU collator
 objects.

Rather than rely on careful ordering of operations in error paths,
track collator objects with CurrentResourceOwner soon after they are
allocated. Then, move them to the new CollationCacheOwner when the
pg_locale_t is successfully allocated, right after copying the memory
to CollationCacheContext.
---
 src/backend/utils/adt/pg_locale.c | 72 ++++++++++++++++++++++++++++++-
 src/include/utils/pg_locale.h     |  4 +-
 2 files changed, 73 insertions(+), 3 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index c52d6989802..47f902b8f59 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -66,6 +66,7 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
+#include "utils/resowner.h"
 #include "utils/syscache.h"
 
 #ifdef USE_ICU
@@ -151,6 +152,12 @@ typedef struct
 static MemoryContext CollationCacheContext = NULL;
 static collation_cache_hash *CollationCache = NULL;
 
+/*
+ * Collator objects (UCollator for ICU or locale_t for libc) are allocated in
+ * an external library, so track them using a resource owner.
+ */
+static ResourceOwner CollationCacheOwner = NULL;
+
 #if defined(WIN32) && defined(LC_MESSAGES)
 static char *IsoLocaleName(const char *);
 #endif
@@ -174,6 +181,24 @@ static void icu_set_collation_attributes(UCollator *collator, const char *loc,
 										 UErrorCode *status);
 #endif
 
+static void ResOwnerReleasePGLocale(Datum val);
+static void pg_freelocale(pg_locale_t val);
+
+static const ResourceOwnerDesc PGLocaleResourceKind =
+{
+	.name = "pg_locale_t reference",
+	.release_phase = RESOURCE_RELEASE_AFTER_LOCKS,
+	.release_priority = RELEASE_PRIO_LAST,
+	.ReleaseResource = ResOwnerReleasePGLocale,
+	.DebugPrint = NULL			/* the default message is fine */
+};
+
+static void
+ResOwnerReleasePGLocale(Datum val)
+{
+	pg_freelocale((pg_locale_t) DatumGetPointer(val));
+}
+
 /*
  * POSIX doesn't define _l-variants of these functions, but several systems
  * have them.  We provide our own replacements here.
@@ -1707,6 +1732,7 @@ pg_newlocale_from_collation(Oid collid)
 	 */
 	if (CollationCache == NULL)
 	{
+		CollationCacheOwner = ResourceOwnerCreate(NULL, "collation cache");
 		CollationCacheContext = AllocSetContextCreate(TopMemoryContext,
 													  "collation cache",
 													  ALLOCSET_DEFAULT_SIZES);
@@ -1717,16 +1743,60 @@ pg_newlocale_from_collation(Oid collid)
 	cache_entry = collation_cache_insert(CollationCache, collid, &found);
 	if (!found || !cache_entry->locale)
 	{
-		pg_locale_t locale = create_pg_locale(collid, CollationCacheContext);
+		pg_locale_t locale;
+
+		ResourceOwnerEnlarge(CollationCacheOwner);
+
+		locale = create_pg_locale(collid, CollationCacheContext);
+		ResourceOwnerRemember(CurrentResourceOwner,
+							  PointerGetDatum(locale),
+							  &PGLocaleResourceKind);
 
 		check_collation_version(collid);
 
+		/* reassign locale from CurrentResourceOwner to CollationCacheOwner */
+		ResourceOwnerForget(CurrentResourceOwner,
+							PointerGetDatum(locale),
+							&PGLocaleResourceKind);
+		ResourceOwnerRemember(CollationCacheOwner,
+							  PointerGetDatum(locale),
+							  &PGLocaleResourceKind);
+
 		cache_entry->locale = locale;
 	}
 
 	return cache_entry->locale;
 }
 
+static void
+pg_freelocale(pg_locale_t locale)
+{
+	if (locale->provider == COLLPROVIDER_BUILTIN)
+	{
+		pfree(locale->info.builtin.locale);
+	}
+#ifdef USE_ICU
+	else if (locale->provider == COLLPROVIDER_ICU)
+	{
+		ucol_close(locale->info.icu.ucol);
+		pfree(locale->info.icu.locale);
+	}
+#endif
+	else if (locale->provider == COLLPROVIDER_LIBC)
+	{
+		if (locale->info.lt != NULL)
+		{
+#ifndef WIN32
+			freelocale(locale->info.lt);
+#else
+			_free_locale(locale->info.lt);
+#endif
+		}
+	}
+
+	pfree(locale);
+}
+
 /*
  * Get provider-specific collation version string for the given collation from
  * the operating system/library.
diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h
index 1ca0b7fa74b..57096fe484f 100644
--- a/src/include/utils/pg_locale.h
+++ b/src/include/utils/pg_locale.h
@@ -92,13 +92,13 @@ struct pg_locale_struct
 	{
 		struct
 		{
-			const char *locale;
+			char	   *locale;
 		}			builtin;
 		locale_t	lt;
 #ifdef USE_ICU
 		struct
 		{
-			const char *locale;
+			char	   *locale;
 			UCollator  *ucol;
 		}			icu;
 #endif
-- 
2.34.1

v2-0006-Invalidate-collation-cache-when-appropriate.patchtext/x-patch; charset=UTF-8; name=v2-0006-Invalidate-collation-cache-when-appropriate.patchDownload
From 9bba83e8c9e19e442c0e8ad2f90a4fd1d14fe4f1 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 7 Aug 2024 13:03:32 -0700
Subject: [PATCH v2 6/6] Invalidate collation cache when appropriate.

Previously, DROP COLLATION could leave a cache entry around. That's
not normally a problem, but can be if oid wraparound causes the same
oid to be reused for a different collation.
---
 src/backend/utils/adt/pg_locale.c | 25 +++++++++++++++++--------
 1 file changed, 17 insertions(+), 8 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 47f902b8f59..f5f45f72a13 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -63,6 +63,7 @@
 #include "utils/builtins.h"
 #include "utils/formatting.h"
 #include "utils/guc_hooks.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
@@ -1453,6 +1454,18 @@ pg_locale_deterministic(pg_locale_t locale)
 	return locale->deterministic;
 }
 
+static void
+CollationCacheInvalidate(Datum arg, int cacheid, uint32 hashvalue)
+{
+	ResourceOwnerReleaseAllOfKind(CollationCacheOwner,
+								  &PGLocaleResourceKind);
+
+	/* free all memory and reset hash table */
+	MemoryContextReset(CollationCacheContext);
+	CollationCache = collation_cache_create(CollationCacheContext,
+											16, NULL);
+}
+
 /*
  * Initialize default_locale with database locale settings.
  */
@@ -1722,14 +1735,7 @@ pg_newlocale_from_collation(Oid collid)
 	if (collid == DEFAULT_COLLATION_OID)
 		return &default_locale;
 
-	/*
-	 * Cache mechanism for collation information.
-	 *
-	 * Note that we currently lack any way to flush the cache.  Since we don't
-	 * support ALTER COLLATION, this is OK.  The worst case is that someone
-	 * drops a collation, and a useless cache entry hangs around in existing
-	 * backends.
-	 */
+	/* cache mechanism for collation information */
 	if (CollationCache == NULL)
 	{
 		CollationCacheOwner = ResourceOwnerCreate(NULL, "collation cache");
@@ -1738,6 +1744,9 @@ pg_newlocale_from_collation(Oid collid)
 													  ALLOCSET_DEFAULT_SIZES);
 		CollationCache = collation_cache_create(CollationCacheContext,
 												16, NULL);
+		CacheRegisterSyscacheCallback(COLLOID,
+									  CollationCacheInvalidate,
+									  (Datum) 0);
 	}
 
 	cache_entry = collation_cache_insert(CollationCache, collid, &found);
-- 
2.34.1

#3Jeff Davis
pgsql@j-davis.com
In reply to: Jeff Davis (#2)
5 attachment(s)
Re: [18] Fix a few issues with the collation cache

On Wed, 2024-08-14 at 16:30 -0700, Jeff Davis wrote:

On Thu, 2024-08-08 at 12:24 -0700, Jeff Davis wrote:

The collation cache, which maps collation oids to pg_locale_t
objects,
has a few longstanding issues:

Here's a patch set v2.

Updated and rebased.

Regards,
Jeff Davis

Attachments:

v4-0001-Tighten-up-make_libc_collator-and-make_icu_collat.patchtext/x-patch; charset=UTF-8; name=v4-0001-Tighten-up-make_libc_collator-and-make_icu_collat.patchDownload
From 224470bc4d0660dc11940f5595031eecb0319d62 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 7 Aug 2024 11:05:46 -0700
Subject: [PATCH v4 1/7] Tighten up make_libc_collator() and
 make_icu_collator().

Return the result rather than using an out parameter, and make it the
caller's responsibility to copy it into the right context. Ensure that
no paths leak a collator.

The function make_icu_collator() doesn't have any external callers, so
change it to be static. Also, when re-opening with rules, use a
try/finally block to avoid leaking the collator.

In make_libc_collator(), if the first newlocale() succeeds and the
second one fails, close the first locale_t object.

Discussion: https://postgr.es/m/54d20e812bd6c3e44c10eddcd757ec494ebf1803.camel@j-davis.com
---
 src/backend/utils/adt/pg_locale.c | 126 +++++++++++++++++++-----------
 src/include/utils/pg_locale.h     |   4 -
 2 files changed, 80 insertions(+), 50 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 5bef1b113a8..12ba5726f77 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1297,14 +1297,15 @@ report_newlocale_failure(const char *localename)
 }
 
 /*
- * Initialize the locale_t field.
+ * Create a locale_t with the given collation and ctype.
  *
- * The "C" and "POSIX" locales are not actually handled by libc, so set the
- * locale_t to zero in that case.
+ * The "C" and "POSIX" locales are not actually handled by libc, so return
+ * NULL.
+ *
+ * Ensure that no path leaks a locale_t.
  */
-static void
-make_libc_collator(const char *collate, const char *ctype,
-				   pg_locale_t result)
+static locale_t
+make_libc_collator(const char *collate, const char *ctype)
 {
 	locale_t	loc = 0;
 
@@ -1343,7 +1344,11 @@ make_libc_collator(const char *collate, const char *ctype,
 			errno = 0;
 			loc = newlocale(LC_CTYPE_MASK, ctype, loc1);
 			if (!loc)
+			{
+				if (loc1)
+					freelocale(loc1);
 				report_newlocale_failure(ctype);
+			}
 		}
 		else
 			loc = loc1;
@@ -1360,60 +1365,78 @@ make_libc_collator(const char *collate, const char *ctype,
 #endif
 	}
 
-	result->info.lt = loc;
+	return loc;
 }
 
-void
-make_icu_collator(const char *iculocstr,
-				  const char *icurules,
-				  struct pg_locale_struct *resultp)
-{
+/*
+ * Create a UCollator with the given locale string and rules.
+ *
+ * Ensure that no path leaks a UCollator.
+ */
 #ifdef USE_ICU
-	UCollator  *collator;
-
-	collator = pg_ucol_open(iculocstr);
-
-	/*
-	 * If rules are specified, we extract the rules of the standard collation,
-	 * add our own rules, and make a new collator with the combined rules.
-	 */
-	if (icurules)
+static UCollator *
+make_icu_collator(const char *iculocstr, const char *icurules)
+{
+	if (!icurules)
 	{
-		const UChar *default_rules;
-		UChar	   *agg_rules;
+		/* simple case without rules */
+		return pg_ucol_open(iculocstr);
+	}
+	else
+	{
+		UCollator  *collator_std_rules;
+		UCollator  *collator_all_rules;
+		const UChar *std_rules;
 		UChar	   *my_rules;
-		UErrorCode	status;
+		UChar	   *all_rules;
 		int32_t		length;
+		int32_t		total;
+		UErrorCode	status;
 
-		default_rules = ucol_getRules(collator, &length);
+		/*
+		 * If rules are specified, we extract the rules of the standard
+		 * collation, add our own rules, and make a new collator with the
+		 * combined rules.
+		 */
 		icu_to_uchar(&my_rules, icurules, strlen(icurules));
 
-		agg_rules = palloc_array(UChar, u_strlen(default_rules) + u_strlen(my_rules) + 1);
-		u_strcpy(agg_rules, default_rules);
-		u_strcat(agg_rules, my_rules);
+		collator_std_rules = pg_ucol_open(iculocstr);
 
-		ucol_close(collator);
+		std_rules = ucol_getRules(collator_std_rules, &length);
+
+		total = u_strlen(std_rules) + u_strlen(my_rules) + 1;
+
+		/* avoid leaking collator on OOM */
+		all_rules = palloc_extended(sizeof(UChar) * total, MCXT_ALLOC_NO_OOM);
+		if (!all_rules)
+		{
+			ucol_close(collator_std_rules);
+			ereport(ERROR,
+					(errcode(ERRCODE_OUT_OF_MEMORY),
+					 errmsg("out of memory")));
+		}
+
+		u_strcpy(all_rules, std_rules);
+		u_strcat(all_rules, my_rules);
+
+		ucol_close(collator_std_rules);
 
 		status = U_ZERO_ERROR;
-		collator = ucol_openRules(agg_rules, u_strlen(agg_rules),
-								  UCOL_DEFAULT, UCOL_DEFAULT_STRENGTH, NULL, &status);
+		collator_all_rules = ucol_openRules(all_rules, u_strlen(all_rules),
+											UCOL_DEFAULT, UCOL_DEFAULT_STRENGTH,
+											NULL, &status);
 		if (U_FAILURE(status))
+		{
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("could not open collator for locale \"%s\" with rules \"%s\": %s",
 							iculocstr, icurules, u_errorName(status))));
-	}
+		}
 
-	/* We will leak this string if the caller errors later :-( */
-	resultp->info.icu.locale = MemoryContextStrdup(TopMemoryContext, iculocstr);
-	resultp->info.icu.ucol = collator;
-#else							/* not USE_ICU */
-	/* could get here if a collation was created by a build with ICU */
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("ICU is not supported in this build")));
-#endif							/* not USE_ICU */
+		return collator_all_rules;
+	}
 }
+#endif							/* not USE_ICU */
 
 /*
  * Initialize default_locale with database locale settings.
@@ -1424,7 +1447,6 @@ init_database_collation(void)
 	HeapTuple	tup;
 	Form_pg_database dbform;
 	Datum		datum;
-	bool		isnull;
 
 	/* Fetch our pg_database row normally, via syscache */
 	tup = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
@@ -1449,8 +1471,10 @@ init_database_collation(void)
 	}
 	else if (dbform->datlocprovider == COLLPROVIDER_ICU)
 	{
+#ifdef USE_ICU
 		char	   *datlocale;
 		char	   *icurules;
+		bool		isnull;
 
 		datum = SysCacheGetAttrNotNull(DATABASEOID, tup, Anum_pg_database_datlocale);
 		datlocale = TextDatumGetCString(datum);
@@ -1464,7 +1488,14 @@ init_database_collation(void)
 		else
 			icurules = NULL;
 
-		make_icu_collator(datlocale, icurules, &default_locale);
+		default_locale.info.icu.locale = MemoryContextStrdup(TopMemoryContext, datlocale);
+		default_locale.info.icu.ucol = make_icu_collator(datlocale, icurules);
+#else							/* not USE_ICU */
+		/* could get here if a collation was created by a build with ICU */
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("ICU is not supported in this build")));
+#endif							/* not USE_ICU */
 	}
 	else
 	{
@@ -1483,7 +1514,7 @@ init_database_collation(void)
 		default_locale.ctype_is_c = (strcmp(datctype, "C") == 0) ||
 			(strcmp(datctype, "POSIX") == 0);
 
-		make_libc_collator(datcollate, datctype, &default_locale);
+		default_locale.info.lt = make_libc_collator(datcollate, datctype);
 	}
 
 	default_locale.provider = dbform->datlocprovider;
@@ -1572,7 +1603,7 @@ pg_newlocale_from_collation(Oid collid)
 			result.ctype_is_c = (strcmp(collctype, "C") == 0) ||
 				(strcmp(collctype, "POSIX") == 0);
 
-			make_libc_collator(collcollate, collctype, &result);
+			result.info.lt = make_libc_collator(collcollate, collctype);
 		}
 		else if (collform->collprovider == COLLPROVIDER_ICU)
 		{
@@ -1591,7 +1622,8 @@ pg_newlocale_from_collation(Oid collid)
 			else
 				icurules = NULL;
 
-			make_icu_collator(iculocstr, icurules, &result);
+			result.info.icu.locale = MemoryContextStrdup(TopMemoryContext, iculocstr);
+			result.info.icu.ucol = make_icu_collator(iculocstr, icurules);
 		}
 
 		datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion,
@@ -2500,6 +2532,8 @@ builtin_validate_locale(int encoding, const char *locale)
 /*
  * Wrapper around ucol_open() to handle API differences for older ICU
  * versions.
+ *
+ * Ensure that no path leaks a UCollator.
  */
 static UCollator *
 pg_ucol_open(const char *loc_str)
diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h
index faae868bfcc..c2d95411e0a 100644
--- a/src/include/utils/pg_locale.h
+++ b/src/include/utils/pg_locale.h
@@ -104,10 +104,6 @@ struct pg_locale_struct
 
 typedef struct pg_locale_struct *pg_locale_t;
 
-extern void make_icu_collator(const char *iculocstr,
-							  const char *icurules,
-							  struct pg_locale_struct *resultp);
-
 extern void init_database_collation(void);
 extern pg_locale_t pg_newlocale_from_collation(Oid collid);
 
-- 
2.34.1

v4-0002-create_pg_locale.patchtext/x-patch; charset=UTF-8; name=v4-0002-create_pg_locale.patchDownload
From eccc4a4a83069c6a14465b4a9239a4d759aaa2a8 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 18 Sep 2024 15:53:56 -0700
Subject: [PATCH v4 2/7] create_pg_locale

---
 src/backend/utils/adt/pg_locale.c | 310 +++++++++++++++---------------
 1 file changed, 155 insertions(+), 155 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 12ba5726f77..1dec00b55ed 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1227,45 +1227,6 @@ IsoLocaleName(const char *winlocname)
 #endif							/* WIN32 && LC_MESSAGES */
 
 
-/*
- * Cache mechanism for collation information.
- *
- * Note that we currently lack any way to flush the cache.  Since we don't
- * support ALTER COLLATION, this is OK.  The worst case is that someone
- * drops a collation, and a useless cache entry hangs around in existing
- * backends.
- */
-static collation_cache_entry *
-lookup_collation_cache(Oid collation)
-{
-	collation_cache_entry *cache_entry;
-	bool		found;
-
-	Assert(OidIsValid(collation));
-	Assert(collation != DEFAULT_COLLATION_OID);
-
-	if (CollationCache == NULL)
-	{
-		CollationCacheContext = AllocSetContextCreate(TopMemoryContext,
-													  "collation cache",
-													  ALLOCSET_DEFAULT_SIZES);
-		CollationCache = collation_cache_create(CollationCacheContext,
-												16, NULL);
-	}
-
-	cache_entry = collation_cache_insert(CollationCache, collation, &found);
-	if (!found)
-	{
-		/*
-		 * Make sure cache entry is marked invalid, in case we fail before
-		 * setting things.
-		 */
-		cache_entry->locale = 0;
-	}
-
-	return cache_entry;
-}
-
 /* simple subroutine for reporting errors from newlocale() */
 static void
 report_newlocale_failure(const char *localename)
@@ -1530,153 +1491,192 @@ init_database_collation(void)
 }
 
 /*
- * Create a pg_locale_t from a collation OID.  Results are cached for the
- * lifetime of the backend.  Thus, do not free the result with freelocale().
- *
- * For simplicity, we always generate COLLATE + CTYPE even though we
- * might only need one of them.  Since this is called only once per session,
- * it shouldn't cost much.
+ * Create and initialize a pg_locale_t.  Be careful to check for errors before
+ * allocating memory.
  */
-pg_locale_t
-pg_newlocale_from_collation(Oid collid)
+static pg_locale_t
+create_pg_locale(Oid collid)
 {
-	collation_cache_entry *cache_entry;
-
-	if (collid == DEFAULT_COLLATION_OID)
-		return &default_locale;
+	/* We haven't computed this yet in this session, so do it */
+	HeapTuple	tp;
+	Form_pg_collation collform;
+	pg_locale_t result;
+	Datum		datum;
+	bool		isnull;
 
-	if (!OidIsValid(collid))
+	tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collid));
+	if (!HeapTupleIsValid(tp))
 		elog(ERROR, "cache lookup failed for collation %u", collid);
+	collform = (Form_pg_collation) GETSTRUCT(tp);
 
-	if (last_collation_cache_oid == collid)
-		return last_collation_cache_locale;
-
-	cache_entry = lookup_collation_cache(collid);
-
-	if (cache_entry->locale == 0)
+	/* compare version in catalog to version from provider */
+	datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion,
+							&isnull);
+	if (!isnull)
 	{
-		/* We haven't computed this yet in this session, so do it */
-		HeapTuple	tp;
-		Form_pg_collation collform;
-		struct pg_locale_struct result;
-		pg_locale_t resultp;
-		Datum		datum;
-		bool		isnull;
+		char	   *actual_versionstr;
+		char	   *collversionstr;
 
-		tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collid));
-		if (!HeapTupleIsValid(tp))
-			elog(ERROR, "cache lookup failed for collation %u", collid);
-		collform = (Form_pg_collation) GETSTRUCT(tp);
+		collversionstr = TextDatumGetCString(datum);
 
-		/* We'll fill in the result struct locally before allocating memory */
-		memset(&result, 0, sizeof(result));
-		result.provider = collform->collprovider;
-		result.deterministic = collform->collisdeterministic;
+		if (collform->collprovider == COLLPROVIDER_LIBC)
+			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
+		else
+			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
 
-		if (collform->collprovider == COLLPROVIDER_BUILTIN)
+		actual_versionstr = get_collation_actual_version(collform->collprovider,
+														 TextDatumGetCString(datum));
+		if (!actual_versionstr)
 		{
-			const char *locstr;
+			/*
+			 * This could happen when specifying a version in CREATE
+			 * COLLATION but the provider does not support versioning, or
+			 * manually creating a mess in the catalogs.
+			 */
+			ereport(ERROR,
+					(errmsg("collation \"%s\" has no actual version, but a version was recorded",
+							NameStr(collform->collname))));
+		}
 
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
-			locstr = TextDatumGetCString(datum);
+		if (strcmp(actual_versionstr, collversionstr) != 0)
+			ereport(WARNING,
+					(errmsg("collation \"%s\" has version mismatch",
+							NameStr(collform->collname)),
+					 errdetail("The collation in the database was created using version %s, "
+							   "but the operating system provides version %s.",
+							   collversionstr, actual_versionstr),
+					 errhint("Rebuild all objects affected by this collation and run "
+							 "ALTER COLLATION %s REFRESH VERSION, "
+							 "or build PostgreSQL with the right library version.",
+							 quote_qualified_identifier(get_namespace_name(collform->collnamespace),
+														NameStr(collform->collname)))));
+	}
 
-			result.collate_is_c = true;
-			result.ctype_is_c = (strcmp(locstr, "C") == 0);
+	if (collform->collprovider == COLLPROVIDER_BUILTIN)
+	{
+		const char *locstr;
 
-			builtin_validate_locale(GetDatabaseEncoding(), locstr);
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
+		locstr = TextDatumGetCString(datum);
 
-			result.info.builtin.locale = MemoryContextStrdup(TopMemoryContext,
-															 locstr);
-		}
-		else if (collform->collprovider == COLLPROVIDER_LIBC)
-		{
-			const char *collcollate;
-			const char *collctype;
+		builtin_validate_locale(GetDatabaseEncoding(), locstr);
 
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
-			collcollate = TextDatumGetCString(datum);
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collctype);
-			collctype = TextDatumGetCString(datum);
+		result = MemoryContextAllocZero(TopMemoryContext,
+										sizeof(struct pg_locale_struct));
 
-			result.collate_is_c = (strcmp(collcollate, "C") == 0) ||
-				(strcmp(collcollate, "POSIX") == 0);
-			result.ctype_is_c = (strcmp(collctype, "C") == 0) ||
-				(strcmp(collctype, "POSIX") == 0);
+		result->provider = collform->collprovider;
+		result->deterministic = collform->collisdeterministic;
+		result->collate_is_c = true;
+		result->ctype_is_c = (strcmp(locstr, "C") == 0);
+		result->info.builtin.locale = MemoryContextStrdup(TopMemoryContext,
+														 locstr);
+	}
+	else if (collform->collprovider == COLLPROVIDER_LIBC)
+	{
+		const char *collcollate;
+		const char *collctype;
+		locale_t	locale;
+
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
+		collcollate = TextDatumGetCString(datum);
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collctype);
+		collctype = TextDatumGetCString(datum);
+
+		locale = make_libc_collator(collcollate, collctype);
+
+		result = MemoryContextAllocZero(TopMemoryContext,
+										sizeof(struct pg_locale_struct));
+
+		result->provider = collform->collprovider;
+		result->deterministic = collform->collisdeterministic;
+		result->collate_is_c = (strcmp(collcollate, "C") == 0) ||
+			(strcmp(collcollate, "POSIX") == 0);
+		result->ctype_is_c = (strcmp(collctype, "C") == 0) ||
+			(strcmp(collctype, "POSIX") == 0);
+		result->info.lt = locale;
+	}
+	else if (collform->collprovider == COLLPROVIDER_ICU)
+	{
+		const char *iculocstr;
+		const char *icurules;
+		UCollator  *collator;
 
-			result.info.lt = make_libc_collator(collcollate, collctype);
-		}
-		else if (collform->collprovider == COLLPROVIDER_ICU)
-		{
-			const char *iculocstr;
-			const char *icurules;
+		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
+		iculocstr = TextDatumGetCString(datum);
 
-			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
-			iculocstr = TextDatumGetCString(datum);
+		datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collicurules, &isnull);
+		if (!isnull)
+			icurules = TextDatumGetCString(datum);
+		else
+			icurules = NULL;
 
-			result.collate_is_c = false;
-			result.ctype_is_c = false;
+		collator = make_icu_collator(iculocstr, icurules);
 
-			datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collicurules, &isnull);
-			if (!isnull)
-				icurules = TextDatumGetCString(datum);
-			else
-				icurules = NULL;
+		result = MemoryContextAllocZero(TopMemoryContext,
+										sizeof(struct pg_locale_struct));
 
-			result.info.icu.locale = MemoryContextStrdup(TopMemoryContext, iculocstr);
-			result.info.icu.ucol = make_icu_collator(iculocstr, icurules);
-		}
+		result->provider = collform->collprovider;
+		result->deterministic = collform->collisdeterministic;
+		result->collate_is_c = false;
+		result->ctype_is_c = false;
+		result->info.icu.locale = MemoryContextStrdup(TopMemoryContext, iculocstr);
+		result->info.icu.ucol = collator;
+	}
 
-		datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion,
-								&isnull);
-		if (!isnull)
-		{
-			char	   *actual_versionstr;
-			char	   *collversionstr;
+	ReleaseSysCache(tp);
 
-			collversionstr = TextDatumGetCString(datum);
+	return result;
+}
 
-			if (collform->collprovider == COLLPROVIDER_LIBC)
-				datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collcollate);
-			else
-				datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_colllocale);
+/*
+ * Create or retrieve a pg_locale_t for the given collation OID.  Results are
+ * cached for the lifetime of the backend.
+ */
+pg_locale_t
+pg_newlocale_from_collation(Oid collid)
+{
+	collation_cache_entry	*cache_entry;
+	bool					 found;
 
-			actual_versionstr = get_collation_actual_version(collform->collprovider,
-															 TextDatumGetCString(datum));
-			if (!actual_versionstr)
-			{
-				/*
-				 * This could happen when specifying a version in CREATE
-				 * COLLATION but the provider does not support versioning, or
-				 * manually creating a mess in the catalogs.
-				 */
-				ereport(ERROR,
-						(errmsg("collation \"%s\" has no actual version, but a version was recorded",
-								NameStr(collform->collname))));
-			}
+	if (collid == DEFAULT_COLLATION_OID)
+		return &default_locale;
 
-			if (strcmp(actual_versionstr, collversionstr) != 0)
-				ereport(WARNING,
-						(errmsg("collation \"%s\" has version mismatch",
-								NameStr(collform->collname)),
-						 errdetail("The collation in the database was created using version %s, "
-								   "but the operating system provides version %s.",
-								   collversionstr, actual_versionstr),
-						 errhint("Rebuild all objects affected by this collation and run "
-								 "ALTER COLLATION %s REFRESH VERSION, "
-								 "or build PostgreSQL with the right library version.",
-								 quote_qualified_identifier(get_namespace_name(collform->collnamespace),
-															NameStr(collform->collname)))));
-		}
+	if (!OidIsValid(collid))
+		elog(ERROR, "cache lookup failed for collation %u", collid);
 
-		ReleaseSysCache(tp);
+	if (last_collation_cache_oid == collid)
+		return last_collation_cache_locale;
 
-		/* We'll keep the pg_locale_t structures in TopMemoryContext */
-		resultp = MemoryContextAlloc(TopMemoryContext, sizeof(*resultp));
-		*resultp = result;
+	/*
+	 * Cache mechanism for collation information.
+	 *
+	 * Note that we currently lack any way to flush the cache.  Since we don't
+	 * support ALTER COLLATION, this is OK.  The worst case is that someone
+	 * drops a collation, and a useless cache entry hangs around in existing
+	 * backends.
+	 */
+	if (CollationCache == NULL)
+	{
+		CollationCacheContext = AllocSetContextCreate(TopMemoryContext,
+													  "collation cache",
+													  ALLOCSET_DEFAULT_SIZES);
+		CollationCache = collation_cache_create(CollationCacheContext,
+												16, NULL);
+	}
 
-		cache_entry->locale = resultp;
+	cache_entry = collation_cache_insert(CollationCache, collid, &found);
+	if (!found)
+	{
+		/*
+		 * Make sure cache entry is marked invalid, in case we fail before
+		 * setting things.
+		 */
+		cache_entry->locale = 0;
 	}
 
+	if (cache_entry->locale == 0)
+		cache_entry->locale = create_pg_locale(collid);
+
 	last_collation_cache_oid = collid;
 	last_collation_cache_locale = cache_entry->locale;
 
-- 
2.34.1

v4-0003-CollationCacheContext.patchtext/x-patch; charset=UTF-8; name=v4-0003-CollationCacheContext.patchDownload
From fca0efa184971f9780b356039aa3ed08a7445524 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 18 Sep 2024 15:55:37 -0700
Subject: [PATCH v4 3/7] CollationCacheContext

---
 src/backend/utils/adt/pg_locale.c | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 1dec00b55ed..d3d9c3920e6 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1495,7 +1495,7 @@ init_database_collation(void)
  * allocating memory.
  */
 static pg_locale_t
-create_pg_locale(Oid collid)
+create_pg_locale(MemoryContext context, Oid collid)
 {
 	/* We haven't computed this yet in this session, so do it */
 	HeapTuple	tp;
@@ -1561,15 +1561,15 @@ create_pg_locale(Oid collid)
 
 		builtin_validate_locale(GetDatabaseEncoding(), locstr);
 
-		result = MemoryContextAllocZero(TopMemoryContext,
+		result = MemoryContextAllocZero(context,
 										sizeof(struct pg_locale_struct));
 
 		result->provider = collform->collprovider;
 		result->deterministic = collform->collisdeterministic;
 		result->collate_is_c = true;
 		result->ctype_is_c = (strcmp(locstr, "C") == 0);
-		result->info.builtin.locale = MemoryContextStrdup(TopMemoryContext,
-														 locstr);
+		result->info.builtin.locale = MemoryContextStrdup(context,
+														  locstr);
 	}
 	else if (collform->collprovider == COLLPROVIDER_LIBC)
 	{
@@ -1584,7 +1584,7 @@ create_pg_locale(Oid collid)
 
 		locale = make_libc_collator(collcollate, collctype);
 
-		result = MemoryContextAllocZero(TopMemoryContext,
+		result = MemoryContextAllocZero(context,
 										sizeof(struct pg_locale_struct));
 
 		result->provider = collform->collprovider;
@@ -1612,14 +1612,14 @@ create_pg_locale(Oid collid)
 
 		collator = make_icu_collator(iculocstr, icurules);
 
-		result = MemoryContextAllocZero(TopMemoryContext,
+		result = MemoryContextAllocZero(context,
 										sizeof(struct pg_locale_struct));
 
 		result->provider = collform->collprovider;
 		result->deterministic = collform->collisdeterministic;
 		result->collate_is_c = false;
 		result->ctype_is_c = false;
-		result->info.icu.locale = MemoryContextStrdup(TopMemoryContext, iculocstr);
+		result->info.icu.locale = MemoryContextStrdup(context, iculocstr);
 		result->info.icu.ucol = collator;
 	}
 
@@ -1675,7 +1675,7 @@ pg_newlocale_from_collation(Oid collid)
 	}
 
 	if (cache_entry->locale == 0)
-		cache_entry->locale = create_pg_locale(collid);
+		cache_entry->locale = create_pg_locale(CollationCacheContext, collid);
 
 	last_collation_cache_oid = collid;
 	last_collation_cache_locale = cache_entry->locale;
-- 
2.34.1

v4-0004-resource-owners.patchtext/x-patch; charset=UTF-8; name=v4-0004-resource-owners.patchDownload
From 5ae3b1be6489617a1639141749c31d2f4419a676 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 18 Sep 2024 16:55:42 -0700
Subject: [PATCH v4 4/7] resource owners

---
 src/backend/utils/adt/pg_locale.c | 74 ++++++++++++++++++++++++++++++-
 1 file changed, 72 insertions(+), 2 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index d3d9c3920e6..9d1d71f1561 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -66,6 +66,7 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
+#include "utils/resowner.h"
 #include "utils/syscache.h"
 
 #ifdef USE_ICU
@@ -148,6 +149,12 @@ typedef struct
 #define SH_DEFINE
 #include "lib/simplehash.h"
 
+/*
+ * Collator objects (UCollator for ICU or locale_t for libc) are allocated in
+ * an external library, so track them using a resource owner.
+ */
+static ResourceOwner CollationCacheOwner = NULL;
+
 static MemoryContext CollationCacheContext = NULL;
 static collation_cache_hash *CollationCache = NULL;
 
@@ -179,8 +186,35 @@ static int32_t uchar_convert(UConverter *converter,
 							 const char *src, int32_t srclen);
 static void icu_set_collation_attributes(UCollator *collator, const char *loc,
 										 UErrorCode *status);
+
+static void ResourceOwnerRememberUCollator(ResourceOwner owner,
+										   UCollator *collator);
+static void ResOwnerReleaseUCollator(Datum val);
+
+static const ResourceOwnerDesc UCollatorResourceKind =
+{
+	.name = "UCollator reference",
+	.release_phase = RESOURCE_RELEASE_AFTER_LOCKS,
+	.release_priority = RELEASE_PRIO_LAST,
+	.ReleaseResource = ResOwnerReleaseUCollator,
+	.DebugPrint = NULL                      /* the default message is fine */
+};
 #endif
 
+static void ResourceOwnerRememberLocaleT(ResourceOwner owner,
+										 locale_t locale);
+static void ResOwnerReleaseLocaleT(Datum val);
+
+static const ResourceOwnerDesc LocaleTResourceKind =
+{
+	.name = "locale_t reference",
+	.release_phase = RESOURCE_RELEASE_AFTER_LOCKS,
+	.release_priority = RELEASE_PRIO_LAST,
+	.ReleaseResource = ResOwnerReleaseLocaleT,
+	.DebugPrint = NULL                      /* the default message is fine */
+};
+
+
 /*
  * POSIX doesn't define _l-variants of these functions, but several systems
  * have them.  We provide our own replacements here.
@@ -1257,6 +1291,20 @@ report_newlocale_failure(const char *localename)
 						localename) : 0)));
 }
 
+static void
+ResourceOwnerRememberLocaleT(ResourceOwner owner, locale_t locale)
+{
+	ResourceOwnerRemember(owner, PointerGetDatum(locale),
+						  &LocaleTResourceKind);
+}
+
+static void
+ResOwnerReleaseLocaleT(Datum val)
+{
+	locale_t locale = (locale_t) DatumGetPointer(val);
+	freelocale(locale);
+}
+
 /*
  * Create a locale_t with the given collation and ctype.
  *
@@ -1335,6 +1383,20 @@ make_libc_collator(const char *collate, const char *ctype)
  * Ensure that no path leaks a UCollator.
  */
 #ifdef USE_ICU
+static void
+ResourceOwnerRememberUCollator(ResourceOwner owner, UCollator *collator)
+{
+	ResourceOwnerRemember(owner, PointerGetDatum(collator),
+						  &UCollatorResourceKind);
+}
+
+static void
+ResOwnerReleaseUCollator(Datum val)
+{
+	UCollator *collator = (UCollator *) DatumGetPointer(val);
+	ucol_close(collator);
+}
+
 static UCollator *
 make_icu_collator(const char *iculocstr, const char *icurules)
 {
@@ -1495,7 +1557,7 @@ init_database_collation(void)
  * allocating memory.
  */
 static pg_locale_t
-create_pg_locale(MemoryContext context, Oid collid)
+create_pg_locale(MemoryContext context, ResourceOwner owner, Oid collid)
 {
 	/* We haven't computed this yet in this session, so do it */
 	HeapTuple	tp;
@@ -1582,7 +1644,10 @@ create_pg_locale(MemoryContext context, Oid collid)
 		datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collctype);
 		collctype = TextDatumGetCString(datum);
 
+		ResourceOwnerEnlarge(owner);
 		locale = make_libc_collator(collcollate, collctype);
+		if (locale)
+			ResourceOwnerRememberLocaleT(owner, locale);
 
 		result = MemoryContextAllocZero(context,
 										sizeof(struct pg_locale_struct));
@@ -1610,7 +1675,9 @@ create_pg_locale(MemoryContext context, Oid collid)
 		else
 			icurules = NULL;
 
+		ResourceOwnerEnlarge(owner);
 		collator = make_icu_collator(iculocstr, icurules);
+		ResourceOwnerRememberUCollator(owner, collator);
 
 		result = MemoryContextAllocZero(context,
 										sizeof(struct pg_locale_struct));
@@ -1657,6 +1724,7 @@ pg_newlocale_from_collation(Oid collid)
 	 */
 	if (CollationCache == NULL)
 	{
+		CollationCacheOwner = ResourceOwnerCreate(NULL, "collation cache");
 		CollationCacheContext = AllocSetContextCreate(TopMemoryContext,
 													  "collation cache",
 													  ALLOCSET_DEFAULT_SIZES);
@@ -1675,7 +1743,9 @@ pg_newlocale_from_collation(Oid collid)
 	}
 
 	if (cache_entry->locale == 0)
-		cache_entry->locale = create_pg_locale(CollationCacheContext, collid);
+		cache_entry->locale = create_pg_locale(CollationCacheContext,
+											   CollationCacheOwner,
+											   collid);
 
 	last_collation_cache_oid = collid;
 	last_collation_cache_locale = cache_entry->locale;
-- 
2.34.1

v4-0005-invalidation.patchtext/x-patch; charset=UTF-8; name=v4-0005-invalidation.patchDownload
From 2f51247615a36dc257b700c2832f3d4aa32fce64 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Wed, 18 Sep 2024 17:49:57 -0700
Subject: [PATCH v4 5/7] invalidation

---
 src/backend/utils/adt/pg_locale.c | 41 +++++++++++++++++++++++++------
 1 file changed, 33 insertions(+), 8 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 9d1d71f1561..c89ac3b9e01 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -63,6 +63,7 @@
 #include "utils/builtins.h"
 #include "utils/formatting.h"
 #include "utils/guc_hooks.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
@@ -1695,6 +1696,34 @@ create_pg_locale(MemoryContext context, ResourceOwner owner, Oid collid)
 	return result;
 }
 
+static void
+CollationCacheInvalidate(Datum arg, int cacheid, uint32 hashvalue)
+{
+	last_collation_cache_oid = InvalidOid;
+
+	if (CollationCache == NULL)
+		return;
+
+	ResourceOwnerRelease(CollationCacheOwner,
+						 RESOURCE_RELEASE_BEFORE_LOCKS,
+						 false, true);
+	ResourceOwnerRelease(CollationCacheOwner,
+						 RESOURCE_RELEASE_LOCKS,
+						 false, true);
+	ResourceOwnerRelease(CollationCacheOwner,
+						 RESOURCE_RELEASE_AFTER_LOCKS,
+						 false, true);
+	ResourceOwnerDelete(CollationCacheOwner);
+	CollationCacheOwner = ResourceOwnerCreate(NULL, "collation cache");
+
+	MemoryContextReset(CollationCacheContext);
+
+	/* free all memory and reset hash table */
+	CollationCache = collation_cache_create(CollationCacheContext,
+											16, NULL);
+}
+
+
 /*
  * Create or retrieve a pg_locale_t for the given collation OID.  Results are
  * cached for the lifetime of the backend.
@@ -1714,14 +1743,7 @@ pg_newlocale_from_collation(Oid collid)
 	if (last_collation_cache_oid == collid)
 		return last_collation_cache_locale;
 
-	/*
-	 * Cache mechanism for collation information.
-	 *
-	 * Note that we currently lack any way to flush the cache.  Since we don't
-	 * support ALTER COLLATION, this is OK.  The worst case is that someone
-	 * drops a collation, and a useless cache entry hangs around in existing
-	 * backends.
-	 */
+	/* cache mechanism for collation information */
 	if (CollationCache == NULL)
 	{
 		CollationCacheOwner = ResourceOwnerCreate(NULL, "collation cache");
@@ -1730,6 +1752,9 @@ pg_newlocale_from_collation(Oid collid)
 													  ALLOCSET_DEFAULT_SIZES);
 		CollationCache = collation_cache_create(CollationCacheContext,
 												16, NULL);
+		CacheRegisterSyscacheCallback(COLLOID,
+									  CollationCacheInvalidate,
+									  (Datum) 0);
 	}
 
 	cache_entry = collation_cache_insert(CollationCache, collid, &found);
-- 
2.34.1

#4Michael Paquier
michael@paquier.xyz
In reply to: Jeff Davis (#3)
Re: [18] Fix a few issues with the collation cache

On Fri, Sep 20, 2024 at 05:28:48PM -0700, Jeff Davis wrote:

Updated and rebased.

The patch has been failing to apply for a couple of weeks now. Could
you rebase please?
--
Michael

#5Jeff Davis
pgsql@j-davis.com
In reply to: Michael Paquier (#4)
Re: [18] Fix a few issues with the collation cache

On Tue, 2024-12-10 at 15:44 +0900, Michael Paquier wrote:

On Fri, Sep 20, 2024 at 05:28:48PM -0700, Jeff Davis wrote:

Updated and rebased.

The patch has been failing to apply for a couple of weeks now.  Could
you rebase please?

I committed some of the patches and fixed problem #1.

The way I used ResourceOwners to fix problems #2 and #3 is a bit
awkward. I'm not sure if it's worth the ceremony to try to avoid leaks
during OOM. And other paths that leak could be fixed more simply by
freeing it directly rather than relying on the resowner mechanism.
I think I'll withdraw this patch and submit a separate patch to do it
the simpler way.

Regards,
Jeff Davis

#6Michael Paquier
michael@paquier.xyz
In reply to: Jeff Davis (#5)
Re: [18] Fix a few issues with the collation cache

On Tue, Dec 10, 2024 at 03:34:50PM -0800, Jeff Davis wrote:

I committed some of the patches and fixed problem #1.

The way I used ResourceOwners to fix problems #2 and #3 is a bit
awkward. I'm not sure if it's worth the ceremony to try to avoid leaks
during OOM. And other paths that leak could be fixed more simply by
freeing it directly rather than relying on the resowner mechanism.
I think I'll withdraw this patch and submit a separate patch to do it
the simpler way.

Okay, thanks for the update.
--
Michael