From 205e22a432753760b3110e851955fd3421ff3564 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Wed, 28 Jan 2026 07:53:13 +0000
Subject: [PATCH v11 3/5] Add GUC to specify non-transactional statistics flush
 interval

Adding pgstat_flush_interval, a new GUC to set the interval between flushes of
non-transactional statistics.
---
 doc/src/sgml/config.sgml                      | 32 +++++++++++++++++++
 src/backend/utils/activity/pgstat.c           | 13 ++++++++
 src/backend/utils/misc/guc_parameters.dat     | 10 ++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/backend/utils/misc/timeout.c              |  6 ++++
 src/include/pgstat.h                          |  6 ++--
 src/include/utils/guc_hooks.h                 |  1 +
 src/include/utils/timeout.h                   |  1 +
 .../test_custom_stats/t/001_custom_stats.pl   |  6 ++--
 9 files changed, 70 insertions(+), 6 deletions(-)
  51.0% doc/src/sgml/
  10.6% src/backend/utils/activity/
  15.9% src/backend/utils/misc/
   3.6% src/include/utils/
   9.0% src/include/
   9.6% src/test/modules/test_custom_stats/t/

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 20dbcaeb3ee..1eed71007a7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8929,6 +8929,38 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-stats-flush-interval" xreflabel="stats_flush_interval">
+      <term><varname>stats_flush_interval</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>stats_flush_interval</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Sets the interval at which certain statistics, which can be updated while a
+        transaction is in progress, are made visible. These include WAL activity
+        and I/O operations.
+        Such statistics are refreshed at the specified interval and can be observed
+        during active transactions in monitoring views such as
+        <link linkend="monitoring-pg-stat-wal-view"><structname>pg_stat_wal</structname></link>
+        and
+        <link linkend="monitoring-pg-stat-io-view"><structname>pg_stat_io</structname></link>.
+        If the value is specified without a unit, milliseconds are assumed.
+        The default is 10 seconds (<literal>10s</literal>), which is generally
+        the smallest practical value for long-running transactions.
+       </para>
+       <note>
+        <para>
+         This parameter does not affect statistics that are only reported at
+         transaction end, such as the columns of <structname>pg_stat_all_tables</structname>
+         (for example, <structfield>n_tup_ins</structfield>, <structfield>n_tup_upd</structfield>,
+         and <structfield>n_tup_del</structfield>). These statistics are always
+         flushed at the end of a transaction.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index ddd331e2c81..fd6ab0db16f 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -124,6 +124,8 @@
  * ----------
  */
 
+/* minimum interval non-forced stats flushes.*/
+#define PGSTAT_MIN_INTERVAL			1000
 /* how long until to block flushing pending stats updates */
 #define PGSTAT_MAX_INTERVAL			60000
 /* when to call pgstat_report_stat() again, even when idle */
@@ -204,6 +206,7 @@ static inline bool pgstat_is_kind_valid(PgStat_Kind kind);
 
 bool		pgstat_track_counts = false;
 int			pgstat_fetch_consistency = PGSTAT_FETCH_CONSISTENCY_CACHE;
+int			pgstat_flush_interval = 10000;
 
 
 /* ----------
@@ -2171,6 +2174,16 @@ assign_stats_fetch_consistency(int newval, void *extra)
 		force_stats_snapshot_clear = true;
 }
 
+/*
+ * GUC assign_hook for stats_flush_interval.
+ */
+void
+assign_stats_flush_interval(int newval, void *extra)
+{
+	if (get_all_timeouts_initialized())
+		enable_timeout_after(ANYTIME_STATS_UPDATE_TIMEOUT, newval);
+}
+
 /*
  * Flushes only FLUSH_ANYTIME stats using non-blocking locks. Transactional
  * stats (FLUSH_AT_TXN_BOUNDARY) remain pending until transaction boundary.
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 9507778415d..073e08c7892 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2801,6 +2801,16 @@
   assign_hook => 'assign_stats_fetch_consistency',
 },
 
+{ name => 'stats_flush_interval', type => 'int', context => 'PGC_USERSET', group => 'STATS_CUMULATIVE',
+  short_desc => 'Sets the interval between flushes of non-transactional statistics.',
+  flags => 'GUC_UNIT_MS',
+  variable => 'pgstat_flush_interval',
+  boot_val => '10000',
+  min => '1000',
+  max => 'INT_MAX',
+  assign_hook => 'assign_stats_flush_interval'
+},
+
 { name => 'subtransaction_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM',
   short_desc => 'Sets the size of the dedicated buffer pool used for the subtransaction cache.',
   long_desc => '0 means use a fraction of "shared_buffers".',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index f938cc65a3a..8bd37a25b38 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -688,6 +688,7 @@
 #track_wal_io_timing = off
 #track_functions = none                 # none, pl, all
 #stats_fetch_consistency = cache        # cache, none, snapshot
+#stats_flush_interval = 10s             # in milliseconds
 
 
 # - Monitoring -
diff --git a/src/backend/utils/misc/timeout.c b/src/backend/utils/misc/timeout.c
index ddba5dc607c..85c4260d1db 100644
--- a/src/backend/utils/misc/timeout.c
+++ b/src/backend/utils/misc/timeout.c
@@ -828,3 +828,9 @@ get_timeout_finish_time(TimeoutId id)
 {
 	return all_timeouts[id].fin_time;
 }
+
+bool
+get_all_timeouts_initialized(void)
+{
+	return all_timeouts_initialized;
+}
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index b011a315679..90237c70829 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -34,9 +34,6 @@
 /* Default directory to store temporary statistics data in */
 #define PG_STAT_TMP_DIR		"pg_stat_tmp"
 
