From 327160f7f96bcdc1c3779d25bb0228f25a4c2504 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 16 Jan 2025 08:51:09 +0000
Subject: [PATCH v5 1/4] Add allow_critical_section to PgStat_KindInfo for
 pgstats kinds

This new field controls if a variable-numbered stats kind is allowed
to allocate memory in contexts while in a critical section.

Allocating memory in contexts while in a critical section isn't usually allowed,
but we make an exception.

It means that there's a theoretical possibility to  run out of memory while
allocating the memory, which leads to a PANIC.

The memory allocation here are small (pending entries or entry reference) so
that's unlikely to happen in practice.

This is useful for a following commit that will track WAL statistics while in
a critical section (while in XLogWrite() for example).
---
 src/backend/utils/activity/pgstat.c           | 19 +++++++++++++++++++
 src/backend/utils/activity/pgstat_shmem.c     | 13 +++++++++++++
 src/include/utils/pgstat_internal.h           |  3 +++
 .../injection_points/injection_stats.c        |  1 +
 4 files changed, 36 insertions(+)
  89.9% src/backend/utils/activity/
   7.6% src/include/utils/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 34520535d54..a942e04bb97 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -284,6 +284,7 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
 
 		.fixed_amount = false,
 		.write_to_file = true,
+		.allow_critical_section = false,
 		/* so pg_stat_database entries can be seen in all databases */
 		.accessed_across_databases = true,
 
@@ -301,6 +302,7 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
 
 		.fixed_amount = false,
 		.write_to_file = true,
+		.allow_critical_section = false,
 
 		.shared_size = sizeof(PgStatShared_Relation),
 		.shared_data_off = offsetof(PgStatShared_Relation, stats),
@@ -316,6 +318,7 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
 
 		.fixed_amount = false,
 		.write_to_file = true,
+		.allow_critical_section = false,
 
 		.shared_size = sizeof(PgStatShared_Function),
 		.shared_data_off = offsetof(PgStatShared_Function, stats),
@@ -330,6 +333,7 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
 
 		.fixed_amount = false,
 		.write_to_file = true,
+		.allow_critical_section = false,
 
 		.accessed_across_databases = true,
 
@@ -347,6 +351,7 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
 
 		.fixed_amount = false,
 		.write_to_file = true,
+		.allow_critical_section = false,
 		/* so pg_stat_subscription_stats entries can be seen in all databases */
 		.accessed_across_databases = true,
 
@@ -364,6 +369,7 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
 
 		.fixed_amount = false,
 		.write_to_file = false,
+		.allow_critical_section = false,
 
 		.accessed_across_databases = true,
 
@@ -1316,7 +1322,20 @@ pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *creat
 
 		Assert(entrysize != (size_t) -1);
 
+		/*
+		 * That could be done within a critical section, which isn't usually
+		 * allowed, but we make an exception. It means that there's a
+		 * theoretical possibility that you run out of memory while preparing
+		 * the entry, which leads to a PANIC. Fortunately the pending entry is
+		 * small so that's unlikely to happen in practice.
+		 */
+		if (pgstat_get_kind_info(kind)->allow_critical_section)
+			MemoryContextAllowInCriticalSection(pgStatPendingContext, true);
+
 		entry_ref->pending = MemoryContextAllocZero(pgStatPendingContext, entrysize);
+
+		MemoryContextAllowInCriticalSection(pgStatPendingContext, false);
+
 		dlist_push_tail(&pgStatPending, &entry_ref->pending_node);
 	}
 
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 342586397d6..cb1ad330bd4 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -402,9 +402,22 @@ pgstat_get_entry_ref_cached(PgStat_HashKey key, PgStat_EntryRef **entry_ref_p)
 	{
 		PgStat_EntryRef *entry_ref;
 
+		/*
+		 * That could be done within a critical section, which isn't usually
+		 * allowed, but we make an exception. It means that there's a
+		 * theoretical possibility that you run out of memory while creating
+		 * the entry ref, which leads to a PANIC. Fortunately the pending
+		 * entry is small so that's unlikely to happen in practice.
+		 */
+		if (pgstat_get_kind_info(key.kind)->allow_critical_section)
+			MemoryContextAllowInCriticalSection(pgStatSharedRefContext, true);
+
 		cache_entry->entry_ref = entry_ref =
 			MemoryContextAlloc(pgStatSharedRefContext,
 							   sizeof(PgStat_EntryRef));
+
+		MemoryContextAllowInCriticalSection(pgStatSharedRefContext, false);
+
 		entry_ref->shared_stats = NULL;
 		entry_ref->shared_entry = NULL;
 		entry_ref->pending = NULL;
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 4bb8e5c53ab..0b21603c863 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -216,6 +216,9 @@ typedef struct PgStat_KindInfo
 	/* Should stats be written to the on-disk stats file? */
 	bool		write_to_file:1;
 
+	/* For variable-numbered stats: allow memory context in critical section */
+	bool		allow_critical_section:1;
+
 	/*
 	 * The size of an entry in the shared stats hash table (pointed to by
 	 * PgStatShared_HashEntry->body).  For fixed-numbered statistics, this is
diff --git a/src/test/modules/injection_points/injection_stats.c b/src/test/modules/injection_points/injection_stats.c
index 5db62bca66f..873dc88aace 100644
--- a/src/test/modules/injection_points/injection_stats.c
+++ b/src/test/modules/injection_points/injection_stats.c
@@ -40,6 +40,7 @@ static const PgStat_KindInfo injection_stats = {
 	.name = "injection_points",
 	.fixed_amount = false,		/* Bounded by the number of points */
 	.write_to_file = true,
+	.allow_critical_section = false,
 
 	/* Injection points are system-wide */
 	.accessed_across_databases = true,
-- 
2.34.1

