From 6728eb684760be8b883c69dedc7bb089fa090d7c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 13 Jun 2024 13:37:24 +0900
Subject: [PATCH 4/6] injection_points: Add statistics for custom points

This acts as a template of what can be achieved with the pluggable
cumulative stats APIs, while being useful on its own for injection
points.

Currently, the only data gathered is the number of times an injection
point is called.  This can be extended as required.  All the routines
related to the stats are located in their own file, for clarity.
---
 src/test/modules/injection_points/Makefile    |   7 +-
 .../expected/injection_points.out             |  67 +++++++
 .../injection_points--1.0.sql                 |  10 +
 .../injection_points/injection_points.c       |  17 ++
 .../injection_points/injection_stats.c        | 176 ++++++++++++++++++
 .../injection_points/injection_stats.h        |  22 +++
 src/test/modules/injection_points/meson.build |   1 +
 .../injection_points/sql/injection_points.sql |  15 ++
 src/tools/pgindent/typedefs.list              |   2 +
 9 files changed, 315 insertions(+), 2 deletions(-)
 create mode 100644 src/test/modules/injection_points/injection_stats.c
 create mode 100644 src/test/modules/injection_points/injection_stats.h

diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 31bd787994..676823a87f 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -1,7 +1,10 @@
 # src/test/modules/injection_points/Makefile
 
-MODULES = injection_points
-
+MODULE_big = injection_points
+OBJS = \
+	$(WIN32RES) \
+	injection_points.o \
+	injection_stats.o
 EXTENSION = injection_points
 DATA = injection_points--1.0.sql
 PGFILEDESC = "injection_points - facility for injection points"
diff --git a/src/test/modules/injection_points/expected/injection_points.out b/src/test/modules/injection_points/expected/injection_points.out
index dd9db06e10..cb50a664f9 100644
--- a/src/test/modules/injection_points/expected/injection_points.out
+++ b/src/test/modules/injection_points/expected/injection_points.out
@@ -208,5 +208,72 @@ SELECT injection_points_detach('TestConditionLocal1');
  
 (1 row)
 
+-- Statistics
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- nothing
+ injection_points_stats_numcalls 
+---------------------------------
+                                
+(1 row)
+
+SELECT injection_points_attach('TestConditionStats', 'notice');
+ injection_points_attach 
+-------------------------
+ 
+(1 row)
+
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- returns 0
+ injection_points_stats_numcalls 
+---------------------------------
+                               0
+(1 row)
+
+SELECT injection_points_run('TestConditionStats');
+NOTICE:  notice triggered for injection point TestConditionStats
+ injection_points_run 
+----------------------
+ 
+(1 row)
+
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- returns 1
+ injection_points_stats_numcalls 
+---------------------------------
+                               1
+(1 row)
+
+-- Check transactions with stats snapshots
+BEGIN;
+SET stats_fetch_consistency TO snapshot;
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- returns 1
+ injection_points_stats_numcalls 
+---------------------------------
+                               1
+(1 row)
+
+SELECT injection_points_run('TestConditionStats');
+NOTICE:  notice triggered for injection point TestConditionStats
+ injection_points_run 
+----------------------
+ 
+(1 row)
+
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- still 1
+ injection_points_stats_numcalls 
+---------------------------------
+                               1
+(1 row)
+
+COMMIT;
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- now 2
+ injection_points_stats_numcalls 
+---------------------------------
+                               2
+(1 row)
+
+SELECT injection_points_detach('TestConditionStats');
+ injection_points_detach 
+-------------------------
+ 
+(1 row)
+
 DROP EXTENSION injection_points;
 DROP FUNCTION wait_pid;
diff --git a/src/test/modules/injection_points/injection_points--1.0.sql b/src/test/modules/injection_points/injection_points--1.0.sql
index c16a33b08d..deaf47d8ae 100644
--- a/src/test/modules/injection_points/injection_points--1.0.sql
+++ b/src/test/modules/injection_points/injection_points--1.0.sql
@@ -54,3 +54,13 @@ CREATE FUNCTION injection_points_detach(IN point_name TEXT)
 RETURNS void
 AS 'MODULE_PATHNAME', 'injection_points_detach'
 LANGUAGE C STRICT PARALLEL UNSAFE;
+
+--
+-- injection_points_stats_numcalls()
+--
+-- Reports statistics, if any, related to the given injection point.
+--
+CREATE FUNCTION injection_points_stats_numcalls(IN point_name TEXT)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'injection_points_stats_numcalls'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/injection_points/injection_points.c b/src/test/modules/injection_points/injection_points.c
index 5c44625d1d..6346af6cf6 100644
--- a/src/test/modules/injection_points/injection_points.c
+++ b/src/test/modules/injection_points/injection_points.c
@@ -18,6 +18,7 @@
 #include "postgres.h"
 
 #include "fmgr.h"
+#include "injection_stats.h"
 #include "miscadmin.h"
 #include "nodes/pg_list.h"
 #include "nodes/value.h"
@@ -170,6 +171,9 @@ injection_points_cleanup(int code, Datum arg)
 		char	   *name = strVal(lfirst(lc));
 
 		(void) InjectionPointDetach(name);
