From 71cfa9efd693ab003b819d20fcd1e4e448efcec5 Mon Sep 17 00:00:00 2001
From: Shinya Kato <shinya11.kato@gmail.com>
Date: Thu, 13 Nov 2025 13:06:33 +0900
Subject: [PATCH v5] Add mode and triggered_by columns to
 pg_stat_progress_vacuum.

Exposes the vacuum mode (normal, aggressive, or failsafe) and the
reason why the current VACUUM operation was initiated (manual,
autovacuum, or autovacuum_wraparound). This allows users and
monitoring tools to better understand VACUUM behavior.

Bump catalog version.

Author: Shinya Kato <shinya11.kato@gmail.com>
Reviewed-by: Kirill Reshke <reshkekirill@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Robert Treat <rob@xzilla.net>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Reviewed-by: Sami Imseih <samimseih@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Discussion: https://postgr.es/m/CAOzEurQcOY-OBL_ouEVfEaFqe_md3vB5pXjR_m6L71Dcp1JKCQ@mail.gmail.com
---
 doc/src/sgml/maintenance.sgml        |  7 ++-
 doc/src/sgml/monitoring.sgml         | 72 ++++++++++++++++++++++++++++
 src/backend/access/heap/vacuumlazy.c | 26 ++++++++--
 src/backend/catalog/system_views.sql | 10 +++-
 src/include/commands/progress.h      | 12 +++++
 src/test/regress/expected/rules.out  | 14 +++++-
 6 files changed, 133 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index 120bac8875f..a3bc9785335 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -1014,8 +1014,11 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu
     see <xref linkend="table-lock-compatibility"/>.  However, if the autovacuum
     is running to prevent transaction ID wraparound (i.e., the autovacuum query
     name in the <structname>pg_stat_activity</structname> view ends with
-    <literal>(to prevent wraparound)</literal>), the autovacuum is not
-    automatically interrupted.
+    <literal>(to prevent wraparound)</literal> or the
+    <literal>autovacuum_wraparound</literal> value in the
+    <structfield>triggered_by</structfield> column in the
+    <structname>pg_stat_progress_vacuum</structname> view), the autovacuum is
+    not automatically interrupted.
    </para>
 
    <warning>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 436ef0e8bd0..f79c0f85cb4 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -6688,6 +6688,78 @@ FROM pg_stat_get_backend_idset() AS backendid;
        stale.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mode</structfield> <type>text</type>
+      </para>
+      <para>
+       The mode in which the current <command>VACUUM</command> operation is
+       running. See <xref linkend="vacuum-for-wraparound"/> for details of each
+       mode. Possible values are:
+       <itemizedlist>
+        <listitem>
+         <para>
+          <literal>normal</literal>: The operation is performing a standard
+          vacuum. It is neither required to run in aggressive mode nor operating
+          in failsafe mode.
+         </para>
+        </listitem>
+        <listitem>
+         <para>
+          <literal>aggressive</literal>: The operation is running an aggressive
+          vacuum, which must scan every page that is not marked all-frozen.
+          The parameters <xref linkend="guc-vacuum-freeze-table-age"/> and
+          <xref linkend="guc-vacuum-multixact-freeze-table-age"/> determine when a
+          table requires aggressive vacuuming.
+         </para>
+        </listitem>
+        <listitem>
+         <para>
+          <literal>failsafe</literal>: The vacuum has entered failsafe mode,
+          in which it performs only the minimum work necessary to avoid
+          transaction ID or multixact ID wraparound failure.
+          The parameters <xref linkend="guc-vacuum-failsafe-age"/> and
+          <xref linkend="guc-vacuum-multixact-failsafe-age"/> determine when the
+          vacuum enters failsafe mode.
+         </para>
+        </listitem>
+       </itemizedlist>
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>triggered_by</structfield> <type>text</type>
+      </para>
+      <para>
+       Shows what caused the current <command>VACUUM</command> operation to be
+       initiated. Possible values are:
+       <itemizedlist>
+        <listitem>
+         <para>
+          <literal>manual</literal>: The vacuum was initiated by an explicit
+          <command>VACUUM</command> command.
+         </para>
+        </listitem>
+        <listitem>
+         <para>
+          <literal>autovacuum</literal>: The vacuum was started by an autovacuum
+          worker. Vacuums run by autovacuum workers may be interrupted due to
+          lock conflicts.
+         </para>
+        </listitem>
+        <listitem>
+         <para>
+          <literal>autovacuum_wraparound</literal>: The vacuum was started by an
+          autovacuum worker to prevent transaction ID or multixact ID wraparound.
+          Vacuums run for wraparound protection are not interrupted due to lock
+          conflicts.
+         </para>
+        </listitem>
+       </itemizedlist>
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index deb9a3dc0d1..9601f15573a 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -664,6 +664,14 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
 	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
 								  RelationGetRelid(rel));
+	if (AmAutoVacuumWorkerProcess())
+		pgstat_progress_update_param(PROGRESS_VACUUM_TRIGGERED_BY,
+									 params.is_wraparound
+									 ? PROGRESS_VACUUM_TRIGGERED_BY_AUTOVACUUM_WRAPAROUND
+									 : PROGRESS_VACUUM_TRIGGERED_BY_AUTOVACUUM);
+	else
+		pgstat_progress_update_param(PROGRESS_VACUUM_TRIGGERED_BY,
+									 PROGRESS_VACUUM_TRIGGERED_BY_MANUAL);
 
 	/*
 	 * Setup error traceback support for ereport() first.  The idea is to set
@@ -787,6 +795,13 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	 * to increase the number of dead tuples it can prune away.)
 	 */
 	vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs);
