commit 0b9b6aa124f97a9c8b0281654d083a3f2644d560 Author: Cédric Villemain Date: Sun Oct 20 14:51:05 2024 +0200 v00 just the counter diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..032560f --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +EXTENSION = pacs +DATA = pacs--0.0.1.sql +REGRESS = pacs +MODULES = pacs + +TAP_TESTS = 1 + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) + +%.man.1: README.%.md + pandoc -s $< -o $@ + @echo man -l $@ + +pgbench: NPROCS=$(shell nproc) +pgbench: NJOBS=$(shell nproc) +pgbench: sql/pgbench.sql + pgbench -f $< -T 10 --progress 1 --no-vacuum \ + -c $$(( $(NPROCS) * 4 )) \ + -j $$(( $(NJOBS) * 4 )) diff --git a/README.pacs.md b/README.pacs.md new file mode 100644 index 0000000..e69de29 diff --git a/expected/pacs.out b/expected/pacs.out new file mode 100644 index 0000000..36d69a7 --- /dev/null +++ b/expected/pacs.out @@ -0,0 +1,152 @@ +CREATE EXTENSION slim; +CREATE EXTENSION pacs; +SELECT pacs_reset_all_measures(); + pacs_reset_all_measures +------------------------- + +(1 row) + +-- create a measure +SELECT pacs_create_measure('A'); + pacs_create_measure +--------------------- + +(1 row) + +-- increment it 3 times +SELECT pacs_increment_measure('A'); + pacs_increment_measure +------------------------ + +(1 row) + +SELECT pacs_increment_measure('A'); + pacs_increment_measure +------------------------ + +(1 row) + +SELECT pacs_increment_measure('A'); + pacs_increment_measure +------------------------ + +(1 row) + +-- check measure value is 3 +SELECT pacs_get_measure('A'); + pacs_get_measure +------------------ + 3 +(1 row) + +-- reset the measure +SELECT pacs_reset_measure('A'); + pacs_reset_measure +-------------------- + +(1 row) + +-- should be 0 now +SELECT pacs_get_measure('A'); + pacs_get_measure +------------------ + 0 +(1 row) + +-- increment again +SELECT pacs_increment_measure('A'); + pacs_increment_measure +------------------------ + +(1 row) + +-- should be 1 +SELECT pacs_get_measure('A'); + pacs_get_measure +------------------ + 1 +(1 row) + +-- and drop the measure +SELECT pacs_drop_measure('A'); + pacs_drop_measure +------------------- + +(1 row) + +-- get stats from pacs (empty) +-- SELECT * FROM pacs_get_stats(); +-- create 2 measures +SELECT pacs_create_measure('B'); + pacs_create_measure +--------------------- + +(1 row) + +SELECT pacs_create_measure('C'); + pacs_create_measure +--------------------- + +(1 row) + +-- expect 2 measures +-- SELECT * FROM pacs_get_stats(); +-- drop one +SELECT pacs_drop_measure('B'); + pacs_drop_measure +------------------- + +(1 row) + +-- so result now is 1 +-- SELECT * FROM pacs_get_stats(); +-- +-- SPECIAL CASES +-- +-- measure A has been dropped already. +-- but it's not erroing, as expected. +SELECT pacs_drop_measure('A'); + pacs_drop_measure +------------------- + +(1 row) + +-- trying to get value for an inexisting measure +-- should ERROR +SELECT pacs_get_measure('absent'); +ERROR: measure "absent" does not exist +HINT: ensure the measure name is correct or $$ select create_measure('absent')$$ to create it +-- reseting an inexsting measure +-- but it's not erroing, as expected +SELECT pacs_reset_measure('absent'); + pacs_reset_measure +-------------------- + +(1 row) + +-- the measure still does not exist +-- so we have an error +SELECT pacs_get_measure('absent'); +ERROR: measure "absent" does not exist +HINT: ensure the measure name is correct or $$ select create_measure('absent')$$ to create it +-- however increment always create the measure if it does not exists +SELECT pacs_increment_measure('absent'); + pacs_increment_measure +------------------------ + +(1 row) + +-- and now we can fetch value (should be 1) +SELECT pacs_get_measure('absent'); + pacs_get_measure +------------------ + 1 +(1 row) + +-- and we drop the "absent" measure (yeah, it's confusing) +SELECT pacs_drop_measure('absent'); + pacs_drop_measure +------------------- + +(1 row) + diff --git a/pacs--0.0.1.sql b/pacs--0.0.1.sql new file mode 100644 index 0000000..bf79a11 --- /dev/null +++ b/pacs--0.0.1.sql @@ -0,0 +1,30 @@ +/* + * measures + */ +CREATE FUNCTION pacs_reset_all_measures() +RETURNS void +AS 'MODULE_PATHNAME', 'pacs_reset_all_measures' +LANGUAGE C STRICT; + +CREATE FUNCTION pacs_reset_measure(measure_name Name) +RETURNS void +AS 'MODULE_PATHNAME', 'pacs_reset_measure' +LANGUAGE C STRICT; + +CREATE FUNCTION pacs_create_measure(measure_name Name) +RETURNS void +AS 'MODULE_PATHNAME', 'pacs_create_measure' +LANGUAGE C STRICT; +CREATE FUNCTION pacs_drop_measure(measure_name Name) +RETURNS void +AS 'MODULE_PATHNAME', 'pacs_drop_measure' +LANGUAGE C STRICT; + +CREATE FUNCTION pacs_increment_measure(measure_name Name) +RETURNS void +AS 'MODULE_PATHNAME', 'pacs_increment_measure' +LANGUAGE C STRICT; +CREATE FUNCTION pacs_get_measure(measure_name Name) +RETURNS bigint +AS 'MODULE_PATHNAME', 'pacs_get_measure' +LANGUAGE C STRICT; diff --git a/pacs.c b/pacs.c new file mode 100644 index 0000000..1ac810e --- /dev/null +++ b/pacs.c @@ -0,0 +1,333 @@ +/*-------------------------------------------------------------------------- + * + * pacs.c + * Code for easier management of "measures". + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "fmgr.h" + +#include "common/hashfn.h" +#include "pgstat.h" +#include "utils/builtins.h" +#include "utils/pgstat_internal.h" + +#include "pacs.h" + +PG_MODULE_MAGIC; + +/* structure for measure */ +typedef struct MeasureStatEntry +{ + PgStat_Counter measure; +} MeasureStatEntry; + +/* boilerplace structure for all measures */ +typedef struct MeasuresStatShared +{ + PgStatShared_Common header; + MeasureStatEntry stats; +} MeasuresStatShared; + +/* boilerplate declaration */ +static bool measure_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); + +/* declare our measures stats */ +static const PgStat_KindInfo measures = { + .name = "measures", + .fixed_amount = false, + + /* Measures are system-wide */ + .accessed_across_databases = true, + + .shared_size = sizeof(MeasuresStatShared), + .shared_data_off = offsetof(MeasuresStatShared, stats), + .shared_data_len = sizeof(((MeasuresStatShared *) 0)->stats), + .pending_size = sizeof(MeasureStatEntry), + .flush_pending_cb = measure_flush_cb, +}; + +/* + * Compute stats entry idx from measure name with an 8-byte hash. + */ +#define PGSTAT_MEASURE_IDX(name) hash_bytes_extended((const unsigned char *) name, strlen(name), 0) + +/* + * Kind ID reserved for statistics of measures. + */ +#define PGSTAT_KIND_MEASURE (PGSTAT_KIND_CUSTOM_MIN + 1) + +/* Track if stats are loaded */ +static bool measures_loaded = false; + +void +_PG_init(void) +{ + if (!process_shared_preload_libraries_in_progress) + return; + + ereport(LOG, + (errmsg("Module pacs is starting"), + errdetail("providing advanced stats structures."))); + pgstat_register_kind(PGSTAT_KIND_MEASURE, &measures); + + /* mark stats as loaded */ + measures_loaded = true; +} + +/* + * Callback for flushing a measure + */ +static bool +measure_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) +{ + MeasureStatEntry *localent; + MeasuresStatShared *shfuncent; + + localent = (MeasureStatEntry *) entry_ref->pending; + shfuncent = (MeasuresStatShared *) entry_ref->shared_stats; + + if (!pgstat_lock_entry(entry_ref, nowait)) + return false; + + shfuncent->stats.measure += localent->measure; + return true; +} + +static void +check_loaded() +{ + if (!measures_loaded) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("measures module not loaded"), + errhint("check shared_preload_libraries"))); +} + +/* + * The following code contains: + * - the exported C functions + * - the SQL API + */ +/* + * Create a measure + */ +PGDLLEXPORT void +pacsCreateMeasure(const char *name) +{ + PgStat_EntryRef *entry_ref; + MeasuresStatShared *shstatent; + + /* error if disabled */ + check_loaded(); + + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_MEASURE, InvalidOid, + PGSTAT_MEASURE_IDX(name), false); + shstatent = (MeasuresStatShared *) entry_ref->shared_stats; + + /* initialize shared memory data */ + memset(&shstatent->stats, 0, sizeof(shstatent->stats)); + pgstat_unlock_entry(entry_ref); +} + +/* + * SQL API + */ +PG_FUNCTION_INFO_V1(pacs_create_measure); +Datum +pacs_create_measure(PG_FUNCTION_ARGS) +{ + Name name; + + name = PG_GETARG_NAME(0); + pacsCreateMeasure(NameStr(*name)); + PG_RETURN_VOID(); +} + +/* + * Drop a measure + * + * XXX register to create, drop to unregister... + */ +PGDLLEXPORT void +pacsDropMeasure(const char *name) +{ + /* error if disabled */ + check_loaded(); + + /* drop and on faillure inform for later garbage collection */ + if (!pgstat_drop_entry(PGSTAT_KIND_MEASURE, InvalidOid, + PGSTAT_MEASURE_IDX(name))) + pgstat_request_entry_refs_gc(); +} + +PG_FUNCTION_INFO_V1(pacs_drop_measure); +Datum +pacs_drop_measure(PG_FUNCTION_ARGS) +{ + Name name; + + name = PG_GETARG_NAME(0); + pacsDropMeasure(NameStr(*name)); + PG_RETURN_VOID(); +} + +/* + * Increment a measure + * + * By design, PostgreSQL does create the measure if absent! + * + * we do not manage ereport() here to not add overhead. + * XXX is it accurate ? + */ +PGDLLEXPORT void +pacsIncrementMeasure(const char *name) +{ + PgStat_EntryRef *entry_ref; + MeasuresStatShared *shstatent; + MeasureStatEntry *statent; + + /* quick leave if disabled */ + if (!measures_loaded) + return; + + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_MEASURE, InvalidOid, + PGSTAT_MEASURE_IDX(name), false); + + shstatent = (MeasuresStatShared *) entry_ref->shared_stats; + statent = &shstatent->stats; + + /* Update the measure */ + statent->measure++; + + pgstat_unlock_entry(entry_ref); +} + +PG_FUNCTION_INFO_V1(pacs_increment_measure); +Datum +pacs_increment_measure(PG_FUNCTION_ARGS) +{ + Name name; + + name = PG_GETARG_NAME(0); + pacsIncrementMeasure(NameStr(*name)); + PG_RETURN_VOID(); +} + +/* + * Support function for the SQL-callable functions. Returns + * a pointer to the measure struct. + */ +static MeasureStatEntry * +pacsGetMeasure(const char *name) +{ + MeasureStatEntry *entry = NULL; + + /* error if disabled */ + check_loaded(); + + /* Compile the lookup key as a hash of the measure name */ + entry = (MeasureStatEntry *) pgstat_fetch_entry(PGSTAT_KIND_MEASURE, + InvalidOid, + PGSTAT_MEASURE_IDX(name)); + return entry; +} + +/* + * SQL function returning the measure current value. + */ +PG_FUNCTION_INFO_V1(pacs_get_measure); +Datum +pacs_get_measure(PG_FUNCTION_ARGS) +{ + MeasureStatEntry *entry; + Name name; + + name = PG_GETARG_NAME(0); + + entry = pacsGetMeasure(NameStr(*name)); + if (entry == NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("measure \"%s\" does not exist", + NameStr(*name)), + errhint("ensure the measure name is correct or " + "$$ select create_measure('%s')$$ to create it", + NameStr(*name)))); + + PG_RETURN_INT64(entry->measure); +} + +/* + * Reset a measure + */ +PGDLLEXPORT void +pacsResetMeasure(const char *name) +{ + /* error if disabled */ + check_loaded(); + pgstat_reset_entry(PGSTAT_KIND_MEASURE, InvalidOid, + PGSTAT_MEASURE_IDX(name), 0); + return; +} + +/* + * SQL function reseting the measure(s) + */ +PG_FUNCTION_INFO_V1(pacs_reset_measure); +Datum +pacs_reset_measure(PG_FUNCTION_ARGS) +{ + Name name; + name = PG_GETARG_NAME(0); + pacsResetMeasure(NameStr(*name)); + PG_RETURN_VOID(); +} + +/* + * Reset a measure + */ +PGDLLEXPORT void +pacsResetAllMeasures(void) +{ + /* error if disabled */ + check_loaded(); + pgstat_reset_of_kind(PGSTAT_KIND_MEASURE); + return; +} + +PG_FUNCTION_INFO_V1(pacs_reset_all_measures); +Datum +pacs_reset_all_measures(PG_FUNCTION_ARGS) +{ + pacsResetAllMeasures(); + PG_RETURN_VOID(); +} + +// PG_FUNCTION_INFO_V1(pacs_list_all_measures); +// PGDLLEXPORT void +// pacs_list_all_measures(PgStat_Kind kind) +// { +// HASH_SEQ_STATUS status; +// PgStat_EntryRef *entry_ref; +// +// const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); +// if (!kind_info || !kind_info->hash_table) +// { +// elog(ERROR, "Invalid or unsupported statistics kind."); +// return; +// } +// +// hash_seq_init(&status, kind_info->hash_table); +// +// while ((entry_ref = hash_seq_search(&status)) != NULL) +// { +// elog(INFO, "Key for kind %d: %u/%llu", kind, +// entry_ref->key.dboid, (unsigned long long)entry_ref->key.objid); +// } +// } diff --git a/pacs.control b/pacs.control new file mode 100644 index 0000000..3edb45b --- /dev/null +++ b/pacs.control @@ -0,0 +1,5 @@ +comment = 'Provides PostgresSQL Advanced Cumulative Statistics' +default_version = '0.0.1' +relocatable = true +module_pathname = '$libdir/pacs' +requires = 'slim' \ No newline at end of file diff --git a/pacs.h b/pacs.h new file mode 100644 index 0000000..292a528 --- /dev/null +++ b/pacs.h @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + * + * pacs.h + * Definitions for easier statistics management. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * ------------------------------------------------------------------------- + */ + +#ifndef PACS +#define PACS + +extern void RegisterMeasuresInPgStat(void); + +/* creating/dropping stats */ +extern void pacsCreateMeasure(const char *name); +extern void pacsDropMeasure(const char *name); + +/* working with stats content */ +extern void pacsIncrementMeasure(const char *name); +extern void pacsResetAllMeasures(void); +extern void pacsResetMeasure(const char *name); + +/* statsmgr_stats.c */ +extern void RegisterPacsInPgStat(void); + +#endif diff --git a/sql/pacs.sql b/sql/pacs.sql new file mode 100644 index 0000000..5b02028 --- /dev/null +++ b/sql/pacs.sql @@ -0,0 +1,59 @@ +CREATE EXTENSION slim; +CREATE EXTENSION pacs; + +SELECT pacs_reset_all_measures(); + +-- create a measure +SELECT pacs_create_measure('A'); +-- increment it 3 times +SELECT pacs_increment_measure('A'); +SELECT pacs_increment_measure('A'); +SELECT pacs_increment_measure('A'); +-- check measure value is 3 +SELECT pacs_get_measure('A'); +-- reset the measure +SELECT pacs_reset_measure('A'); +-- should be 0 now +SELECT pacs_get_measure('A'); +-- increment again +SELECT pacs_increment_measure('A'); +-- should be 1 +SELECT pacs_get_measure('A'); +-- and drop the measure +SELECT pacs_drop_measure('A'); + +-- get stats from pacs (empty) +-- SELECT * FROM pacs_get_stats(); +-- create 2 measures +SELECT pacs_create_measure('B'); +SELECT pacs_create_measure('C'); +-- expect 2 measures +-- SELECT * FROM pacs_get_stats(); +-- drop one +SELECT pacs_drop_measure('B'); +-- so result now is 1 +-- SELECT * FROM pacs_get_stats(); + +-- +-- SPECIAL CASES +-- +-- measure A has been dropped already. +-- but it's not erroing, as expected. +SELECT pacs_drop_measure('A'); + +-- trying to get value for an inexisting measure +-- should ERROR +SELECT pacs_get_measure('absent'); + +-- reseting an inexsting measure +-- but it's not erroing, as expected +SELECT pacs_reset_measure('absent'); +-- the measure still does not exist +-- so we have an error +SELECT pacs_get_measure('absent'); +-- however increment always create the measure if it does not exists +SELECT pacs_increment_measure('absent'); +-- and now we can fetch value (should be 1) +SELECT pacs_get_measure('absent'); +-- and we drop the "absent" measure (yeah, it's confusing) +SELECT pacs_drop_measure('absent'); diff --git a/sql/pgbench.sql b/sql/pgbench.sql new file mode 100644 index 0000000..e69de29