From 87d34f2c618798efb37f7e225a753d1e3c69b11c Mon Sep 17 00:00:00 2001 From: Sami Imseih Date: Mon, 10 Nov 2025 00:03:41 -0600 Subject: [PATCH v4 2/2] Allow cumulative statistics to serialize auxiliary data to disk. Cumulative Statistics kinds can now write additional per-entry data to the statistics file that doesn't fit in shared memory. This is useful for statistics with variable-length auxiliary data. Three new optional callbacks are added to PgStat_KindInfo: * to_serialized_extra_stats: writes auxiliary data for an entry * from_serialized_extra_stats: reads auxiliary data for an entry * end_extra_stats: performs cleanup after read/write/discard operations All three callbacks must be provided together to ensure the reader consumes exactly what the writer produces. The end_extra_stats callback is invoked after processing all entries of a kind, allowing extensions to close file handles and clean up resources. Tests are also added to test_custom_stats. Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0s9SDOu+Z6veoJCHWk+kDeTktAtC-KY9fQ9Z6BJdDUirQ@mail.gmail.com --- src/backend/utils/activity/pgstat.c | 81 ++++- src/include/utils/pgstat_internal.h | 37 ++ .../test_custom_stats/t/001_custom_stats.pl | 20 +- .../test_custom_var_stats--1.0.sql | 6 +- .../test_custom_stats/test_custom_var_stats.c | 327 +++++++++++++++++- 5 files changed, 447 insertions(+), 24 deletions(-) diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index 8713c7a0483..8804c1688c5 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -194,6 +194,7 @@ static void pgstat_build_snapshot(void); static void pgstat_build_snapshot_fixed(PgStat_Kind kind); static inline bool pgstat_is_kind_valid(PgStat_Kind kind); +static inline void pgstat_check_extra_callbacks(const PgStat_KindInfo *kind_info, int elevel); /* ---------- @@ -523,6 +524,7 @@ pgstat_discard_stats(void) /* NB: this needs to be done even in single user mode */ + /* First, cleanup the main stats file, PGSTAT_STAT_PERMANENT_FILENAME */ ret = unlink(PGSTAT_STAT_PERMANENT_FILENAME); if (ret != 0) { @@ -544,6 +546,15 @@ pgstat_discard_stats(void) PGSTAT_STAT_PERMANENT_FILENAME))); } + /* Let each stats kind run its cleanup callback, if it provides one */ + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + + if (kind_info && kind_info->end_extra_stats) + kind_info->end_extra_stats(STATS_DISCARD); + } + /* * Reset stats contents. This will set reset timestamps of fixed-numbered * stats to the current time (no variable stats exist). @@ -645,6 +656,10 @@ pgstat_initialize(void) pgstat_attach_shmem(); + /* Check a kind's extra-data callback setup */ + for (PgStat_Kind kind = PGSTAT_KIND_BUILTIN_MIN; kind <= PGSTAT_KIND_BUILTIN_MAX; kind++) + pgstat_check_extra_callbacks(&pgstat_kind_builtin_infos[kind], PANIC); + pgstat_init_snapshot_fixed(); /* Backend initialization callbacks */ @@ -1432,6 +1447,32 @@ pgstat_is_kind_valid(PgStat_Kind kind) return pgstat_is_kind_builtin(kind) || pgstat_is_kind_custom(kind); } +/* + * Validate that extra stats callbacks are all provided together or not at all. + * Reports error at specified level if validation fails. + */ +static inline void +pgstat_check_extra_callbacks(const PgStat_KindInfo *kind_info, int elevel) +{ + bool has_extra; + + has_extra = + kind_info->to_serialized_extra_stats || + kind_info->from_serialized_extra_stats || + kind_info->end_extra_stats; + + if (has_extra && + (!kind_info->to_serialized_extra_stats || + !kind_info->from_serialized_extra_stats || + !kind_info->end_extra_stats)) + { + ereport(elevel, + (errmsg("incomplete serialization callbacks for statistics kind \"%s\"", + kind_info->name), + errdetail("callbacks to_serialized_extra_stats, from_serialized_extra_stats, and end_extra_stats must be provided together."))); + } +} + const PgStat_KindInfo * pgstat_get_kind_info(PgStat_Kind kind) { @@ -1525,6 +1566,9 @@ pgstat_register_kind(PgStat_Kind kind, const PgStat_KindInfo *kind_info) errdetail("Existing cumulative statistics with ID %u has the same name.", existing_kind))); } + /* Check a kind's extra-data callback setup */ + pgstat_check_extra_callbacks(kind_info, ERROR); + /* Register it */ pgstat_kind_custom_infos[idx] = kind_info; ereport(LOG, @@ -1702,6 +1746,9 @@ pgstat_write_statsfile(void) pgstat_write_chunk(fpout, pgstat_get_entry_data(ps->key.kind, shstats), pgstat_get_entry_len(ps->key.kind)); + + if (kind_info->to_serialized_extra_stats) + kind_info->to_serialized_extra_stats(&ps->key, shstats, fpout); } dshash_seq_term(&hstat); @@ -1734,6 +1781,15 @@ pgstat_write_statsfile(void) /* durable_rename already emitted log message */ unlink(tmpfile); } + + /* Now, allow kinds to finalize the writes for the extra files */ + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + + if (kind_info && kind_info->end_extra_stats) + kind_info->end_extra_stats(STATS_WRITE); + } } /* helper for pgstat_read_statsfile() */ @@ -1871,6 +1927,7 @@ pgstat_read_statsfile(void) PgStat_HashKey key; PgStatShared_HashEntry *p; PgStatShared_Common *header; + const PgStat_KindInfo *kind_info = NULL; CHECK_FOR_INTERRUPTS(); @@ -1891,7 +1948,8 @@ pgstat_read_statsfile(void) goto error; } - if (!pgstat_get_kind_info(key.kind)) + kind_info = pgstat_get_kind_info(key.kind); + if (!kind_info) { elog(WARNING, "could not find information of kind for entry %u/%u/%" PRIu64 " of type %c", key.kind, key.dboid, @@ -1902,7 +1960,6 @@ pgstat_read_statsfile(void) else { /* stats entry identified by name on disk (e.g. slots) */ - const PgStat_KindInfo *kind_info = NULL; PgStat_Kind kind; NameData name; @@ -1996,6 +2053,16 @@ pgstat_read_statsfile(void) goto error; } + if (kind_info->from_serialized_extra_stats) + { + if (!kind_info->from_serialized_extra_stats(&key, header, fpin)) + { + elog(WARNING, "could not read extra stats for entry %u/%u/%" PRIu64, + key.kind, key.dboid, key.objid); + goto error; + } + } + break; } case PGSTAT_FILE_ENTRY_END: @@ -2019,11 +2086,21 @@ pgstat_read_statsfile(void) } done: + /* First, cleanup the main stats file, PGSTAT_STAT_PERMANENT_FILENAME */ FreeFile(fpin); elog(DEBUG2, "removing permanent stats file \"%s\"", statfile); unlink(statfile); + /* Let each stats kind run its cleanup callback, if it provides one */ + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + + if (kind_info && kind_info->end_extra_stats) + kind_info->end_extra_stats(STATS_READ); + } + return; error: diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index ca1ba6420ca..48b40816570 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -63,6 +63,20 @@ typedef struct PgStat_HashKey * identifier. */ } PgStat_HashKey; +/* + * Tracks if the stats file is being read, written or discarded. + * + * These states allow plugins that create extra statistics files + * to determine the current operation and perform any necessary + * file cleanup. + */ +typedef enum PgStat_StatsFileOp +{ + STATS_WRITE, + STATS_READ, + STATS_DISCARD, +} PgStat_StatsFileOp; + /* * PgStat_HashKey should not have any padding. Checking that the structure * size matches with the sum of each field is a check simple enough to @@ -303,6 +317,29 @@ typedef struct PgStat_KindInfo const PgStatShared_Common *header, NameData *name); bool (*from_serialized_name) (const NameData *name, PgStat_HashKey *key); + /* + * Optional callbacks for kinds that write additional per-entry data to + * the stats file. If any of these callbacks are provided, all three must + * be provided to ensure that the reader consumes exactly the data written + * by the writer. + * + * to_serialized_extra_stats: write extra data for an entry. + * + * from_serialized_extra_stats: read the extra data for an entry. Returns + * true on success, false on read error. + * + * end_extra_stats: invoked once per operation (read, write, discard) + * after all entries of this kind have been processed. + * + * Note: statfile is a pointer to the main stats file, + * PGSTAT_STAT_PERMANENT_FILENAME. + */ + void (*to_serialized_extra_stats) (const PgStat_HashKey *key, + const PgStatShared_Common *header, FILE *statfile); + bool (*from_serialized_extra_stats) (const PgStat_HashKey *key, + const PgStatShared_Common *header, FILE *statfile); + void (*end_extra_stats) (PgStat_StatsFileOp status); + /* * For fixed-numbered statistics: Initialize shared memory state. * 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 index c4fceb7d267..55a8956a0d9 100644 --- a/src/test/modules/test_custom_stats/t/001_custom_stats.pl +++ b/src/test/modules/test_custom_stats/t/001_custom_stats.pl @@ -26,10 +26,10 @@ $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'))); +$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry1', 'Test entry 1'))); +$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry2', 'Test entry 2'))); +$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry3', 'Test entry 3'))); +$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry4', 'Test entry 4'))); # Update counters: entry1=2, entry2=3, entry3=2, entry4=3, fixed=3 $node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry1'))); @@ -48,16 +48,16 @@ $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"); +is($result, "entry1|2|Test entry 1", "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"); +is($result, "entry2|3|Test entry 2", "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"); +is($result, "entry3|2|Test entry 3", "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"); +is($result, "entry4|3|Test entry 4", "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"); @@ -76,10 +76,10 @@ $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"); +is($result, "entry1|2|Test entry 1", "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"); +is($result, "entry2|3|Test entry 2", "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"); 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 index 84ae2bf5666..c509567de7d 100644 --- 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 @@ -3,7 +3,7 @@ -- 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) +CREATE FUNCTION pgstat_create_custom_var_stats(IN name TEXT, in description TEXT) RETURNS void AS 'MODULE_PATHNAME', 'pgstat_create_custom_var_stats' LANGUAGE C STRICT PARALLEL UNSAFE; @@ -19,7 +19,9 @@ 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) +CREATE FUNCTION pgstat_report_custom_var_stats(INOUT name TEXT, + OUT calls BIGINT, + OUT description TEXT) 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 index 6320eaf2cae..6d755904df5 100644 --- a/src/test/modules/test_custom_stats/test_custom_var_stats.c +++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c @@ -14,6 +14,7 @@ #include "common/hashfn.h" #include "funcapi.h" +#include "storage/dsm_registry.h" #include "utils/builtins.h" #include "utils/pgstat_internal.h" @@ -33,6 +34,9 @@ PG_MODULE_MAGIC_EXT( */ #define PGSTAT_KIND_TEST_CUSTOM_VAR_STATS 25 +/* File paths for extra statistics data serialization */ +#define PGSTAT_CUSTOM_EXTRA_DATA_DESC "pg_stat/test_custom_var_stats_desc.stats" + /* * Hash statistic name to generate entry index for pgstat lookup. */ @@ -54,15 +58,41 @@ typedef struct PgStatShared_CustomEntry { PgStatShared_Common header; /* standard pgstat entry header */ PgStat_StatCustomEntry stats; /* custom statistics data */ + dsa_pointer description; /* extra statistics data */ } PgStatShared_CustomEntry; +/*-------------------------------------------------------------------------- + * Global Variables + *-------------------------------------------------------------------------- + */ + +/* File handle for extra statistics data serialization */ +static FILE *fd_description = NULL; + +/* Current write offset in fd_description file */ +static long fd_description_offset = 0; + +/* DSA area for storing variable-length description strings */ +dsa_area *custom_stats_description_dsa = NULL; + /*-------------------------------------------------------------------------- * Function prototypes *-------------------------------------------------------------------------- */ /* Flush callback: merge pending stats into shared memory */ -static bool pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); +static bool pgstat_custom_var_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); + +/* Serialization callback: serialize extra statistics data */ +static void pgstat_custom_var_stats_serialize(const PgStat_HashKey *key, + const PgStatShared_Common *header, FILE *statfile); + +/* Deserialization callback: deserialize extra statistics data */ +static bool pgstat_custom_var_stats_deserialize(const PgStat_HashKey *key, + const PgStatShared_Common *header, FILE *statfile); + +/* Cleanup callback: end of statistics file operations */ +static void pgstat_custom_var_stats_file_cleanup(PgStat_StatsFileOp status); /*-------------------------------------------------------------------------- * Custom kind configuration @@ -79,7 +109,10 @@ static const PgStat_KindInfo custom_stats = { .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, + .flush_pending_cb = pgstat_custom_var_entry_flush_cb, + .to_serialized_extra_stats = pgstat_custom_var_stats_serialize, + .from_serialized_extra_stats = pgstat_custom_var_stats_deserialize, + .end_extra_stats = pgstat_custom_var_stats_file_cleanup, }; /*-------------------------------------------------------------------------- @@ -113,7 +146,7 @@ _PG_init(void) * Returns false only if nowait=true and lock acquisition fails. */ static bool -pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) +pgstat_custom_var_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) { PgStat_StatCustomEntry *pending_entry; PgStatShared_CustomEntry *shared_entry; @@ -132,6 +165,234 @@ pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) return true; } +/* + * pgstat_custom_var_stats_serialize() - + * + * Serialize extra data (descriptions) for custom statistics entries to + * the statistics file. Called during statistics file writing to preserve + * description strings across restarts. + */ +static void +pgstat_custom_var_stats_serialize(const PgStat_HashKey *key, + const PgStatShared_Common *header, FILE *statfile) +{ + char *description; + size_t len; + long offset; + PgStatShared_CustomEntry *entry = (PgStatShared_CustomEntry *) header; + bool found; + + if (!custom_stats_description_dsa) + custom_stats_description_dsa = GetNamedDSA("pgstat_custom_stat_dsa", &found); + + /* Open statistics file for writing if not already open */ + if (!fd_description) + { + fd_description = AllocateFile(PGSTAT_CUSTOM_EXTRA_DATA_DESC, PG_BINARY_W); + if (fd_description == NULL) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not open statistics file \"%s\" for writing: %m", + PGSTAT_CUSTOM_EXTRA_DATA_DESC))); + len = 0; + offset = 0; + fwrite(&len, sizeof(len), 1, statfile); + fwrite(&offset, sizeof(offset), 1, statfile); + return; + } + fd_description_offset = 0; + } + + /* Handle entries without descriptions */ + if (!DsaPointerIsValid(entry->description) || !custom_stats_description_dsa) + { + len = 0; + offset = 0; + fwrite(&len, sizeof(len), 1, statfile); + fwrite(&offset, sizeof(offset), 1, statfile); + return; + } + + /* Get current offset in fd_description */ + offset = fd_description_offset; + + /* Retrieve description from DSA and write to fd_description */ + description = dsa_get_address(custom_stats_description_dsa, entry->description); + len = strlen(description) + 1; + fwrite(description, 1, len, fd_description); + fd_description_offset += len; + + /* Write length and offset to statfile */ + fwrite(&len, sizeof(len), 1, statfile); + fwrite(&offset, sizeof(offset), 1, statfile); +} + +/* + * pgstat_custom_var_stats_deserialize() - + * + * Deserialize extra data (descriptions) for custom statistics entries from + * the statistics file. Called during statistics file reading to restore + * description strings after a restart. + */ +static bool +pgstat_custom_var_stats_deserialize(const PgStat_HashKey *key, + const PgStatShared_Common *header, FILE *statfile) +{ + PgStatShared_CustomEntry *entry; + dsa_pointer dp; + size_t len; + long offset; + char *buffer; + bool found; + + /* Read length and offset from statfile */ + if (fread(&len, sizeof(len), 1, statfile) != 1 || + fread(&offset, sizeof(offset), 1, statfile) != 1) + { + elog(WARNING, "failed to read description metadata from statistics file"); + return false; + } + + entry = (PgStatShared_CustomEntry *) header; + + /* Handle empty descriptions */ + if (len == 0) + { + entry->description = InvalidDsaPointer; + return true; + } + + /* Initialize DSA if needed */ + if (!custom_stats_description_dsa) + custom_stats_description_dsa = GetNamedDSA("pgstat_custom_stat_dsa", &found); + + if (!custom_stats_description_dsa) + { + elog(WARNING, "could not access DSA for custom statistics descriptions"); + return false; + } + + /* Open statistics file for reading if not already open */ + if (!fd_description) + { + fd_description = AllocateFile(PGSTAT_CUSTOM_EXTRA_DATA_DESC, PG_BINARY_R); + if (fd_description == NULL) + { + if (errno != ENOENT) + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not open statistics file \"%s\" for reading: %m", + PGSTAT_CUSTOM_EXTRA_DATA_DESC))); + pgstat_reset_of_kind(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS); + return false; + } + } + + /* Seek to the offset and read description */ + if (fseek(fd_description, offset, SEEK_SET) != 0) + { + elog(WARNING, "failed to seek to offset %ld in description file", offset); + return false; + } + + buffer = palloc(len); + if (fread(buffer, 1, len, fd_description) != len) + { + pfree(buffer); + elog(WARNING, "failed to read description from file"); + return false; + } + + /* Allocate space in DSA and copy the description */ + dp = dsa_allocate(custom_stats_description_dsa, len); + memcpy(dsa_get_address(custom_stats_description_dsa, dp), buffer, len); + entry->description = dp; + + pfree(buffer); + + return true; +} + +/* + * pgstat_custom_var_stats_file_cleanup() - + * + * Cleanup function called at the end of statistics file operations. + * Handles closing files and cleanup based on the operation type. + */ +static void +pgstat_custom_var_stats_file_cleanup(PgStat_StatsFileOp status) +{ + switch (status) + { + case STATS_WRITE: + if (!fd_description) + return; + + fd_description_offset = 0; + + /* Check for write errors and cleanup if necessary */ + if (ferror(fd_description)) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not write statistics file \"%s\": %m", + PGSTAT_CUSTOM_EXTRA_DATA_DESC))); + FreeFile(fd_description); + unlink(PGSTAT_CUSTOM_EXTRA_DATA_DESC); + } + else if (FreeFile(fd_description) < 0) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not close statistics file \"%s\": %m", + PGSTAT_CUSTOM_EXTRA_DATA_DESC))); + unlink(PGSTAT_CUSTOM_EXTRA_DATA_DESC); + } + break; + + case STATS_READ: + if (!fd_description) + return; + + FreeFile(fd_description); + + /* Remove the temporary statistics file after reading */ + elog(DEBUG2, "removing statistics file \"%s\"", PGSTAT_CUSTOM_EXTRA_DATA_DESC); + unlink(PGSTAT_CUSTOM_EXTRA_DATA_DESC); + break; + + case STATS_DISCARD: + { + int ret; + + /* Attempt to remove the statistics file */ + ret = unlink(PGSTAT_CUSTOM_EXTRA_DATA_DESC); + if (ret != 0) + { + if (errno == ENOENT) + elog(LOG, + "didn't need to unlink permanent stats file \"%s\" - didn't exist", + PGSTAT_CUSTOM_EXTRA_DATA_DESC); + else + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not unlink permanent statistics file \"%s\": %m", + PGSTAT_CUSTOM_EXTRA_DATA_DESC))); + } + else + { + ereport(LOG, + (errmsg_internal("unlinked permanent statistics file \"%s\"", + PGSTAT_CUSTOM_EXTRA_DATA_DESC))); + } + } + break; + } + + fd_description = NULL; +} + /*-------------------------------------------------------------------------- * Helper functions *-------------------------------------------------------------------------- @@ -161,8 +422,7 @@ pgstat_fetch_custom_entry(const char *stat_name) * 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. + * Initializes a statistics entry with the given name and description. */ PG_FUNCTION_INFO_V1(pgstat_create_custom_var_stats); Datum @@ -171,6 +431,9 @@ 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)); + char *description = text_to_cstring(PG_GETARG_TEXT_PP(1)); + dsa_pointer dp = InvalidDsaPointer; + bool found; /* Validate name length first */ if (strlen(stat_name) >= NAMEDATALEN) @@ -179,6 +442,20 @@ pgstat_create_custom_var_stats(PG_FUNCTION_ARGS) errmsg("custom statistic name \"%s\" is too long", stat_name), errdetail("Name must be less than %d characters.", NAMEDATALEN))); + /* Initialize DSA and description provided */ + if (!custom_stats_description_dsa) + custom_stats_description_dsa = GetNamedDSA("pgstat_custom_stat_dsa", &found); + + if (!custom_stats_description_dsa) + ereport(ERROR, + (errmsg("could not access DSA for custom statistics descriptions"))); + + /* Allocate space in DSA and copy description */ + dp = dsa_allocate(custom_stats_description_dsa, strlen(description) + 1); + memcpy(dsa_get_address(custom_stats_description_dsa, dp), + description, + strlen(description) + 1); + /* 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); @@ -191,6 +468,9 @@ pgstat_create_custom_var_stats(PG_FUNCTION_ARGS) /* Zero-initialize statistics */ memset(&shared_entry->stats, 0, sizeof(shared_entry->stats)); + /* Store description pointer */ + shared_entry->description = dp; + pgstat_unlock_entry(entry_ref); PG_RETURN_VOID(); @@ -225,8 +505,7 @@ pgstat_update_custom_var_stats(PG_FUNCTION_ARGS) * pgstat_drop_custom_var_stats * Remove custom statistic entry * - * Drops the named statistic from shared memory and requests - * garbage collection if needed. + * Drops the named statistic from shared memory. */ PG_FUNCTION_INFO_V1(pgstat_drop_custom_var_stats); Datum @@ -246,7 +525,7 @@ pgstat_drop_custom_var_stats(PG_FUNCTION_ARGS) * pgstat_report_custom_var_stats * Retrieve custom statistic values * - * Returns single row with statistic name and call count if the + * Returns single row with statistic name, call count, and description if the * statistic exists, otherwise returns no rows. */ PG_FUNCTION_INFO_V1(pgstat_report_custom_var_stats); @@ -280,9 +559,13 @@ pgstat_report_custom_var_stats(PG_FUNCTION_ARGS) if (funcctx->call_cntr < funcctx->max_calls) { - Datum values[2]; - bool nulls[2] = {false, false}; + Datum values[3]; + bool nulls[3] = {false, false, false}; HeapTuple tuple; + PgStat_EntryRef *entry_ref; + PgStatShared_CustomEntry *shared_entry; + char *description = NULL; + bool found; stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); stat_entry = pgstat_fetch_custom_entry(stat_name); @@ -290,9 +573,33 @@ pgstat_report_custom_var_stats(PG_FUNCTION_ARGS) /* Return row only if entry exists */ if (stat_entry) { + /* Get entry ref to access shared entry */ + entry_ref = pgstat_get_entry_ref(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid, + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), false, NULL); + + if (entry_ref) + { + shared_entry = (PgStatShared_CustomEntry *) entry_ref->shared_stats; + + /* Get description from DSA if available */ + if (DsaPointerIsValid(shared_entry->description)) + { + if (!custom_stats_description_dsa) + custom_stats_description_dsa = GetNamedDSA("pgstat_custom_stat_dsa", &found); + + if (custom_stats_description_dsa) + description = dsa_get_address(custom_stats_description_dsa, shared_entry->description); + } + } + values[0] = PointerGetDatum(cstring_to_text(stat_name)); values[1] = Int64GetDatum(stat_entry->numcalls); + if (description) + values[2] = PointerGetDatum(cstring_to_text(description)); + else + nulls[2] = true; + tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls); SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple)); } -- 2.43.0