+	if (vacrel->aggressive)
+		pgstat_progress_update_param(PROGRESS_VACUUM_MODE,
+									 PROGRESS_VACUUM_MODE_AGGRESSIVE);
+	else
+		pgstat_progress_update_param(PROGRESS_VACUUM_MODE,
+									 PROGRESS_VACUUM_MODE_NORMAL);
+
 	vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel);
 	vacrel->vistest = GlobalVisTestFor(rel);
 
@@ -808,6 +823,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 		 * visibility map (even those set all-frozen)
 		 */
 		vacrel->aggressive = true;
+		pgstat_progress_update_param(PROGRESS_VACUUM_MODE,
+									 PROGRESS_VACUUM_MODE_AGGRESSIVE);
 		skipwithvm = false;
 	}
 
@@ -2996,9 +3013,10 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 	{
 		const int	progress_index[] = {
 			PROGRESS_VACUUM_INDEXES_TOTAL,
-			PROGRESS_VACUUM_INDEXES_PROCESSED
+			PROGRESS_VACUUM_INDEXES_PROCESSED,
+			PROGRESS_VACUUM_MODE
 		};
-		int64		progress_val[2] = {0, 0};
+		int64		progress_val[3] = {0, 0, PROGRESS_VACUUM_MODE_FAILSAFE};
 
 		VacuumFailsafeActive = true;
 
@@ -3014,8 +3032,8 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		vacrel->do_index_cleanup = false;
 		vacrel->do_rel_truncate = false;
 
-		/* Reset the progress counters */
-		pgstat_progress_update_multi_param(2, progress_index, progress_val);
+		/* Reset the progress counters and set the failsafe mode */
+		pgstat_progress_update_multi_param(3, progress_index, progress_val);
 
 		ereport(WARNING,
 				(errmsg("bypassing nonessential maintenance of table \"%s.%s.%s\" as a failsafe after %d index scans",
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 95ad29a64b9..d4b0f06e422 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1265,7 +1265,15 @@ CREATE VIEW pg_stat_progress_vacuum AS
         S.param6 AS max_dead_tuple_bytes, S.param7 AS dead_tuple_bytes,
         S.param8 AS num_dead_item_ids, S.param9 AS indexes_total,
         S.param10 AS indexes_processed,
-        S.param11 / 1000000::double precision AS delay_time
+        S.param11 / 1000000::double precision AS delay_time,
+        CASE S.param12 WHEN 1 THEN 'normal'
+                       WHEN 2 THEN 'aggressive'
+                       WHEN 3 THEN 'failsafe'
+                       ELSE NULL END AS mode,
+        CASE S.param13 WHEN 1 THEN 'manual'
+                       WHEN 2 THEN 'autovacuum'
+                       WHEN 3 THEN 'autovacuum_wraparound'
+                       ELSE NULL END AS triggered_by
     FROM pg_stat_get_progress_info('VACUUM') AS S
         LEFT JOIN pg_database D ON S.datid = D.oid;
 
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 1cde4bd9bcf..5fc5d3fcbee 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -29,6 +29,8 @@
 #define PROGRESS_VACUUM_INDEXES_TOTAL			8
 #define PROGRESS_VACUUM_INDEXES_PROCESSED		9
 #define PROGRESS_VACUUM_DELAY_TIME				10
+#define PROGRESS_VACUUM_MODE					11
+#define PROGRESS_VACUUM_TRIGGERED_BY			12
 
 /* Phases of vacuum (as advertised via PROGRESS_VACUUM_PHASE) */
 #define PROGRESS_VACUUM_PHASE_SCAN_HEAP			1
@@ -38,6 +40,16 @@
 #define PROGRESS_VACUUM_PHASE_TRUNCATE			5
 #define PROGRESS_VACUUM_PHASE_FINAL_CLEANUP		6
 
+/* Modes of vacuum (as advertised via PROGRESS_VACUUM_MODE) */
+#define PROGRESS_VACUUM_MODE_NORMAL				1
+#define PROGRESS_VACUUM_MODE_AGGRESSIVE			2
+#define PROGRESS_VACUUM_MODE_FAILSAFE			3
+
+/* Reasons for vacuum (as advertised via PROGRESS_VACUUM_TRIGGERED_BY) */
+#define PROGRESS_VACUUM_TRIGGERED_BY_MANUAL					1
+#define PROGRESS_VACUUM_TRIGGERED_BY_AUTOVACUUM				2
+#define PROGRESS_VACUUM_TRIGGERED_BY_AUTOVACUUM_WRAPAROUND	3
+
 /* Progress parameters for analyze */
 #define PROGRESS_ANALYZE_PHASE						0
 #define PROGRESS_ANALYZE_BLOCKS_TOTAL				1
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 372a2188c22..2d75baa21dd 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2104,7 +2104,19 @@ pg_stat_progress_vacuum| SELECT s.pid,
     s.param8 AS num_dead_item_ids,
     s.param9 AS indexes_total,
     s.param10 AS indexes_processed,
-    ((s.param11)::double precision / (1000000)::double precision) AS delay_time
+    ((s.param11)::double precision / (1000000)::double precision) AS delay_time,
+        CASE s.param12
+            WHEN 1 THEN 'normal'::text
+            WHEN 2 THEN 'aggressive'::text
+            WHEN 3 THEN 'failsafe'::text
+            ELSE NULL::text
+        END AS mode,
+        CASE s.param13
+            WHEN 1 THEN 'manual'::text
+            WHEN 2 THEN 'autovacuum'::text
+            WHEN 3 THEN 'autovacuum_wraparound'::text
+            ELSE NULL::text
+        END AS triggered_by
    FROM (pg_stat_get_progress_info('VACUUM'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
      LEFT JOIN pg_database d ON ((s.datid = d.oid)));
 pg_stat_recovery_prefetch| SELECT stats_reset,
-- 
2.47.3

