diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml index 9cfa5bef36c..8e936af465c 100644 --- a/doc/src/sgml/xfunc.sgml +++ b/doc/src/sgml/xfunc.sgml @@ -3933,6 +3933,12 @@ extern bool InjectionPointDetach(const char *name); using InjectionPointDetach. + + An example can be found in + src/test/modules/test_custom_stats in the PostgreSQL + source tree. + + Enabling injection points requires with 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/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..324020d061a --- /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 - custom pgstats" + +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..f6b3f5c45a6 --- /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 - variable-sized custom pgstats',]) +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 - fixed-sized custom pgstats',]) +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..e528595cfb0 --- /dev/null +++ b/src/test/modules/test_custom_stats/t/001_custom_stats.pl @@ -0,0 +1,139 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test custom pgstats functionality +# +# This script includes tests for both variable and fixed-sized custom +# pgstats: +# - Creation, updates, and reporting. +# - Persistence across restarts. +# - Loss after crash recovery. +# - Resets for fixed-sized 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 entries for variable-sized stats. +$node->safe_psql('postgres', + q(select test_custom_stats_var_create('entry1'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_create('entry2'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_create('entry3'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_create('entry4'))); + +# Update counters: entry1=2, entry2=3, entry3=2, entry4=3, fixed=3 +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry1'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry1'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry2'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry2'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry2'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry3'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry3'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry4'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry4'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry4'))); +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); + +# Test data reports. +my $result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry1'))); +is($result, "entry1|2", "report for variable-sized data of entry1"); +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry2'))); +is($result, "entry2|3", "report for variable-sized data of entry2"); +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry3'))); +is($result, "entry3|2", "report for variable-sized data of entry3"); +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry4'))); +is($result, "entry4|3", "report for variable-sized data of entry4"); +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_fixed_report())); +is($result, "3|", "report for fixed-sized stats"); + +# Test drop of variable-sized stats. +$node->safe_psql('postgres', + q(select * from test_custom_stats_var_drop('entry3'))); +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry3'))); +is($result, "", "entry3 not found after drop"); +$node->safe_psql('postgres', + q(select * from test_custom_stats_var_drop('entry4'))); +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('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 test_custom_stats_var_report('entry1'))); +is($result, "entry1|2", "variable-sized stats persist after clean restart"); +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_fixed_report())); +is($result, "3|", "fixed-sized stats persist after clean restart"); + +# Test persistence after crash recovery. +$node->stop('immediate'); +$node->start; + +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry1'))); +is($result, "", "variable-sized stats of entry1 lost after crash recovery"); +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry2'))); +is($result, "", "variable-sized stats of entry2 lost after crash recovery"); + +# Crash recovery sets the reset timestamp. +$result = $node->safe_psql('postgres', + q(select numcalls from test_custom_stats_fixed_report() where stats_reset is not null) +); +is($result, "0", "fixed-sized stats are reset after crash recovery"); + +# Test reset of fixed-sized stats. +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); + +$result = $node->safe_psql('postgres', + q(select numcalls from test_custom_stats_fixed_report())); +is($result, "3", "report of fixed-sized before manual reset"); + +$node->safe_psql('postgres', q(select test_custom_stats_fixed_reset())); + +$result = $node->safe_psql('postgres', + q(select numcalls from test_custom_stats_fixed_report() where stats_reset is not null) +); +is($result, "0", "report of fixed-sized after manual reset"); + +# Test completed successfully +done_testing(); 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..69a93b5241f --- /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 test_custom_stats_fixed_update() +RETURNS void +AS 'MODULE_PATHNAME', 'test_custom_stats_fixed_update' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION test_custom_stats_fixed_report(OUT numcalls bigint, + OUT stats_reset timestamptz) +RETURNS record +AS 'MODULE_PATHNAME', 'test_custom_stats_fixed_report' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION test_custom_stats_fixed_reset() +RETURNS void +AS 'MODULE_PATHNAME', 'test_custom_stats_fixed_reset' +LANGUAGE C STRICT PARALLEL UNSAFE; 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..bdca267a6df --- /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-sized custom pgstats + * + * Copyright (c) 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 test_custom_stats_fixed_init_shmem_cb(void *stats); +static void test_custom_stats_fixed_reset_all_cb(TimestampTz ts); +static void test_custom_stats_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 = test_custom_stats_fixed_init_shmem_cb, + .reset_all_cb = test_custom_stats_fixed_reset_all_cb, + .snapshot_cb = test_custom_stats_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); +} + +/* + * test_custom_stats_fixed_init_shmem_cb + * Initialize shared memory structure + */ +static void +test_custom_stats_fixed_init_shmem_cb(void *stats) +{ + PgStatShared_CustomFixedEntry *stats_shmem = + (PgStatShared_CustomFixedEntry *) stats; + + LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA); +} + +/* + * test_custom_stats_fixed_reset_all_cb + * Reset the fixed-sized stats + */ +static void +test_custom_stats_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); +} + +/* + * test_custom_stats_fixed_snapshot_cb + * Copy current stats to snapshot area + */ +static void +test_custom_stats_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 + *-------------------------------------------------------------------------- + */ + +/* + * test_custom_stats_fixed_update + * Increment call counter + */ +PG_FUNCTION_INFO_V1(test_custom_stats_fixed_update); +Datum +test_custom_stats_fixed_update(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(); +} + +/* + * test_custom_stats_fixed_reset + * Reset statistics by calling pgstat system + */ +PG_FUNCTION_INFO_V1(test_custom_stats_fixed_reset); +Datum +test_custom_stats_fixed_reset(PG_FUNCTION_ARGS) +{ + pgstat_reset_of_kind(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + + PG_RETURN_VOID(); +} + +/* + * test_custom_stats_fixed_report + * Return current counter values + */ +PG_FUNCTION_INFO_V1(test_custom_stats_fixed_report); +Datum +test_custom_stats_fixed_report(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..3e80cc24e6b --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats.control @@ -0,0 +1,4 @@ +comment = 'fixed-sized custom pgstats' +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..d5f82b5d546 --- /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 test_custom_stats_var_create(IN name TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'test_custom_stats_var_create' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION test_custom_stats_var_update(IN name TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'test_custom_stats_var_update' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION test_custom_stats_var_drop(IN name TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'test_custom_stats_var_drop' +LANGUAGE C STRICT PARALLEL UNSAFE; + + +CREATE FUNCTION test_custom_stats_var_report(INOUT name TEXT, OUT calls BIGINT) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'test_custom_stats_var_report' +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..d4905ab4ee9 --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c @@ -0,0 +1,303 @@ +/*------------------------------------------------------------------------------------ + * + * test_custom_var_stats.c + * Test module for variable-sized custom pgstats + * + * Copyright (c) 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. + */ +#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_StatCustomVarEntry +{ + PgStat_Counter numcalls; /* times statistic was incremented */ +} PgStat_StatCustomVarEntry; + +/* Shared memory statistics entry visible to all backends */ +typedef struct PgStatShared_CustomVarEntry +{ + PgStatShared_Common header; /* standard pgstat entry header */ + PgStat_StatCustomVarEntry stats; /* custom statistics data */ +} PgStatShared_CustomVarEntry; + +/*-------------------------------------------------------------------------- + * Function prototypes + *-------------------------------------------------------------------------- + */ + +/* Flush callback: merge pending stats into shared memory */ +static bool test_custom_stats_var_flush_pending_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_CustomVarEntry), + .shared_data_off = offsetof(PgStatShared_CustomVarEntry, stats), + .shared_data_len = sizeof(((PgStatShared_CustomVarEntry *) 0)->stats), + .pending_size = sizeof(PgStat_StatCustomVarEntry), + .flush_pending_cb = test_custom_stats_var_flush_pending_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 + *-------------------------------------------------------------------------- + */ + +/* + * test_custom_stats_var_flush_pending_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 +test_custom_stats_var_flush_pending_cb(PgStat_EntryRef *entry_ref, bool nowait) +{ + PgStat_StatCustomVarEntry *pending_entry; + PgStatShared_CustomVarEntry *shared_entry; + + pending_entry = (PgStat_StatCustomVarEntry *) entry_ref->pending; + shared_entry = (PgStatShared_CustomVarEntry *) 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 + *-------------------------------------------------------------------------- + */ + +/* + * test_custom_stats_var_fetch_entry + * Look up custom statistic by name + * + * Returns statistics entry from shared memory, or NULL if not found. + */ +static PgStat_StatCustomVarEntry * +test_custom_stats_var_fetch_entry(const char *stat_name) +{ + /* Fetch entry by hashed name */ + return (PgStat_StatCustomVarEntry *) + pgstat_fetch_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, + InvalidOid, + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name)); +} + +/*-------------------------------------------------------------------------- + * SQL-callable functions + *-------------------------------------------------------------------------- + */ + +/* + * test_custom_stats_var_create + * Create new custom statistic entry + * + * Initializes a zero-valued statistics entry in shared memory. + * Validates name length against NAMEDATALEN limit. + */ +PG_FUNCTION_INFO_V1(test_custom_stats_var_create); +Datum +test_custom_stats_var_create(PG_FUNCTION_ARGS) +{ + PgStat_EntryRef *entry_ref; + PgStatShared_CustomVarEntry *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_CustomVarEntry *) entry_ref->shared_stats; + + /* Zero-initialize statistics */ + memset(&shared_entry->stats, 0, sizeof(shared_entry->stats)); + + pgstat_unlock_entry(entry_ref); + + PG_RETURN_VOID(); +} + +/* + * test_custom_stats_var_update + * 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(test_custom_stats_var_update); +Datum +test_custom_stats_var_update(PG_FUNCTION_ARGS) +{ + char *stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + PgStat_EntryRef *entry_ref; + PgStat_StatCustomVarEntry *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_StatCustomVarEntry *) entry_ref->pending; + pending_entry->numcalls++; + + PG_RETURN_VOID(); +} + +/* + * test_custom_stats_var_drop + * Remove custom statistic entry + * + * Drops the named statistic from shared memory and requests + * garbage collection if needed. + */ +PG_FUNCTION_INFO_V1(test_custom_stats_var_drop); +Datum +test_custom_stats_var_drop(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(); +} + +/* + * test_custom_stats_var_report + * 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(test_custom_stats_var_report); +Datum +test_custom_stats_var_report(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + char *stat_name; + PgStat_StatCustomVarEntry *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, "test_custom_stats_var_report: 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 = test_custom_stats_var_fetch_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..bea2097a545 --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_var_stats.control @@ -0,0 +1,4 @@ +comment = 'variable-sized custom pgstats' +default_version = '1.0' +module_pathname = '$libdir/test_custom_var_stats' +relocatable = true diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 205b282f6b4..6e2ed0c8825 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2226,6 +2226,8 @@ PgStatShared_Backend PgStatShared_BgWriter PgStatShared_Checkpointer PgStatShared_Common +PgStatShared_CustomFixedEntry +PgStatShared_CustomVarEntry PgStatShared_Database PgStatShared_Function PgStatShared_HashEntry @@ -2258,6 +2260,8 @@ PgStat_SLRUStats PgStat_ShmemControl PgStat_Snapshot PgStat_SnapshotEntry +PgStat_StatCustomFixedEntry +PgStat_StatCustomVarEntry PgStat_StatDBEntry PgStat_StatFuncEntry PgStat_StatReplSlotEntry