-/* Minimum interval non-forced stats flushes */
-#define PGSTAT_MIN_INTERVAL	1000
-
 /* Values for track_functions GUC variable --- order is significant! */
 typedef enum TrackFunctionsLevel
 {
@@ -548,7 +545,7 @@ extern void pgstat_force_next_flush(void);
 	do {																				\
 		if (IsUnderPostmaster && !pgstat_pending_anytime)								\
 		{																				\
-			enable_timeout_after(ANYTIME_STATS_UPDATE_TIMEOUT, PGSTAT_MIN_INTERVAL);	\
+			enable_timeout_after(ANYTIME_STATS_UPDATE_TIMEOUT, pgstat_flush_interval);	\
 			pgstat_pending_anytime = true;												\
 		}																				\
 	} while (0)
@@ -831,6 +828,7 @@ extern PGDLLIMPORT bool pgstat_pending_anytime;
 extern PGDLLIMPORT bool pgstat_track_counts;
 extern PGDLLIMPORT int pgstat_track_functions;
 extern PGDLLIMPORT int pgstat_fetch_consistency;
+extern PGDLLIMPORT int pgstat_flush_interval;
 
 
 /*
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 9c90670d9b8..9b5d2a90387 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -132,6 +132,7 @@ extern bool check_session_authorization(char **newval, void **extra, GucSource s
 extern void assign_session_authorization(const char *newval, void *extra);
 extern void assign_session_replication_role(int newval, void *extra);
 extern void assign_stats_fetch_consistency(int newval, void *extra);
+extern void assign_stats_flush_interval(int newval, void *extra);
 extern bool check_ssl(bool *newval, void **extra, GucSource source);
 extern bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
 extern bool check_standard_conforming_strings(bool *newval, void **extra,
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 10723bb664c..fe7327de209 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -93,5 +93,6 @@ extern bool get_timeout_active(TimeoutId id);
 extern bool get_timeout_indicator(TimeoutId id, bool reset_indicator);
 extern TimestampTz get_timeout_start_time(TimeoutId id);
 extern TimestampTz get_timeout_finish_time(TimeoutId id);
+extern bool get_all_timeouts_initialized(void);
 
 #endif							/* TIMEOUT_H */
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 6ba4022418f..920443487c0 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
@@ -164,11 +164,12 @@ $node->safe_psql('postgres', q(select test_custom_stats_fixed_reset()));
 $node->safe_psql('postgres', q(select pg_stat_force_next_flush()));
 
 my $anytime_test = q[
+    SET stats_flush_interval = '1s';
     BEGIN;
     SET LOCAL stats_fetch_consistency = none;
     -- Accumulate stats
     select test_custom_stats_fixed_anytime_update() from generate_series(1, 2);
-    -- Wait (has to be greater than PGSTAT_MIN_INTERVAL)
+    -- Wait (has to be greater than stats_flush_interval)
     select pg_sleep(1.5);
     -- Check
     select 'fixed_anytime:'||numcalls from test_custom_stats_fixed_report();
@@ -184,12 +185,13 @@ like($result, qr/^fixed_anytime:2/m,
 $node->safe_psql('postgres', q(select pg_stat_force_next_flush()));
 
 $anytime_test = q[
+    SET stats_flush_interval = '1s';
     BEGIN;
     SET LOCAL stats_fetch_consistency = none;
     -- Accumulate stats
     select test_custom_stats_var_anytime_update('entry2');
     select test_custom_stats_var_anytime_update('entry2');
-    -- Wait (has to be greater than PGSTAT_MIN_INTERVAL)
+    -- Wait (has to be greater than stats_flush_interval)
     select pg_sleep(1.5);
     -- Check
     select 'var_anytime:'||calls from test_custom_stats_var_report('entry2');
-- 
2.34.1

