From c5aeb083265efbd6041ed3868b669997d4430760 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 16 Mar 2026 15:12:24 +0900 Subject: [PATCH] Add support for transient stats updates This introduces a function able to push stats updates, with a new stats kind property to allow stats to be updated while in a transaction. --- src/include/catalog/pg_proc.dat | 7 +++ src/include/miscadmin.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/pgstat_internal.h | 16 ++++++ src/backend/storage/ipc/procsignal.c | 16 ++++++ src/backend/tcop/postgres.c | 11 +++- src/backend/utils/activity/pgstat.c | 42 ++++++++++++-- src/backend/utils/adt/pgstatfuncs.c | 58 ++++++++++++++++++++ src/backend/utils/init/globals.c | 1 + src/test/regress/expected/misc_functions.out | 52 ++++++++++++++++++ src/test/regress/sql/misc_functions.sql | 29 ++++++++++ doc/src/sgml/func/func-admin.sgml | 16 ++++++ 12 files changed, 242 insertions(+), 8 deletions(-) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 361e2cfffebe..85869154657a 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8692,6 +8692,13 @@ prosrc => 'pg_log_backend_memory_contexts', proacl => '{POSTGRES=X}' }, +# request an update of statistics +{ oid => '8789', descr => 'have the specified backend push a pgstats update', + proname => 'pg_stat_report', provolatile => 'v', + prorettype => 'bool', proargtypes => 'int4', + prosrc => 'pg_stat_report', + proacl => '{POSTGRES=X}' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index f16f35659b9b..a245cdd79d17 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -94,6 +94,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; +extern PGDLLIMPORT volatile sig_atomic_t ProcSignalStatsUpdatePending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 348fba53a931..d2e60403ad52 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -34,6 +34,7 @@ typedef enum PROCSIG_PARALLEL_MESSAGE, /* message from cooperating parallel backend */ PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ + PROCSIG_STATS_UPDATE, /* pgstats update */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index 9b8fbae00ed5..fa4cc4fe9c60 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -224,6 +224,17 @@ typedef struct PgStat_SubXactStatus PgStat_TableXactStatus *first; /* head of list for this subxact */ } PgStat_SubXactStatus; +/* + * Contexts related to the report of the statistics, defined as properties + * of PgStat_KindInfo.report_context. These define when a stats report is + * allowed depending on the stats kind and the context where + * pgstat_report_stat() is called. + */ + +/* report allowed while idle, outside a transaction (default) */ +#define PGSTAT_REPORT_IDLE 0x00 +/* report of stats data allowed within a transaction */ +#define PGSTAT_REPORT_TRANSACTION 0x01 /* * Metadata for a specific kind of statistics. @@ -251,6 +262,11 @@ typedef struct PgStat_KindInfo */ bool track_entry_count:1; + /* + * Contexts allowed for the report of this stats kind data. + */ + bits32 report_context; + /* * The size of an entry in the shared stats hash table (pointed to by * PgStatShared_HashEntry->body). For fixed-numbered statistics, this is diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 7e017c8d53b5..e1bff2185933 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -490,6 +490,19 @@ HandleProcSignalBarrierInterrupt(void) /* latch will be set by procsignal_sigusr1_handler */ } +/* + * Handle receipt of an interrupt indicating that a stats update has been + * requested. This routine only gets called when PROCSIG_STATS_UPDATE is + * sent. + */ +static void +HandleProcSignalStatsUpdateInterrupt(void) +{ + InterruptPending = true; + ProcSignalStatsUpdatePending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + /* * Perform global barrier related interrupt checking. * @@ -694,6 +707,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_BARRIER)) HandleProcSignalBarrierInterrupt(); + if (CheckProcSignal(PROCSIG_STATS_UPDATE)) + HandleProcSignalStatsUpdateInterrupt(); + if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index d01a09dd0c41..ddd57dfea780 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3555,12 +3555,17 @@ ProcessInterrupts(void) /* * If there are pending stats updates and we currently are truly idle - * (matching the conditions in PostgresMain(), report stats now. + * (matching the conditions in PostgresMain(), or if a status update has + * been requested, report stats now. */ if (IdleStatsUpdateTimeoutPending && - DoingCommandRead && !IsTransactionOrTransactionBlock()) + DoingCommandRead && !IsTransactionOrTransactionBlock() || + ProcSignalStatsUpdatePending) { - IdleStatsUpdateTimeoutPending = false; + if (IdleStatsUpdateTimeoutPending) + IdleStatsUpdateTimeoutPending = false; + if (ProcSignalStatsUpdatePending) + ProcSignalStatsUpdatePending = false; pgstat_report_stat(true); } diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index 11bb71cad5ad..5521e96d0cae 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -436,6 +436,7 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] .fixed_amount = true, .write_to_file = true, + .report_context = PGSTAT_REPORT_TRANSACTION, .snapshot_ctl_off = offsetof(PgStat_Snapshot, io), .shared_ctl_off = offsetof(PgStat_ShmemControl, io), @@ -470,6 +471,7 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] .fixed_amount = true, .write_to_file = true, + .report_context = PGSTAT_REPORT_TRANSACTION, .snapshot_ctl_off = offsetof(PgStat_Snapshot, wal), .shared_ctl_off = offsetof(PgStat_ShmemControl, wal), @@ -698,8 +700,9 @@ pgstat_initialize(void) * a timeout after which to call pgstat_report_stat(true), but are not * required to do so. * - * Note that this is called only when not within a transaction, so it is fair - * to use transaction stop time as an approximation of current time. + * Note that when this is called only when not within a transaction, we use + * the transaction stop time as an approximation of current time. "force" + * is required when this is called within a transaction. */ long pgstat_report_stat(bool force) @@ -709,9 +712,14 @@ pgstat_report_stat(bool force) bool partial_flush; TimestampTz now; bool nowait; + bool is_xact = IsTransactionOrTransactionBlock(); pgstat_assert_is_up(); - Assert(!IsTransactionOrTransactionBlock()); + /* + * "force" is required if this routine is called inside a transaction + * block. + */ + Assert(!is_xact || force); /* "absorb" the forced flush even if there's nothing to flush */ if (pgStatForceNextFlush) @@ -789,6 +797,11 @@ pgstat_report_stat(bool force) if (!kind_info->flush_static_cb) continue; + /* Skip if this stats kind cannot be flushed in a transaction */ + if (is_xact && + (kind_info->report_context & PGSTAT_REPORT_TRANSACTION) == 0) + continue; + partial_flush |= kind_info->flush_static_cb(nowait); } } @@ -801,8 +814,11 @@ pgstat_report_stat(bool force) */ if (partial_flush) { - /* force should have prevented us from getting here */ - Assert(!force); + /* + * force should have prevented us from getting here, and partial + * flushes are accepted inside a transaction. + */ + Assert(!force || is_xact); /* remember since when stats have been pending */ if (pending_since == 0) @@ -1351,6 +1367,7 @@ pgstat_flush_pending_entries(bool nowait) { bool have_pending = false; dlist_node *cur = NULL; + bool is_xact = IsTransactionOrTransactionBlock(); /* * Need to be a bit careful iterating over the list of pending entries. @@ -1377,6 +1394,21 @@ pgstat_flush_pending_entries(bool nowait) Assert(!kind_info->fixed_amount); Assert(kind_info->flush_pending_cb != NULL); + /* Skip if this stats kind cannot be flushed while in a transaction */ + if (is_xact && + (kind_info->report_context & PGSTAT_REPORT_TRANSACTION) == 0) + { + have_pending = true; + + if (dlist_has_next(&pgStatPending, cur)) + next = dlist_next_node(&pgStatPending, cur); + else + next = NULL; + + cur = next; + continue; + } + /* flush the stats, if possible */ did_flush = kind_info->flush_pending_cb(entry_ref, nowait); diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index bad5642d9c90..b27a5eb58e18 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -28,6 +28,7 @@ #include "replication/logicallauncher.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "storage/procsignal.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/timestamp.h" @@ -2325,3 +2326,60 @@ pg_stat_have_stats(PG_FUNCTION_ARGS) PG_RETURN_BOOL(pgstat_have_entry(kind, dboid, objid)); } + +/* + * pg_stat_report + * Signal a backend or an auxiliary process to have it push an update + * of its statistics data. + * + * By default, only superusers are allowed to signal to log the memory + * contexts because allowing any users to issue this request at an unbounded + * rate would cause lots of log messages and which can lead to denial of + * service. Additional roles can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to update their statistics. + */ +Datum +pg_stat_report(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + + /* + * See if the process with given pid is a backend or an auxiliary process. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + proc = AuxiliaryPidGetProc(pid); + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; but by the time we reach kill(), a process for which we + * get a valid proc here might have terminated on its own. This is OK, + * as at shutdown processes flush their stats. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + (errmsg("PID %d is not a PostgreSQL server process", pid))); + PG_RETURN_BOOL(false); + } + + procNumber = GetNumberFromPGProc(proc); + if (SendProcSignal(pid, PROCSIG_STATS_UPDATE, procNumber) < 0) + { + /* Again, just a warning to allow loops */ + ereport(WARNING, + (errmsg("could not send signal to process %d: %m", pid))); + PG_RETURN_BOOL(false); + } + + PG_RETURN_BOOL(true); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 36ad708b3602..22960ee3b27b 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -38,6 +38,7 @@ volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false; volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; +volatile sig_atomic_t ProcSignalStatsUpdatePending = false; volatile sig_atomic_t LogMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out index 6c03b1a79d75..68a0e1e02bcc 100644 --- a/src/test/regress/expected/misc_functions.out +++ b/src/test/regress/expected/misc_functions.out @@ -397,6 +397,58 @@ REVOKE EXECUTE ON FUNCTION pg_log_backend_memory_contexts(integer) FROM regress_log_memory; DROP ROLE regress_log_memory; -- +-- pg_stat_report +-- +-- check execution +SELECT pg_stat_report(pg_backend_pid()); + pg_stat_report +---------------- + t +(1 row) + +SELECT pg_stat_report(pid) FROM pg_stat_activity + WHERE backend_type = 'checkpointer'; + pg_stat_report +---------------- + t +(1 row) + +-- Check privileges +CREATE ROLE regress_stat_report; +SELECT has_function_privilege('regress_stat_report', + 'pg_stat_report(integer)', 'EXECUTE'); -- no + has_function_privilege +------------------------ + f +(1 row) + +-- Fails +SET ROLE regress_stat_report; +SELECT pg_stat_report(pg_backend_pid()); +ERROR: permission denied for function pg_stat_report +RESET ROLE; +-- Access granted, then function works +GRANT EXECUTE ON FUNCTION pg_stat_report(integer) + TO regress_stat_report; +SELECT has_function_privilege('regress_stat_report', + 'pg_stat_report(integer)', 'EXECUTE'); -- yes + has_function_privilege +------------------------ + t +(1 row) + +SET ROLE regress_stat_report; +SELECT pg_stat_report(pg_backend_pid()); + pg_stat_report +---------------- + t +(1 row) + +RESET ROLE; +REVOKE EXECUTE ON FUNCTION pg_stat_report(integer) + FROM regress_stat_report; +DROP ROLE regress_stat_report; +-- -- Test some built-in SRFs -- -- The outputs of these are variable, so we can't just print their results diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql index 35b7983996c4..fe366904c9e8 100644 --- a/src/test/regress/sql/misc_functions.sql +++ b/src/test/regress/sql/misc_functions.sql @@ -154,6 +154,35 @@ REVOKE EXECUTE ON FUNCTION pg_log_backend_memory_contexts(integer) DROP ROLE regress_log_memory; +-- +-- pg_stat_report +-- + +-- check execution +SELECT pg_stat_report(pg_backend_pid()); +SELECT pg_stat_report(pid) FROM pg_stat_activity + WHERE backend_type = 'checkpointer'; + +-- Check privileges +CREATE ROLE regress_stat_report; +SELECT has_function_privilege('regress_stat_report', + 'pg_stat_report(integer)', 'EXECUTE'); -- no +-- Fails +SET ROLE regress_stat_report; +SELECT pg_stat_report(pg_backend_pid()); +RESET ROLE; +-- Access granted, then function works +GRANT EXECUTE ON FUNCTION pg_stat_report(integer) + TO regress_stat_report; +SELECT has_function_privilege('regress_stat_report', + 'pg_stat_report(integer)', 'EXECUTE'); -- yes +SET ROLE regress_stat_report; +SELECT pg_stat_report(pg_backend_pid()); +RESET ROLE; +REVOKE EXECUTE ON FUNCTION pg_stat_report(integer) + FROM regress_stat_report; +DROP ROLE regress_stat_report; + -- -- Test some built-in SRFs -- diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 210b1118bdf7..114202c4fc19 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -220,6 +220,22 @@ + + + + pg_stat_report + + pg_log_backend_memory_contexts ( pid integer ) + boolean + + + Requests to update the statistics computed by the backend with the + specified process ID. This function can send the request to + backends and auxiliary processes except logger, pushing an + update of the statistics data. + + + -- 2.53.0