From 316abf87ff67bfbbe9c43291a864f5860f5b0043 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 6 Dec 2025 00:54:06 +0000 Subject: [PATCH v3 1/1] Move custom stats tests from injection_points to dedicated module Extract custom statistics testing code from injection_points module into new test_custom_stats module with separate variable and fixed statistics components. This improves test organization and removes statistics functionality that was not core to injection points. Discussion: https://www.postgresql.org/message-id/CAA5RZ0sJgO6GAwgFxmzg9MVP%3DrM7Us8KKcWpuqxe-f5qxmpE0g%40mail.gmail.com --- src/test/modules/Makefile | 1 + src/test/modules/injection_points/Makefile | 4 - .../injection_points--1.0.sql | 43 --- .../injection_points/injection_points.c | 48 --- .../injection_points/injection_stats.c | 228 ------------- .../injection_points/injection_stats.h | 35 -- .../injection_points/injection_stats_fixed.c | 214 ------------- src/test/modules/injection_points/meson.build | 12 - .../modules/injection_points/t/001_stats.pl | 103 ------ src/test/modules/meson.build | 1 + src/test/modules/test_custom_stats/.gitignore | 4 + src/test/modules/test_custom_stats/Makefile | 27 ++ .../modules/test_custom_stats/meson.build | 55 ++++ .../test_custom_stats/t/001_custom_stats.pl | 115 +++++++ .../test_custom_fixed_stats--1.0.sql | 20 ++ .../test_custom_fixed_stats.c | 224 +++++++++++++ .../test_custom_fixed_stats.control | 4 + .../test_custom_var_stats--1.0.sql | 25 ++ .../test_custom_stats/test_custom_var_stats.c | 302 ++++++++++++++++++ .../test_custom_var_stats.control | 4 + 20 files changed, 782 insertions(+), 687 deletions(-) delete mode 100644 src/test/modules/injection_points/injection_stats.c delete mode 100644 src/test/modules/injection_points/injection_stats.h delete mode 100644 src/test/modules/injection_points/injection_stats_fixed.c delete mode 100644 src/test/modules/injection_points/t/001_stats.pl create mode 100644 src/test/modules/test_custom_stats/.gitignore create mode 100644 src/test/modules/test_custom_stats/Makefile create mode 100644 src/test/modules/test_custom_stats/meson.build create mode 100644 src/test/modules/test_custom_stats/t/001_custom_stats.pl create mode 100644 src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql create mode 100644 src/test/modules/test_custom_stats/test_custom_fixed_stats.c create mode 100644 src/test/modules/test_custom_stats/test_custom_fixed_stats.control create mode 100644 src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql create mode 100644 src/test/modules/test_custom_stats/test_custom_var_stats.c create mode 100644 src/test/modules/test_custom_stats/test_custom_var_stats.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index d079b91b1a2..4a109ccbf6c 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -21,6 +21,7 @@ SUBDIRS = \ test_bloomfilter \ test_copy_callbacks \ test_custom_rmgrs \ + test_custom_stats \ test_ddl_deparse \ test_dsa \ test_dsm_registry \ diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index a618e6a9899..c85034eb8cc 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -4,8 +4,6 @@ MODULE_big = injection_points OBJS = \ $(WIN32RES) \ injection_points.o \ - injection_stats.o \ - injection_stats_fixed.o \ regress_injection.o EXTENSION = injection_points DATA = injection_points--1.0.sql @@ -23,8 +21,6 @@ ISOLATION = basic \ reindex-concurrently-upsert-on-constraint \ reindex-concurrently-upsert-partitioned -TAP_TESTS = 1 - # The injection points are cluster-wide, so disable installcheck NO_INSTALLCHECK = 1 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 a51ff538684..861c7355d4e 100644 --- a/src/test/modules/injection_points/injection_points--1.0.sql +++ b/src/test/modules/injection_points/injection_points--1.0.sql @@ -101,49 +101,6 @@ RETURNS SETOF record AS 'MODULE_PATHNAME', 'injection_points_list' LANGUAGE C STRICT VOLATILE PARALLEL RESTRICTED; --- --- 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; - --- --- injection_points_stats_count() --- --- Return the number of entries stored in the pgstats hash table. --- -CREATE FUNCTION injection_points_stats_count() -RETURNS bigint -AS 'MODULE_PATHNAME', 'injection_points_stats_count' -LANGUAGE C STRICT; - --- --- injection_points_stats_drop() --- --- Drop all statistics of injection points. --- -CREATE FUNCTION injection_points_stats_drop() -RETURNS void -AS 'MODULE_PATHNAME', 'injection_points_stats_drop' -LANGUAGE C STRICT; - --- --- injection_points_stats_fixed() --- --- Reports fixed-numbered statistics for injection points. -CREATE FUNCTION injection_points_stats_fixed(OUT numattach int8, - OUT numdetach int8, - OUT numrun int8, - OUT numcached int8, - OUT numloaded int8) -RETURNS record -AS 'MODULE_PATHNAME', 'injection_points_stats_fixed' -LANGUAGE C STRICT; - -- -- regress_injection.c functions -- diff --git a/src/test/modules/injection_points/injection_points.c b/src/test/modules/injection_points/injection_points.c index b7c1c58ea56..417b61f31c5 100644 --- a/src/test/modules/injection_points/injection_points.c +++ b/src/test/modules/injection_points/injection_points.c @@ -19,7 +19,6 @@ #include "fmgr.h" #include "funcapi.h" -#include "injection_stats.h" #include "miscadmin.h" #include "nodes/pg_list.h" #include "nodes/value.h" @@ -107,15 +106,6 @@ extern PGDLLEXPORT void injection_wait(const char *name, /* track if injection points attached in this process are linked to it */ static bool injection_point_local = false; -/* - * GUC variable - * - * This GUC is useful to control if statistics should be enabled or not - * during a test with injection points, like for example if a test relies - * on a callback run in a critical section where no allocation should happen. - */ -bool inj_stats_enabled = false; - /* Shared memory init callbacks */ static shmem_request_hook_type prev_shmem_request_hook = NULL; static shmem_startup_hook_type prev_shmem_startup_hook = NULL; @@ -235,9 +225,6 @@ injection_points_cleanup(int code, Datum arg) char *name = strVal(lfirst(lc)); (void) InjectionPointDetach(name); - - /* Remove stats entry */ - pgstat_drop_inj(name); } } @@ -251,8 +238,6 @@ injection_error(const char *name, const void *private_data, void *arg) if (!injection_point_allowed(condition)) return; - pgstat_report_inj(name); - if (argstr) elog(ERROR, "error triggered for injection point %s (%s)", name, argstr); @@ -269,8 +254,6 @@ injection_notice(const char *name, const void *private_data, void *arg) if (!injection_point_allowed(condition)) return; - pgstat_report_inj(name); - if (argstr) elog(NOTICE, "notice triggered for injection point %s (%s)", name, argstr); @@ -293,8 +276,6 @@ injection_wait(const char *name, const void *private_data, void *arg) 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 @@ -371,7 +352,6 @@ injection_points_attach(PG_FUNCTION_ARGS) condition.pid = MyProcPid; } - pgstat_report_inj_fixed(1, 0, 0, 0, 0); InjectionPointAttach(name, "injection_points", function, &condition, sizeof(InjectionPointCondition)); @@ -385,9 +365,6 @@ injection_points_attach(PG_FUNCTION_ARGS) MemoryContextSwitchTo(oldctx); } - /* Add entry for stats */ - pgstat_create_inj(name); - PG_RETURN_VOID(); } @@ -422,7 +399,6 @@ injection_points_attach_func(PG_FUNCTION_ARGS) private_data_size = VARSIZE_ANY_EXHDR(private_data); } - pgstat_report_inj_fixed(1, 0, 0, 0, 0); if (private_data != NULL) InjectionPointAttach(name, lib_name, function, VARDATA_ANY(private_data), private_data_size); @@ -444,7 +420,6 @@ injection_points_load(PG_FUNCTION_ARGS) if (inj_state == NULL) injection_init_shmem(); - pgstat_report_inj_fixed(0, 0, 0, 0, 1); INJECTION_POINT_LOAD(name); PG_RETURN_VOID(); @@ -467,7 +442,6 @@ injection_points_run(PG_FUNCTION_ARGS) if (!PG_ARGISNULL(1)) arg = text_to_cstring(PG_GETARG_TEXT_PP(1)); - pgstat_report_inj_fixed(0, 0, 1, 0, 0); INJECTION_POINT(name, arg); PG_RETURN_VOID(); @@ -490,7 +464,6 @@ injection_points_cached(PG_FUNCTION_ARGS) if (!PG_ARGISNULL(1)) arg = text_to_cstring(PG_GETARG_TEXT_PP(1)); - pgstat_report_inj_fixed(0, 0, 0, 1, 0); INJECTION_POINT_CACHED(name, arg); PG_RETURN_VOID(); @@ -567,7 +540,6 @@ injection_points_detach(PG_FUNCTION_ARGS) { char *name = text_to_cstring(PG_GETARG_TEXT_PP(0)); - pgstat_report_inj_fixed(0, 1, 0, 0, 0); if (!InjectionPointDetach(name)) elog(ERROR, "could not detach injection point \"%s\"", name); @@ -581,9 +553,6 @@ injection_points_detach(PG_FUNCTION_ARGS) MemoryContextSwitchTo(oldctx); } - /* Remove stats entry */ - pgstat_drop_inj(name); - PG_RETURN_VOID(); } @@ -625,32 +594,15 @@ injection_points_list(PG_FUNCTION_ARGS) #undef NUM_INJECTION_POINTS_LIST } - void _PG_init(void) { if (!process_shared_preload_libraries_in_progress) return; - DefineCustomBoolVariable("injection_points.stats", - "Enables statistics for injection points.", - NULL, - &inj_stats_enabled, - false, - PGC_POSTMASTER, - 0, - NULL, - NULL, - NULL); - - MarkGUCPrefixReserved("injection_points"); - /* Shared memory initialization */ prev_shmem_request_hook = shmem_request_hook; shmem_request_hook = injection_shmem_request; prev_shmem_startup_hook = shmem_startup_hook; shmem_startup_hook = injection_shmem_startup; - - pgstat_register_inj(); - pgstat_register_inj_fixed(); } diff --git a/src/test/modules/injection_points/injection_stats.c b/src/test/modules/injection_points/injection_stats.c deleted file mode 100644 index 158e1631af9..00000000000 --- a/src/test/modules/injection_points/injection_stats.c +++ /dev/null @@ -1,228 +0,0 @@ -/*-------------------------------------------------------------------------- - * - * injection_stats.c - * Code for statistics of injection points. - * - * Portions Copyright (c) 1996-2025, 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 */ - .write_to_file = true, - .track_entry_count = true, - - /* 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 an 8-byte hash. - */ -#define PGSTAT_INJ_IDX(name) hash_bytes_extended((const unsigned char *) name, strlen(name), 0) - -/* - * Kind ID reserved for statistics of injection points. - */ -#define PGSTAT_KIND_INJECTION 25 - -/* Track if stats are loaded */ -static bool inj_stats_loaded = false; - -/* - * 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; - - pgstat_unlock_entry(entry_ref); - - 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_loaded || !inj_stats_enabled) - return NULL; - - /* Compile the lookup key as a hash of the point name */ - entry = (PgStat_StatInjEntry *) pgstat_fetch_entry(PGSTAT_KIND_INJECTION, - InvalidOid, - PGSTAT_INJ_IDX(name)); - return entry; -} - -/* - * Workhorse to do the registration work, called in _PG_init(). - */ -void -pgstat_register_inj(void) -{ - pgstat_register_kind(PGSTAT_KIND_INJECTION, &injection_stats); - - /* mark stats as loaded */ - inj_stats_loaded = true; -} - -/* - * Report injection point creation. - */ -void -pgstat_create_inj(const char *name) -{ - PgStat_EntryRef *entry_ref; - PgStatShared_InjectionPoint *shstatent; - - /* leave if disabled */ - if (!inj_stats_loaded || !inj_stats_enabled) - return; - - entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_INJECTION, InvalidOid, - PGSTAT_INJ_IDX(name), NULL); - - shstatent = (PgStatShared_InjectionPoint *) entry_ref->shared_stats; - - /* initialize shared memory data */ - memset(&shstatent->stats, 0, sizeof(shstatent->stats)); -} - -/* - * Report injection point drop. - */ -void -pgstat_drop_inj(const char *name) -{ - /* leave if disabled */ - if (!inj_stats_loaded || !inj_stats_enabled) - return; - - if (!pgstat_drop_entry(PGSTAT_KIND_INJECTION, 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; - PgStat_StatInjEntry *pending; - - /* leave if disabled */ - if (!inj_stats_loaded || !inj_stats_enabled) - return; - - entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_INJECTION, InvalidOid, - PGSTAT_INJ_IDX(name), NULL); - - pending = (PgStat_StatInjEntry *) entry_ref->pending; - - /* Update the injection point pending statistics */ - pending->numcalls++; -} - -/* - * 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); -} - -/* - * SQL function returning the number of entries allocated for injection - * points in the shared hashtable of pgstats. - */ -PG_FUNCTION_INFO_V1(injection_points_stats_count); -Datum -injection_points_stats_count(PG_FUNCTION_ARGS) -{ - PG_RETURN_INT64(pgstat_get_entry_count(PGSTAT_KIND_INJECTION)); -} - -/* Only used by injection_points_stats_drop() */ -static bool -match_inj_entries(PgStatShared_HashEntry *entry, Datum match_data) -{ - return entry->key.kind == PGSTAT_KIND_INJECTION; -} - -/* - * SQL function that drops all injection point statistics. - */ -PG_FUNCTION_INFO_V1(injection_points_stats_drop); -Datum -injection_points_stats_drop(PG_FUNCTION_ARGS) -{ - pgstat_drop_matching_entries(match_inj_entries, 0); - - PG_RETURN_VOID(); -} diff --git a/src/test/modules/injection_points/injection_stats.h b/src/test/modules/injection_points/injection_stats.h deleted file mode 100644 index ba310c52c7f..00000000000 --- a/src/test/modules/injection_points/injection_stats.h +++ /dev/null @@ -1,35 +0,0 @@ -/*-------------------------------------------------------------------------- - * - * injection_stats.h - * Definitions for statistics of injection points. - * - * Portions Copyright (c) 1996-2025, 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 - -/* GUC variable */ -extern bool inj_stats_enabled; - -/* injection_stats.c */ -extern void pgstat_register_inj(void); -extern void pgstat_create_inj(const char *name); -extern void pgstat_drop_inj(const char *name); -extern void pgstat_report_inj(const char *name); - -/* injection_stats_fixed.c */ -extern void pgstat_register_inj_fixed(void); -extern void pgstat_report_inj_fixed(uint32 numattach, - uint32 numdetach, - uint32 numrun, - uint32 numcached, - uint32 numloaded); - -#endif diff --git a/src/test/modules/injection_points/injection_stats_fixed.c b/src/test/modules/injection_points/injection_stats_fixed.c deleted file mode 100644 index b493e8f77a3..00000000000 --- a/src/test/modules/injection_points/injection_stats_fixed.c +++ /dev/null @@ -1,214 +0,0 @@ -/*-------------------------------------------------------------------------- - * - * injection_stats_fixed.c - * Code for fixed-numbered statistics of injection points. - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California - * - * IDENTIFICATION - * src/test/modules/injection_points/injection_stats_fixed.c - * - * ------------------------------------------------------------------------- - */ - -#include "postgres.h" - -#include "fmgr.h" - -#include "access/htup_details.h" -#include "common/hashfn.h" -#include "funcapi.h" -#include "injection_stats.h" -#include "pgstat.h" -#include "utils/builtins.h" -#include "utils/pgstat_internal.h" - -/* Structures for statistics of injection points, fixed-size */ -typedef struct PgStat_StatInjFixedEntry -{ - PgStat_Counter numattach; /* number of points attached */ - PgStat_Counter numdetach; /* number of points detached */ - PgStat_Counter numrun; /* number of points run */ - PgStat_Counter numcached; /* number of points cached */ - PgStat_Counter numloaded; /* number of points loaded */ - TimestampTz stat_reset_timestamp; -} PgStat_StatInjFixedEntry; - -typedef struct PgStatShared_InjectionPointFixed -{ - LWLock lock; /* protects all the counters */ - uint32 changecount; - PgStat_StatInjFixedEntry stats; - PgStat_StatInjFixedEntry reset_offset; -} PgStatShared_InjectionPointFixed; - -/* Callbacks for fixed-numbered stats */ -static void injection_stats_fixed_init_shmem_cb(void *stats); -static void injection_stats_fixed_reset_all_cb(TimestampTz ts); -static void injection_stats_fixed_snapshot_cb(void); - -static const PgStat_KindInfo injection_stats_fixed = { - .name = "injection_points_fixed", - .fixed_amount = true, - .write_to_file = true, - - .shared_size = sizeof(PgStat_StatInjFixedEntry), - .shared_data_off = offsetof(PgStatShared_InjectionPointFixed, stats), - .shared_data_len = sizeof(((PgStatShared_InjectionPointFixed *) 0)->stats), - - .init_shmem_cb = injection_stats_fixed_init_shmem_cb, - .reset_all_cb = injection_stats_fixed_reset_all_cb, - .snapshot_cb = injection_stats_fixed_snapshot_cb, -}; - -/* - * Kind ID reserved for statistics of injection points. - */ -#define PGSTAT_KIND_INJECTION_FIXED 26 - -/* Track if fixed-numbered stats are loaded */ -static bool inj_fixed_loaded = false; - -static void -injection_stats_fixed_init_shmem_cb(void *stats) -{ - PgStatShared_InjectionPointFixed *stats_shmem = - (PgStatShared_InjectionPointFixed *) stats; - - LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA); -} - -static void -injection_stats_fixed_reset_all_cb(TimestampTz ts) -{ - PgStatShared_InjectionPointFixed *stats_shmem = - pgstat_get_custom_shmem_data(PGSTAT_KIND_INJECTION_FIXED); - - LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE); - pgstat_copy_changecounted_stats(&stats_shmem->reset_offset, - &stats_shmem->stats, - sizeof(stats_shmem->stats), - &stats_shmem->changecount); - stats_shmem->stats.stat_reset_timestamp = ts; - LWLockRelease(&stats_shmem->lock); -} - -static void -injection_stats_fixed_snapshot_cb(void) -{ - PgStatShared_InjectionPointFixed *stats_shmem = - pgstat_get_custom_shmem_data(PGSTAT_KIND_INJECTION_FIXED); - PgStat_StatInjFixedEntry *stat_snap = - pgstat_get_custom_snapshot_data(PGSTAT_KIND_INJECTION_FIXED); - PgStat_StatInjFixedEntry *reset_offset = &stats_shmem->reset_offset; - PgStat_StatInjFixedEntry reset; - - pgstat_copy_changecounted_stats(stat_snap, - &stats_shmem->stats, - sizeof(stats_shmem->stats), - &stats_shmem->changecount); - - LWLockAcquire(&stats_shmem->lock, LW_SHARED); - memcpy(&reset, reset_offset, sizeof(stats_shmem->stats)); - LWLockRelease(&stats_shmem->lock); - - /* compensate by reset offsets */ -#define FIXED_COMP(fld) stat_snap->fld -= reset.fld; - FIXED_COMP(numattach); - FIXED_COMP(numdetach); - FIXED_COMP(numrun); - FIXED_COMP(numcached); - FIXED_COMP(numloaded); -#undef FIXED_COMP -} - -/* - * Workhorse to do the registration work, called in _PG_init(). - */ -void -pgstat_register_inj_fixed(void) -{ - pgstat_register_kind(PGSTAT_KIND_INJECTION_FIXED, &injection_stats_fixed); - - /* mark stats as loaded */ - inj_fixed_loaded = true; -} - -/* - * Report fixed number of statistics for an injection point. - */ -void -pgstat_report_inj_fixed(uint32 numattach, - uint32 numdetach, - uint32 numrun, - uint32 numcached, - uint32 numloaded) -{ - PgStatShared_InjectionPointFixed *stats_shmem; - - /* leave if disabled */ - if (!inj_fixed_loaded || !inj_stats_enabled) - return; - - stats_shmem = pgstat_get_custom_shmem_data(PGSTAT_KIND_INJECTION_FIXED); - - LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE); - - pgstat_begin_changecount_write(&stats_shmem->changecount); - stats_shmem->stats.numattach += numattach; - stats_shmem->stats.numdetach += numdetach; - stats_shmem->stats.numrun += numrun; - stats_shmem->stats.numcached += numcached; - stats_shmem->stats.numloaded += numloaded; - pgstat_end_changecount_write(&stats_shmem->changecount); - - LWLockRelease(&stats_shmem->lock); -} - -/* - * SQL function returning fixed-numbered statistics for injection points. - */ -PG_FUNCTION_INFO_V1(injection_points_stats_fixed); -Datum -injection_points_stats_fixed(PG_FUNCTION_ARGS) -{ - TupleDesc tupdesc; - Datum values[5] = {0}; - bool nulls[5] = {0}; - PgStat_StatInjFixedEntry *stats; - - if (!inj_fixed_loaded || !inj_stats_enabled) - PG_RETURN_NULL(); - - pgstat_snapshot_fixed(PGSTAT_KIND_INJECTION_FIXED); - stats = pgstat_get_custom_snapshot_data(PGSTAT_KIND_INJECTION_FIXED); - - /* Initialise attributes information in the tuple descriptor */ - tupdesc = CreateTemplateTupleDesc(5); - TupleDescInitEntry(tupdesc, (AttrNumber) 1, "numattach", - INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 2, "numdetach", - INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 3, "numrun", - INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 4, "numcached", - INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 5, "numloaded", - INT8OID, -1, 0); - BlessTupleDesc(tupdesc); - - values[0] = Int64GetDatum(stats->numattach); - values[1] = Int64GetDatum(stats->numdetach); - values[2] = Int64GetDatum(stats->numrun); - values[3] = Int64GetDatum(stats->numcached); - values[4] = Int64GetDatum(stats->numloaded); - nulls[0] = false; - nulls[1] = false; - nulls[2] = false; - nulls[3] = false; - nulls[4] = false; - - /* Returns the record as Datum */ - PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); -} diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index 1a2af8a26c4..8d6f662040d 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -6,8 +6,6 @@ endif injection_points_sources = files( 'injection_points.c', - 'injection_stats.c', - 'injection_stats_fixed.c', 'regress_injection.c', ) @@ -58,14 +56,4 @@ tests += { # Some tests wait for all snapshots, so avoid parallel execution 'runningcheck-parallel': false, }, - 'tap': { - 'env': { - 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', - }, - 'tests': [ - 't/001_stats.pl', - ], - # The injection points are cluster-wide, so disable installcheck - 'runningcheck': false, - }, } diff --git a/src/test/modules/injection_points/t/001_stats.pl b/src/test/modules/injection_points/t/001_stats.pl deleted file mode 100644 index 47ab58d0e9b..00000000000 --- a/src/test/modules/injection_points/t/001_stats.pl +++ /dev/null @@ -1,103 +0,0 @@ - -# Copyright (c) 2024-2025, PostgreSQL Global Development Group - -# Tests for Custom Cumulative Statistics. - -use strict; -use warnings FATAL => 'all'; -use locale; - -use PostgreSQL::Test::Cluster; -use PostgreSQL::Test::Utils; -use Test::More; - -# Test persistency of statistics generated for injection points. -if ($ENV{enable_injection_points} ne 'yes') -{ - plan skip_all => 'Injection points not supported by this build'; -} - -# Node initialization -my $node = PostgreSQL::Test::Cluster->new('master'); -$node->init; -$node->append_conf( - 'postgresql.conf', qq( -shared_preload_libraries = 'injection_points' -injection_points.stats = true -)); -$node->start; -$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); - -# This should count for two calls. -$node->safe_psql('postgres', - "SELECT injection_points_attach('stats-notice', 'notice');"); -$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');"); -$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');"); -my $numcalls = $node->safe_psql('postgres', - "SELECT injection_points_stats_numcalls('stats-notice');"); -is($numcalls, '2', 'number of stats calls'); -my $entrycount = - $node->safe_psql('postgres', "SELECT injection_points_stats_count();"); -is($entrycount, '1', 'number of entries'); -my $fixedstats = $node->safe_psql('postgres', - "SELECT * FROM injection_points_stats_fixed();"); -is($fixedstats, '1|0|2|0|0', 'fixed stats after some calls'); - -# Loading and caching. -$node->safe_psql( - 'postgres', " -SELECT injection_points_load('stats-notice'); -SELECT injection_points_cached('stats-notice'); -"); -$fixedstats = $node->safe_psql('postgres', - "SELECT * FROM injection_points_stats_fixed();"); -is($fixedstats, '1|0|2|1|1', 'fixed stats after loading and caching'); - -# Restart the node cleanly, stats should still be around. -$node->restart; -$numcalls = $node->safe_psql('postgres', - "SELECT injection_points_stats_numcalls('stats-notice');"); -is($numcalls, '3', 'number of stats after clean restart'); -$entrycount = - $node->safe_psql('postgres', "SELECT injection_points_stats_count();"); -is($entrycount, '1', 'number of entries after clean restart'); -$fixedstats = $node->safe_psql('postgres', - "SELECT * FROM injection_points_stats_fixed();"); -is($fixedstats, '1|0|2|1|1', 'fixed stats after clean restart'); - -# On crash the stats are gone. -$node->stop('immediate'); -$node->start; -$numcalls = $node->safe_psql('postgres', - "SELECT injection_points_stats_numcalls('stats-notice');"); -is($numcalls, '', 'number of stats after crash'); -$entrycount = - $node->safe_psql('postgres', "SELECT injection_points_stats_count();"); -is($entrycount, '0', 'number of entries after crash'); -$fixedstats = $node->safe_psql('postgres', - "SELECT * FROM injection_points_stats_fixed();"); -is($fixedstats, '0|0|0|0|0', 'fixed stats after crash'); - -# On drop all stats are gone -$node->safe_psql('postgres', - "SELECT injection_points_attach('stats-notice', 'notice');"); -$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');"); -$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');"); -$numcalls = $node->safe_psql('postgres', - "SELECT injection_points_stats_numcalls('stats-notice');"); -is($numcalls, '2', 'number of stats calls'); -$node->safe_psql('postgres', "SELECT injection_points_stats_drop();"); -$numcalls = $node->safe_psql('postgres', - "SELECT injection_points_stats_numcalls('stats-notice');"); -is($numcalls, '', 'no stats after drop via SQL function'); -$entrycount = - $node->safe_psql('postgres', "SELECT injection_points_stats_count();"); -is($entrycount, '0', 'number of entries after drop via SQL function'); - -# Stop the server, disable the module, then restart. The server -# should be able to come up. -$node->stop; -$node->adjust_conf('postgresql.conf', 'shared_preload_libraries', "''"); -$node->start; - -done_testing(); diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index cc57461e59a..2806db485d3 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -21,6 +21,7 @@ subdir('test_bitmapset') subdir('test_bloomfilter') subdir('test_copy_callbacks') subdir('test_custom_rmgrs') +subdir('test_custom_stats') subdir('test_ddl_deparse') subdir('test_dsa') subdir('test_dsm_registry') diff --git a/src/test/modules/test_custom_stats/.gitignore b/src/test/modules/test_custom_stats/.gitignore new file mode 100644 index 00000000000..5dcb3ff9723 --- /dev/null +++ b/src/test/modules/test_custom_stats/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_custom_stats/Makefile b/src/test/modules/test_custom_stats/Makefile new file mode 100644 index 00000000000..5b065a1cd42 --- /dev/null +++ b/src/test/modules/test_custom_stats/Makefile @@ -0,0 +1,27 @@ +# src/test/modules/test_custom_stats/Makefile + +MODULES = test_custom_var_stats test_custom_fixed_stats + +EXTENSION = test_custom_var_stats test_custom_fixed_stats + +OBJS = \ + $(WIN32RES) \ + test_custom_var_stats.o \ + test_custom_fixed_stats.o +PGFILEDESC = "test_custom_stats - test code for custom stat kinds" + +DATA = test_custom_var_stats--1.0.sql \ + test_custom_fixed_stats--1.0.sql + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_custom_stats +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_custom_stats/meson.build b/src/test/modules/test_custom_stats/meson.build new file mode 100644 index 00000000000..a734467e169 --- /dev/null +++ b/src/test/modules/test_custom_stats/meson.build @@ -0,0 +1,55 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +test_custom_var_stats_sources = files( + 'test_custom_var_stats.c', +) + +if host_system == 'windows' + test_custom_var_stats_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_custom_var_stats', + '--FILEDESC', 'test_custom_var_stats - test code for variable custom stat kinds',]) +endif + +test_custom_var_stats = shared_module('test_custom_var_stats', + test_custom_var_stats_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_custom_var_stats + +test_install_data += files( + 'test_custom_var_stats.control', + 'test_custom_var_stats--1.0.sql', +) + +test_custom_fixed_stats_sources = files( + 'test_custom_fixed_stats.c', +) + +if host_system == 'windows' + test_custom_fixed_stats_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_custom_fixed_stats', + '--FILEDESC', 'test_custom_fixed_stats - test code for fixed custom stat kinds',]) +endif + +test_custom_fixed_stats = shared_module('test_custom_fixed_stats', + test_custom_fixed_stats_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_custom_fixed_stats + +test_install_data += files( + 'test_custom_fixed_stats.control', + 'test_custom_fixed_stats--1.0.sql', +) + +tests += { + 'name': 'test_custom_stats', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_custom_stats.pl', + ], + 'runningcheck': false, + }, +} diff --git a/src/test/modules/test_custom_stats/t/001_custom_stats.pl b/src/test/modules/test_custom_stats/t/001_custom_stats.pl new file mode 100644 index 00000000000..c4fceb7d267 --- /dev/null +++ b/src/test/modules/test_custom_stats/t/001_custom_stats.pl @@ -0,0 +1,115 @@ +# Copyright (c) 2021-2025, PostgreSQL Global Development Group + +# Test custom statistics functionality +# +# Tests both variable-amount and fixed-amount custom statistics: +# - Creation, updates, and reporting +# - Persistence across clean restarts +# - Loss after crash recovery +# - Reset functionality for fixed stats + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; +use File::Copy; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf('postgresql.conf', + "shared_preload_libraries = 'test_custom_var_stats, test_custom_fixed_stats'"); +$node->start; + +$node->safe_psql('postgres', q(CREATE EXTENSION test_custom_var_stats)); +$node->safe_psql('postgres', q(CREATE EXTENSION test_custom_fixed_stats)); + +# Create variable statistics entries +$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry1'))); +$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry2'))); +$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry3'))); +$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry4'))); + +# Update counters: entry1=2, entry2=3, entry3=2, entry4=3, fixed=3 +$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry1'))); +$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry1'))); +$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry2'))); +$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry2'))); +$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry2'))); +$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry3'))); +$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry3'))); +$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry4'))); +$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry4'))); +$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry4'))); +$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats())); +$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats())); +$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats())); + +# Test variable statistics reporting +my $result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry1'))); +is($result, "entry1|2", "var stats entry1 reports correct calls"); + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry2'))); +is($result, "entry2|3", "var stats entry2 reports correct calls"); + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry3'))); +is($result, "entry3|2", "var stats entry3 reports correct calls"); + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry4'))); +is($result, "entry4|3", "var stats entry4 reports correct calls"); + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_fixed_stats())); +is($result, "3|", "fixed stats reports correct calls"); + +# Test variable statistics drop functionality +$result = $node->safe_psql('postgres', q(select * from pgstat_drop_custom_var_stats('entry3'))); +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry3'))); +is($result, "", "entry3 not found after drop"); + +$result = $node->safe_psql('postgres', q(select * from pgstat_drop_custom_var_stats('entry4'))); +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry4'))); +is($result, "", "entry4 not found after drop"); + +# Test persistence across clean restart +$node->stop(); +$node->start(); + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry1'))); +is($result, "entry1|2", "var stats entry1 persists after clean restart"); + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry2'))); +is($result, "entry2|3", "var stats entry2 persists after clean restart"); + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_fixed_stats())); +is($result, "3|", "fixed stats persists after clean restart"); + +# Test crash recovery behavior +$node->stop('immediate'); +$node->start; + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry1'))); +is($result, "", "var stats entry1 lost after crash recovery"); + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry2'))); +is($result, "", "var stats entry2 lost after crash recovery"); + +# crash recovery sets the reset timestamp +$result = $node->safe_psql('postgres', q(select numcalls from pgstat_report_custom_fixed_stats() where stats_reset is not null)); +is($result, "0", "fixed stats reset after crash recovery"); + +# Test fixed statistics reset functionality +$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats())); +$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats())); +$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats())); + +$result = $node->safe_psql('postgres', q(select numcalls from pgstat_report_custom_fixed_stats())); +is($result, "3", "fixed stats shows calls before manual reset"); + +$node->safe_psql('postgres', q(select pgstat_reset_custom_fixed_stats())); + +$result = $node->safe_psql('postgres', q(select numcalls from pgstat_report_custom_fixed_stats() where stats_reset is not null)); +is($result, "0", "fixed stats reset after manual reset"); + +# Test completed successfully +done_testing(); \ No newline at end of file diff --git a/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql b/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql new file mode 100644 index 00000000000..e4c39749398 --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql @@ -0,0 +1,20 @@ +/* src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_custom_fixed_stats" to load this file. \quit + +CREATE FUNCTION pgstat_update_custom_fixed_stats() +RETURNS void +AS 'MODULE_PATHNAME', 'pgstat_update_custom_fixed_stats' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION pgstat_report_custom_fixed_stats(OUT numcalls bigint, + OUT stats_reset timestamptz) +RETURNS record +AS 'MODULE_PATHNAME', 'pgstat_report_custom_fixed_stats' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION pgstat_reset_custom_fixed_stats() +RETURNS void +AS 'MODULE_PATHNAME', 'pgstat_reset_custom_fixed_stats' +LANGUAGE C STRICT PARALLEL UNSAFE; \ No newline at end of file diff --git a/src/test/modules/test_custom_stats/test_custom_fixed_stats.c b/src/test/modules/test_custom_stats/test_custom_fixed_stats.c new file mode 100644 index 00000000000..f0821b4848a --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats.c @@ -0,0 +1,224 @@ +/*-------------------------------------------------------------------------- + * + * test_custom_fixed_stats.c + * Test module for fixed-amount custom statistics + * + * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_custom_stats/test_custom_fixed_stats.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "funcapi.h" +#include "pgstat.h" +#include "utils/builtins.h" +#include "utils/pgstat_internal.h" + +PG_MODULE_MAGIC_EXT( + .name = "test_custom_fixed_stats", + .version = PG_VERSION +); + +/* Fixed-amount custom statistics entry */ +typedef struct PgStat_StatCustomFixedEntry +{ + PgStat_Counter numcalls; /* # of times update function called */ + TimestampTz stat_reset_timestamp; +} PgStat_StatCustomFixedEntry; + +typedef struct PgStatShared_CustomFixedEntry +{ + LWLock lock; /* protects counters */ + uint32 changecount; /* for atomic reads */ + PgStat_StatCustomFixedEntry stats; /* current counters */ + PgStat_StatCustomFixedEntry reset_offset; /* reset baseline */ +} PgStatShared_CustomFixedEntry; + +/* Callbacks for fixed-amount statistics */ +static void pgstat_custom_fixed_init_shmem_cb(void *stats); +static void pgstat_custom_fixed_reset_all_cb(TimestampTz ts); +static void pgstat_custom_fixed_snapshot_cb(void); + +static const PgStat_KindInfo custom_stats = { + .name = "test_custom_fixed_stats", + .fixed_amount = true, /* exactly one entry */ + .write_to_file = true, /* persist to stats file */ + + .shared_size = sizeof(PgStat_StatCustomFixedEntry), + .shared_data_off = offsetof(PgStatShared_CustomFixedEntry, stats), + .shared_data_len = sizeof(((PgStatShared_CustomFixedEntry *) 0)->stats), + + .init_shmem_cb = pgstat_custom_fixed_init_shmem_cb, + .reset_all_cb = pgstat_custom_fixed_reset_all_cb, + .snapshot_cb = pgstat_custom_fixed_snapshot_cb, +}; + +/* + * Kind ID for test_custom_fixed_stats. + */ +#define PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS 26 + +/*-------------------------------------------------------------------------- + * Module initialization + *-------------------------------------------------------------------------- + */ + +void +_PG_init(void) +{ + /* Must be loaded via shared_preload_libraries */ + if (!process_shared_preload_libraries_in_progress) + return; + + /* Register custom statistics kind */ + pgstat_register_kind(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS, &custom_stats); +} + +/* + * pgstat_custom_fixed_init_shmem_cb + * Initialize shared memory structure + */ +static void +pgstat_custom_fixed_init_shmem_cb(void *stats) +{ + PgStatShared_CustomFixedEntry *stats_shmem = + (PgStatShared_CustomFixedEntry *) stats; + + LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA); +} + +/* + * pgstat_custom_fixed_reset_all_cb + * Reset the fixed stats + */ +static void +pgstat_custom_fixed_reset_all_cb(TimestampTz ts) +{ + PgStatShared_CustomFixedEntry *stats_shmem = + pgstat_get_custom_shmem_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + + /* see explanation above PgStatShared_Archiver for the reset protocol */ + LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE); + pgstat_copy_changecounted_stats(&stats_shmem->reset_offset, + &stats_shmem->stats, + sizeof(stats_shmem->stats), + &stats_shmem->changecount); + stats_shmem->stats.stat_reset_timestamp = ts; + LWLockRelease(&stats_shmem->lock); +} + +/* + * pgstat_custom_fixed_snapshot_cb + * Copy current stats to snapshot area + */ +static void +pgstat_custom_fixed_snapshot_cb(void) +{ + PgStatShared_CustomFixedEntry *stats_shmem = + pgstat_get_custom_shmem_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + PgStat_StatCustomFixedEntry *stat_snap = + pgstat_get_custom_snapshot_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + PgStat_StatCustomFixedEntry *reset_offset = &stats_shmem->reset_offset; + PgStat_StatCustomFixedEntry reset; + + pgstat_copy_changecounted_stats(stat_snap, + &stats_shmem->stats, + sizeof(stats_shmem->stats), + &stats_shmem->changecount); + + LWLockAcquire(&stats_shmem->lock, LW_SHARED); + memcpy(&reset, reset_offset, sizeof(stats_shmem->stats)); + LWLockRelease(&stats_shmem->lock); + + /* Apply reset offsets */ +#define FIXED_COMP(fld) stat_snap->fld -= reset.fld; + FIXED_COMP(numcalls); +#undef FIXED_COMP +} + +/*-------------------------------------------------------------------------- + * SQL-callable functions + *-------------------------------------------------------------------------- + */ + +/* + * pgstat_update_custom_fixed_stats + * Increment call counter + */ +PG_FUNCTION_INFO_V1(pgstat_update_custom_fixed_stats); +Datum +pgstat_update_custom_fixed_stats(PG_FUNCTION_ARGS) +{ + PgStatShared_CustomFixedEntry *stats_shmem; + + stats_shmem = pgstat_get_custom_shmem_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + + LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE); + + pgstat_begin_changecount_write(&stats_shmem->changecount); + stats_shmem->stats.numcalls++; + pgstat_end_changecount_write(&stats_shmem->changecount); + + LWLockRelease(&stats_shmem->lock); + + PG_RETURN_VOID(); +} + +/* + * pgstat_reset_custom_fixed_stats + * Reset statistics by calling pgstat system + */ +PG_FUNCTION_INFO_V1(pgstat_reset_custom_fixed_stats); +Datum +pgstat_reset_custom_fixed_stats(PG_FUNCTION_ARGS) +{ + pgstat_reset_of_kind(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + + PG_RETURN_VOID(); +} + +/* + * pgstat_report_custom_fixed_stats + * Return current counter values + */ +PG_FUNCTION_INFO_V1(pgstat_report_custom_fixed_stats); +Datum +pgstat_report_custom_fixed_stats(PG_FUNCTION_ARGS) +{ + TupleDesc tupdesc; + Datum values[2] = {0}; + bool nulls[2] = {false}; + PgStat_StatCustomFixedEntry *stats; + + /* Take snapshot (applies reset offsets) */ + pgstat_snapshot_fixed(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + stats = pgstat_get_custom_snapshot_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + + /* Build return tuple */ + tupdesc = CreateTemplateTupleDesc(2); + TupleDescInitEntry(tupdesc, (AttrNumber) 1, "numcalls", + INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 2, "stats_reset", + TIMESTAMPTZOID, -1, 0); + BlessTupleDesc(tupdesc); + + values[0] = Int64GetDatum(stats->numcalls); + + /* Handle uninitialized timestamp (no reset yet) */ + if (stats->stat_reset_timestamp == 0) + { + nulls[1] = true; + } + else + { + values[1] = TimestampTzGetDatum(stats->stat_reset_timestamp); + } + + /* Return as tuple */ + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} diff --git a/src/test/modules/test_custom_stats/test_custom_fixed_stats.control b/src/test/modules/test_custom_stats/test_custom_fixed_stats.control new file mode 100644 index 00000000000..b96e2aa18fb --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats.control @@ -0,0 +1,4 @@ +comment = 'Test code for fixed custom stat kinds' +default_version = '1.0' +module_pathname = '$libdir/test_custom_fixed_stats' +relocatable = true diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql b/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql new file mode 100644 index 00000000000..84ae2bf5666 --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql @@ -0,0 +1,25 @@ +/* src/test/modules/test_custom_var_stats/test_custom_var_stats--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_custom_var_stats" to load this file. \quit + +CREATE FUNCTION pgstat_create_custom_var_stats(IN name TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'pgstat_create_custom_var_stats' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION pgstat_update_custom_var_stats(IN name TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'pgstat_update_custom_var_stats' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION pgstat_drop_custom_var_stats(IN name TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'pgstat_drop_custom_var_stats' +LANGUAGE C STRICT PARALLEL UNSAFE; + + +CREATE FUNCTION pgstat_report_custom_var_stats(INOUT name TEXT, OUT calls BIGINT) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pgstat_report_custom_var_stats' +LANGUAGE C STRICT PARALLEL UNSAFE; diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.c b/src/test/modules/test_custom_stats/test_custom_var_stats.c new file mode 100644 index 00000000000..6320eaf2cae --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c @@ -0,0 +1,302 @@ +/*------------------------------------------------------------------------------------ + * + * test_custom_var_stats.c + * Test module for custom PostgreSQL variable-numbered custom statistic kinds + * + * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_custom_var_stats/test_custom_var_stats.c + * + * ------------------------------------------------------------------------------------ + */ +#include "postgres.h" + +#include "common/hashfn.h" +#include "funcapi.h" +#include "utils/builtins.h" +#include "utils/pgstat_internal.h" + +PG_MODULE_MAGIC_EXT( + .name = "test_custom_var_stats", + .version = PG_VERSION +); + +/*-------------------------------------------------------------------------- + * Macros and constants + *-------------------------------------------------------------------------- + */ + +/* + * Kind ID for test_custom_var_stats statistics. + * Reuses the same ID as injection points to avoid reserving a new kind ID. + */ +#define PGSTAT_KIND_TEST_CUSTOM_VAR_STATS 25 + +/* + * Hash statistic name to generate entry index for pgstat lookup. + */ +#define PGSTAT_CUSTOM_VAR_STATS_IDX(name) hash_bytes_extended((const unsigned char *) name, strlen(name), 0) + +/*-------------------------------------------------------------------------- + * Type definitions + *-------------------------------------------------------------------------- + */ + +/* Backend-local pending statistics before flush to shared memory */ +typedef struct PgStat_StatCustomEntry +{ + PgStat_Counter numcalls; /* times statistic was incremented */ +} PgStat_StatCustomEntry; + +/* Shared memory statistics entry visible to all backends */ +typedef struct PgStatShared_CustomEntry +{ + PgStatShared_Common header; /* standard pgstat entry header */ + PgStat_StatCustomEntry stats; /* custom statistics data */ +} PgStatShared_CustomEntry; + +/*-------------------------------------------------------------------------- + * Function prototypes + *-------------------------------------------------------------------------- + */ + +/* Flush callback: merge pending stats into shared memory */ +static bool pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); + +/*-------------------------------------------------------------------------- + * Custom kind configuration + *-------------------------------------------------------------------------- + */ + +static const PgStat_KindInfo custom_stats = { + .name = "test_custom_var_stats", + .fixed_amount = false, /* variable number of entries */ + .write_to_file = true, /* persist across restarts */ + .track_entry_count = true, /* count active entries */ + .accessed_across_databases = true, /* global statistics */ + .shared_size = sizeof(PgStatShared_CustomEntry), + .shared_data_off = offsetof(PgStatShared_CustomEntry, stats), + .shared_data_len = sizeof(((PgStatShared_CustomEntry *) 0)->stats), + .pending_size = sizeof(PgStat_StatCustomEntry), + .flush_pending_cb = pgstat_custom_entry_flush_cb, +}; + +/*-------------------------------------------------------------------------- + * Module initialization + *-------------------------------------------------------------------------- + */ + +void +_PG_init(void) +{ + /* Must be loaded via shared_preload_libraries */ + if (!process_shared_preload_libraries_in_progress) + return; + + /* Register custom statistics kind */ + pgstat_register_kind(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, &custom_stats); +} + +/*-------------------------------------------------------------------------- + * Statistics callback functions + *-------------------------------------------------------------------------- + */ + +/* + * pgstat_custom_entry_flush_cb + * Merge pending backend statistics into shared memory + * + * Called by pgstat collector to flush accumulated local statistics + * to shared memory where other backends can read them. + * + * Returns false only if nowait=true and lock acquisition fails. + */ +static bool +pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) +{ + PgStat_StatCustomEntry *pending_entry; + PgStatShared_CustomEntry *shared_entry; + + pending_entry = (PgStat_StatCustomEntry *) entry_ref->pending; + shared_entry = (PgStatShared_CustomEntry *) entry_ref->shared_stats; + + if (!pgstat_lock_entry(entry_ref, nowait)) + return false; + + /* Add pending counts to shared totals */ + shared_entry->stats.numcalls += pending_entry->numcalls; + + pgstat_unlock_entry(entry_ref); + + return true; +} + +/*-------------------------------------------------------------------------- + * Helper functions + *-------------------------------------------------------------------------- + */ + +/* + * pgstat_fetch_custom_entry + * Look up custom statistic by name + * + * Returns statistics entry from shared memory, or NULL if not found. + */ +static PgStat_StatCustomEntry * +pgstat_fetch_custom_entry(const char *stat_name) +{ + /* Fetch entry by hashed name */ + return (PgStat_StatCustomEntry *) pgstat_fetch_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, + InvalidOid, + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name)); +} + +/*-------------------------------------------------------------------------- + * SQL-callable functions + *-------------------------------------------------------------------------- + */ + +/* + * pgstat_create_custom_var_stats + * Create new custom statistic entry + * + * Initializes a zero-valued statistics entry in shared memory. + * Validates name length against NAMEDATALEN limit. + */ +PG_FUNCTION_INFO_V1(pgstat_create_custom_var_stats); +Datum +pgstat_create_custom_var_stats(PG_FUNCTION_ARGS) +{ + PgStat_EntryRef *entry_ref; + PgStatShared_CustomEntry *shared_entry; + char *stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + + /* Validate name length first */ + if (strlen(stat_name) >= NAMEDATALEN) + ereport(ERROR, + (errcode(ERRCODE_NAME_TOO_LONG), + errmsg("custom statistic name \"%s\" is too long", stat_name), + errdetail("Name must be less than %d characters.", NAMEDATALEN))); + + /* Create or get existing entry */ + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid, + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), true); + + if (!entry_ref) + PG_RETURN_VOID(); + + shared_entry = (PgStatShared_CustomEntry *) entry_ref->shared_stats; + + /* Zero-initialize statistics */ + memset(&shared_entry->stats, 0, sizeof(shared_entry->stats)); + + pgstat_unlock_entry(entry_ref); + + PG_RETURN_VOID(); +} + +/* + * pgstat_update_custom_var_stats + * Increment custom statistic counter + * + * Increments call count in backend-local memory. Changes are flushed + * to shared memory by the statistics collector. + */ +PG_FUNCTION_INFO_V1(pgstat_update_custom_var_stats); +Datum +pgstat_update_custom_var_stats(PG_FUNCTION_ARGS) +{ + char *stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + PgStat_EntryRef *entry_ref; + PgStat_StatCustomEntry *pending_entry; + + /* Get pending entry in local memory */ + entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid, + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), NULL); + + pending_entry = (PgStat_StatCustomEntry *) entry_ref->pending; + pending_entry->numcalls++; + + PG_RETURN_VOID(); +} + +/* + * pgstat_drop_custom_var_stats + * Remove custom statistic entry + * + * Drops the named statistic from shared memory and requests + * garbage collection if needed. + */ +PG_FUNCTION_INFO_V1(pgstat_drop_custom_var_stats); +Datum +pgstat_drop_custom_var_stats(PG_FUNCTION_ARGS) +{ + char *stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + + /* Drop entry and request GC if the entry could not be freed */ + if (!pgstat_drop_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid, + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name))) + pgstat_request_entry_refs_gc(); + + PG_RETURN_VOID(); +} + +/* + * pgstat_report_custom_var_stats + * Retrieve custom statistic values + * + * Returns single row with statistic name and call count if the + * statistic exists, otherwise returns no rows. + */ +PG_FUNCTION_INFO_V1(pgstat_report_custom_var_stats); +Datum +pgstat_report_custom_var_stats(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + char *stat_name; + PgStat_StatCustomEntry *stat_entry; + + if (SRF_IS_FIRSTCALL()) + { + TupleDesc tupdesc; + MemoryContext oldcontext; + + /* Initialize SRF context */ + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + /* Get composite return type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "pgstat_report_custom_var_stats: return type is not composite"); + + funcctx->tuple_desc = BlessTupleDesc(tupdesc); + funcctx->max_calls = 1; /* single row result */ + + MemoryContextSwitchTo(oldcontext); + } + + funcctx = SRF_PERCALL_SETUP(); + + if (funcctx->call_cntr < funcctx->max_calls) + { + Datum values[2]; + bool nulls[2] = {false, false}; + HeapTuple tuple; + + stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + stat_entry = pgstat_fetch_custom_entry(stat_name); + + /* Return row only if entry exists */ + if (stat_entry) + { + values[0] = PointerGetDatum(cstring_to_text(stat_name)); + values[1] = Int64GetDatum(stat_entry->numcalls); + + tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls); + SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple)); + } + } + + SRF_RETURN_DONE(funcctx); +} diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.control b/src/test/modules/test_custom_stats/test_custom_var_stats.control new file mode 100644 index 00000000000..43a2783f965 --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_var_stats.control @@ -0,0 +1,4 @@ +comment = 'Test code for variable custom stat kinds' +default_version = '1.0' +module_pathname = '$libdir/test_custom_var_stats' +relocatable = true -- 2.43.0