+
+		/* Remove stats entry */
+		pgstat_drop_inj(name);
 	}
 }
 
@@ -182,6 +186,8 @@ injection_error(const char *name, const void *private_data)
 	if (!injection_point_allowed(condition))
 		return;
 
+	pgstat_report_inj(name);
+
 	elog(ERROR, "error triggered for injection point %s", name);
 }
 
@@ -193,6 +199,8 @@ injection_notice(const char *name, const void *private_data)
 	if (!injection_point_allowed(condition))
 		return;
 
+	pgstat_report_inj(name);
+
 	elog(NOTICE, "notice triggered for injection point %s", name);
 }
 
@@ -211,6 +219,8 @@ injection_wait(const char *name, const void *private_data)
 	if (!injection_point_allowed(condition))
 		return;
 
+	pgstat_report_inj(name);
+
 	/*
 	 * Use the injection point name for this custom wait event.  Note that
 	 * this custom wait event name is not released, but we don't care much for
@@ -299,6 +309,10 @@ injection_points_attach(PG_FUNCTION_ARGS)
 		inj_list_local = lappend(inj_list_local, makeString(pstrdup(name)));
 		MemoryContextSwitchTo(oldctx);
 	}
+
+	/* Add entry for stats */
+	pgstat_create_inj(name);
+
 	PG_RETURN_VOID();
 }
 
@@ -400,5 +414,8 @@ injection_points_detach(PG_FUNCTION_ARGS)
 		MemoryContextSwitchTo(oldctx);
 	}
 
+	/* Remove stats entry */
+	pgstat_drop_inj(name);
+
 	PG_RETURN_VOID();
 }
