injection_points: Add some cumulative stats for injection points

This acts as a template of what can be achieved with the pluggable
cumulative stats APIs introduced in 7949d95945 for the
variable-numbered case where stats entries are stored in the pgstats
dshash, while being potentially useful on its own for injection points,
say to add starting and/or stopping conditions based on the statistics
(want to trigger a callback after N calls, for example?).

Currently, the only data gathered is the number of times an injection
point is run.  More fields can always be added as required.  All the
routines related to the stats are located in their own file, called
injection_stats.c in the test module injection_points, for clarity.

The stats can be used only if the test module is loaded through
shared_preload_libraries.  The key of the dshash uses InvalidOid for the
database, and an int4 hash of the injection point name as object ID.

A TAP test is added to provide coverage for the new custom cumulative
stats APIs, showing the persistency of the data across restarts, for
example.

Author: Michael Paquier
Reviewed-by: Dmitry Dolgov, Bertrand Drouvot
Discussion: https://postgr.es/m/Zmqm9j5EO0I4W8dx@paquier.xyz
This commit is contained in:
Michael Paquier 2024-08-05 12:06:54 +09:00
parent 2eff9e678d
commit 75534436a4
9 changed files with 332 additions and 2 deletions

View File

@ -3919,6 +3919,11 @@ extern PgStat_Kind pgstat_add_kind(PgStat_Kind kind,
type in <xref linkend="guc-shared-preload-libraries"/> so that it will
be loaded early during <productname>PostgreSQL</productname> startup.
</para>
<para>
An example describing how to register and use custom statistics can be
found in <filename>src/test/modules/injection_points</filename>.
</para>
</sect2>
<sect2 id="extend-cpp">

View File

@ -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"
@ -11,9 +14,13 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
ISOLATION = inplace
TAP_TESTS = 1
# The injection points are cluster-wide, so disable installcheck
NO_INSTALLCHECK = 1
export enable_injection_points enable_injection_points
ifdef USE_PGXS
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)

View File

@ -74,3 +74,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;

View File

@ -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();
}
@ -431,5 +445,18 @@ injection_points_detach(PG_FUNCTION_ARGS)
MemoryContextSwitchTo(oldctx);
}
/* Remove stats entry */
pgstat_drop_inj(name);
PG_RETURN_VOID();
}
void
_PG_init(void)
{
if (!process_shared_preload_libraries_in_progress)
return;
pgstat_register_inj();
}

View File

@ -0,0 +1,197 @@
/*--------------------------------------------------------------------------
*
* 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))
/*
* Kind ID reserved for statistics of injection points.
*/
#define PGSTAT_KIND_INJECTION 129
/* 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;
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)
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)
return;
entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_INJECTION, 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)
{
/* leave if disabled */
if (!inj_stats_loaded)
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;
PgStatShared_InjectionPoint *shstatent;
PgStat_StatInjEntry *statent;
/* leave if disabled */
if (!inj_stats_loaded)
return;
entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_INJECTION, 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);
}

View File

@ -0,0 +1,23 @@
/*--------------------------------------------------------------------------
*
* 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_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);
#endif

View File

@ -6,6 +6,7 @@ endif
injection_points_sources = files(
'injection_points.c',
'injection_stats.c',
)
if host_system == 'windows'
@ -42,4 +43,12 @@ tests += {
'inplace',
],
},
'tap': {
'env': {
'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
},
'tests': [
't/001_stats.pl',
],
},
}

View File

@ -0,0 +1,50 @@
# Copyright (c) 2024, 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',
"shared_preload_libraries = 'injection_points'");
$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');
# 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, '2', 'number of 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 clean restart');
done_testing();

View File

@ -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