diff --git a/src/test/modules/injection_points/injection_stats.c b/src/test/modules/injection_points/injection_stats.c
new file mode 100644
index 0000000000..6314486d79
--- /dev/null
+++ b/src/test/modules/injection_points/injection_stats.c
@@ -0,0 +1,176 @@
+/*--------------------------------------------------------------------------
+ *
+ * injection_stats.c
+ *		Code for statistics of injection points.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/test/modules/injection_points/injection_stats.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+
+#include "common/hashfn.h"
+#include "injection_stats.h"
+#include "pgstat.h"
+#include "utils/builtins.h"
+#include "utils/pgstat_internal.h"
+
+/* Structures for statistics of injection points */
+typedef struct PgStat_StatInjEntry
+{
+	PgStat_Counter numcalls;	/* number of times point has been run */
+} PgStat_StatInjEntry;
+
+typedef struct PgStatShared_InjectionPoint
+{
+	PgStatShared_Common header;
+	PgStat_StatInjEntry stats;
+} PgStatShared_InjectionPoint;
+
+static bool injection_stats_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
+
+static const PgStat_KindInfo injection_stats = {
+	.name = "injection_points",
+	.fixed_amount = false,		/* Bounded by the number of points */
+
+	/* Injection points are system-wide */
+	.accessed_across_databases = true,
+
+	.shared_size = sizeof(PgStatShared_InjectionPoint),
+	.shared_data_off = offsetof(PgStatShared_InjectionPoint, stats),
+	.shared_data_len = sizeof(((PgStatShared_InjectionPoint *) 0)->stats),
+	.pending_size = sizeof(PgStat_StatInjEntry),
+	.flush_pending_cb = injection_stats_flush_cb,
+};
+
+/*
+ * Compute stats entry idx from point name with a 4-byte hash.
+ */
+#define PGSTAT_INJ_IDX(name) hash_bytes((const unsigned char *) name, strlen(name))
+
+static PgStat_Kind inj_stats_kind = PGSTAT_KIND_INVALID;
+
+/*
+ * Callback for stats handling
+ */
+static bool
+injection_stats_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
+{
+	PgStat_StatInjEntry *localent;
+	PgStatShared_InjectionPoint *shfuncent;
+
+	localent = (PgStat_StatInjEntry *) entry_ref->pending;
+	shfuncent = (PgStatShared_InjectionPoint *) entry_ref->shared_stats;
+
+	if (!pgstat_lock_entry(entry_ref, nowait))
+		return false;
+
+	shfuncent->stats.numcalls += localent->numcalls;
+	return true;
+}
+
+/*
+ * Support function for the SQL-callable pgstat* functions.  Returns
+ * a pointer to the injection point statistics struct.
+ */
+static PgStat_StatInjEntry *
+pgstat_fetch_stat_injentry(const char *name)
+{
+	PgStat_StatInjEntry *entry = NULL;
+
+	if (inj_stats_kind == PGSTAT_KIND_INVALID)
+		inj_stats_kind = pgstat_add_kind(&injection_stats);
+
+	/* Compile the lookup key as a hash of the point name */
+	entry = (PgStat_StatInjEntry *) pgstat_fetch_entry(inj_stats_kind,
+													   InvalidOid,
+													   PGSTAT_INJ_IDX(name));
+	return entry;
+}
+
+/*
+ * Report injection point creation.
+ */
+void
+pgstat_create_inj(const char *name)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_InjectionPoint *shstatent;
+
+	if (inj_stats_kind == PGSTAT_KIND_INVALID)
+		inj_stats_kind = pgstat_add_kind(&injection_stats);
+
+	entry_ref = pgstat_get_entry_ref_locked(inj_stats_kind, InvalidOid,
+											PGSTAT_INJ_IDX(name), false);
+	shstatent = (PgStatShared_InjectionPoint *) entry_ref->shared_stats;
+
+	/* initialize shared memory data */
+	memset(&shstatent->stats, 0, sizeof(shstatent->stats));
+	pgstat_unlock_entry(entry_ref);
+}
+
+/*
+ * Report injection point drop.
+ */
+void
+pgstat_drop_inj(const char *name)
+{
+	if (inj_stats_kind == PGSTAT_KIND_INVALID)
+		inj_stats_kind = pgstat_add_kind(&injection_stats);
+
+	if (!pgstat_drop_entry(inj_stats_kind, InvalidOid,
+						   PGSTAT_INJ_IDX(name)))
+		pgstat_request_entry_refs_gc();
+}
+
+/*
+ * Report statistics for injection point.
+ *
+ * This is simple because the set of stats to report currently is simple:
+ * track the number of times a point has been run.
+ */
+void
+pgstat_report_inj(const char *name)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_InjectionPoint *shstatent;
+	PgStat_StatInjEntry *statent;
+
+	if (inj_stats_kind == PGSTAT_KIND_INVALID)
+		inj_stats_kind = pgstat_add_kind(&injection_stats);
+
+	entry_ref = pgstat_get_entry_ref_locked(inj_stats_kind, InvalidOid,
+											PGSTAT_INJ_IDX(name), false);
+
+	shstatent = (PgStatShared_InjectionPoint *) entry_ref->shared_stats;
+	statent = &shstatent->stats;
+
+	/* Update the injection point statistics */
+	statent->numcalls++;
+
+	pgstat_unlock_entry(entry_ref);
+}
+
+/*
+ * SQL function returning the number of times an injection point
+ * has been called.
+ */
+PG_FUNCTION_INFO_V1(injection_points_stats_numcalls);
+Datum
+injection_points_stats_numcalls(PG_FUNCTION_ARGS)
+{
+	char	   *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	PgStat_StatInjEntry *entry = pgstat_fetch_stat_injentry(name);
+
+	if (entry == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(entry->numcalls);
+}
diff --git a/src/test/modules/injection_points/injection_stats.h b/src/test/modules/injection_points/injection_stats.h
new file mode 100644
index 0000000000..15621f49fd
--- /dev/null
+++ b/src/test/modules/injection_points/injection_stats.h
@@ -0,0 +1,22 @@
+/*--------------------------------------------------------------------------
+ *
+ * injection_stats.h
+ *		Definitions for statistics of injection points.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/test/modules/injection_points/injection_stats.h
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#ifndef INJECTION_STATS
+#define INJECTION_STATS
+
+extern void pgstat_create_inj(const char *name);
+extern void pgstat_drop_inj(const char *name);
+extern void pgstat_report_inj(const char *name);
+
+#endif
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 8e1b5b4539..526fbc1457 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -6,6 +6,7 @@ endif
 
 injection_points_sources = files(
   'injection_points.c',
+  'injection_stats.c',
 )
 
 if host_system == 'windows'
diff --git a/src/test/modules/injection_points/sql/injection_points.sql b/src/test/modules/injection_points/sql/injection_points.sql
index 71e2972a7e..04cf7f48db 100644
--- a/src/test/modules/injection_points/sql/injection_points.sql
+++ b/src/test/modules/injection_points/sql/injection_points.sql
@@ -66,5 +66,20 @@ SELECT injection_points_detach('TestConditionError');
 SELECT injection_points_attach('TestConditionLocal1', 'error');
 SELECT injection_points_detach('TestConditionLocal1');
 
+-- Statistics
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- nothing
+SELECT injection_points_attach('TestConditionStats', 'notice');
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- returns 0
+SELECT injection_points_run('TestConditionStats');
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- returns 1
+-- Check transactions with stats snapshots
+BEGIN;
+SET stats_fetch_consistency TO snapshot;
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- returns 1
+SELECT injection_points_run('TestConditionStats');
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- still 1
+COMMIT;
+SELECT injection_points_stats_numcalls('TestConditionStats'); -- now 2
+SELECT injection_points_detach('TestConditionStats');
 DROP EXTENSION injection_points;
 DROP FUNCTION wait_pid;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8718ca54e0..29197fe212 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2119,6 +2119,7 @@ PgStatShared_Common
 PgStatShared_Database
 PgStatShared_Function
 PgStatShared_HashEntry
+PgStatShared_InjectionPoint
 PgStatShared_IO
 PgStatShared_Relation
 PgStatShared_ReplSlot
@@ -2150,6 +2151,7 @@ PgStat_Snapshot
 PgStat_SnapshotEntry
 PgStat_StatDBEntry
 PgStat_StatFuncEntry
+PgStat_StatInjEntry
 PgStat_StatReplSlotEntry
 PgStat_StatSubEntry
 PgStat_StatTabEntry
-- 
2.43.0

