From 01f9385e418a3560bb7875d3c1de30112cd4c2c4 Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Thu, 23 Jul 2026 11:11:53 +0300 Subject: [PATCH v42 3/9] Count wraparound-failsafe vacuums in pg_stat views. When a vacuum crosses vacuum_failsafe_age it enters the failsafe mode: cost-based delay is disabled and index vacuuming and heap truncation are skipped. The event means autovacuum on the table systematically fails to keep up, yet its only trace today is a WARNING in the server log. Store it cumulatively: * pg_stat_all_tables: vacuum_failsafe_count - the number of vacuums of this relation that entered the failsafe mode; * pg_stat_database: vacuum_failsafe_count - the same, accumulated over the vacuums of the database's tables. A failsafe vacuum still reaches the end of heap_vacuum_rel(), so the counters go through the regular pgstat_report_vacuum() path. --- doc/src/sgml/monitoring.sgml | 20 ++++++ src/backend/access/heap/vacuumlazy.c | 3 +- src/backend/catalog/system_views.sql | 2 + src/backend/utils/activity/pgstat_database.c | 1 + src/backend/utils/activity/pgstat_relation.c | 8 ++- src/backend/utils/adt/pgstatfuncs.c | 6 ++ src/include/catalog/pg_proc.dat | 10 +++ src/include/pgstat.h | 15 +++- src/test/modules/test_misc/meson.build | 1 + .../test_misc/t/017_vacuum_failsafe.pl | 71 +++++++++++++++++++ src/test/regress/expected/rules.out | 4 ++ 11 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 src/test/modules/test_misc/t/017_vacuum_failsafe.pl diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index d298877949e..8b5bff944a3 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -4122,6 +4122,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + + vacuum_failsafe_count bigint + + + Number of vacuums in this database that entered the wraparound + failsafe mode (see ) + + + session_time double precision @@ -4748,6 +4758,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + + vacuum_failsafe_count bigint + + + Number of vacuums of this table that entered the wraparound failsafe + mode (see ) + + + stats_reset timestamp with time zone diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index bdd22cbeba8..e5c8477515c 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -1000,7 +1000,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params, vacrel->recently_dead_tuples + vacrel->missed_dead_tuples, starttime, - (PgStat_Counter) rint(VacuumDelayTime - startdelaytime)); + (PgStat_Counter) rint(VacuumDelayTime - startdelaytime), + VacuumFailsafeActive); pgstat_progress_end_command(); if (instrument) diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 1e9bb3c3bba..dda36a10d08 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -749,6 +749,7 @@ CREATE VIEW pg_stat_all_tables AS pg_stat_get_total_autoanalyze_time(C.oid) AS total_autoanalyze_time, pg_stat_get_total_vacuum_delay_time(C.oid) AS total_vacuum_delay_time, pg_stat_get_total_autovacuum_delay_time(C.oid) AS total_autovacuum_delay_time, + pg_stat_get_vacuum_failsafe_count(C.oid) AS vacuum_failsafe_count, pg_stat_get_stat_reset_time(C.oid) AS stats_reset FROM pg_class C LEFT JOIN pg_index I ON C.oid = I.indrelid @@ -1182,6 +1183,7 @@ CREATE VIEW pg_stat_database AS pg_stat_get_db_total_autovacuum_time(D.oid) AS total_autovacuum_time, pg_stat_get_db_total_vacuum_delay_time(D.oid) AS total_vacuum_delay_time, pg_stat_get_db_total_autovacuum_delay_time(D.oid) AS total_autovacuum_delay_time, + pg_stat_get_db_vacuum_failsafe_count(D.oid) AS vacuum_failsafe_count, pg_stat_get_db_session_time(D.oid) AS session_time, pg_stat_get_db_active_time(D.oid) AS active_time, pg_stat_get_db_idle_in_transaction_time(D.oid) AS idle_in_transaction_time, diff --git a/src/backend/utils/activity/pgstat_database.c b/src/backend/utils/activity/pgstat_database.c index c906f16eb1b..3d9f90d1970 100644 --- a/src/backend/utils/activity/pgstat_database.c +++ b/src/backend/utils/activity/pgstat_database.c @@ -484,6 +484,7 @@ pgstat_database_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) PGSTAT_ACCUM_DBCOUNT(total_autovacuum_time); PGSTAT_ACCUM_DBCOUNT(total_vacuum_delay_time); PGSTAT_ACCUM_DBCOUNT(total_autovacuum_delay_time); + PGSTAT_ACCUM_DBCOUNT(vacuum_failsafe_count); PGSTAT_ACCUM_DBCOUNT(sessions); PGSTAT_ACCUM_DBCOUNT(session_time); diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c index 1b913f2777c..466617cda60 100644 --- a/src/backend/utils/activity/pgstat_relation.c +++ b/src/backend/utils/activity/pgstat_relation.c @@ -210,7 +210,7 @@ pgstat_drop_relation(Relation rel) void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples, PgStat_Counter deadtuples, TimestampTz starttime, - PgStat_Counter delaytime) + PgStat_Counter delaytime, bool failsafe) { PgStat_EntryRef *entry_ref; PgStatShared_Relation *shtabentry; @@ -263,6 +263,9 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples, tabentry->total_vacuum_delay_time += delaytime; } + if (failsafe) + tabentry->vacuum_failsafe_count++; + pgstat_unlock_entry(entry_ref); /* @@ -284,6 +287,9 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples, dbentry->total_vacuum_time += elapsedtime * 1000; dbentry->total_vacuum_delay_time += delaytime * 1000; } + + if (failsafe) + dbentry->vacuum_failsafe_count++; } /* diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 1af3b4758af..2aeed43d96e 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -108,6 +108,9 @@ PG_STAT_GET_RELENTRY_INT64(tuples_updated) /* pg_stat_get_vacuum_count */ PG_STAT_GET_RELENTRY_INT64(vacuum_count) +/* pg_stat_get_vacuum_failsafe_count */ +PG_STAT_GET_RELENTRY_INT64(vacuum_failsafe_count) + #define PG_STAT_GET_RELENTRY_FLOAT8(stat) \ Datum \ CppConcat(pg_stat_get_,stat)(PG_FUNCTION_ARGS) \ @@ -1257,6 +1260,9 @@ PG_STAT_GET_DBENTRY_FLOAT8_MS(total_vacuum_delay_time) /* pg_stat_get_db_total_autovacuum_delay_time */ PG_STAT_GET_DBENTRY_FLOAT8_MS(total_autovacuum_delay_time) +/* pg_stat_get_db_vacuum_failsafe_count */ +PG_STAT_GET_DBENTRY_INT64(vacuum_failsafe_count) + /* pg_stat_get_db_session_time */ PG_STAT_GET_DBENTRY_FLOAT8_MS(session_time) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 479c0bb236e..b975a265a10 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5953,6 +5953,16 @@ proname => 'pg_stat_get_db_total_autovacuum_delay_time', provolatile => 's', proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', prosrc => 'pg_stat_get_db_total_autovacuum_delay_time' }, +{ oid => '8691', + descr => 'statistics: number of vacuums that entered the wraparound failsafe mode', + proname => 'pg_stat_get_vacuum_failsafe_count', provolatile => 's', + proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', + prosrc => 'pg_stat_get_vacuum_failsafe_count' }, +{ oid => '8692', + descr => 'statistics: number of vacuums in database that entered the wraparound failsafe mode', + proname => 'pg_stat_get_db_vacuum_failsafe_count', provolatile => 's', + proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', + prosrc => 'pg_stat_get_db_vacuum_failsafe_count' }, { oid => '6185', descr => 'statistics: session time, in milliseconds', proname => 'pg_stat_get_db_session_time', provolatile => 's', proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 7a17787b717..6a18582a486 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -410,6 +410,12 @@ typedef struct PgStat_StatDBEntry PgStat_Counter total_vacuum_delay_time; PgStat_Counter total_autovacuum_delay_time; + /* + * Number of vacuums in this database that entered the wraparound + * failsafe mode (see vacuum_failsafe_age). + */ + PgStat_Counter vacuum_failsafe_count; + TimestampTz stat_reset_timestamp; } PgStat_StatDBEntry; @@ -503,6 +509,12 @@ typedef struct PgStat_StatTabEntry PgStat_Counter total_vacuum_delay_time; PgStat_Counter total_autovacuum_delay_time; + /* + * Number of vacuums of this relation that entered the wraparound + * failsafe mode (see vacuum_failsafe_age). + */ + PgStat_Counter vacuum_failsafe_count; + TimestampTz stat_reset_time; } PgStat_StatTabEntry; @@ -731,7 +743,8 @@ extern void pgstat_unlink_relation(Relation rel); extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples, PgStat_Counter deadtuples, TimestampTz starttime, - PgStat_Counter delaytime); + PgStat_Counter delaytime, + bool failsafe); extern void pgstat_report_index_vacuum_time(Relation rel, PgStat_Counter elapsedtime, PgStat_Counter delaytime, diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build index 4429c780418..82348b896ba 100644 --- a/src/test/modules/test_misc/meson.build +++ b/src/test/modules/test_misc/meson.build @@ -24,6 +24,7 @@ tests += { 't/013_temp_obj_multisession.pl', 't/014_log_statement_max_length.pl', 't/016_index_vacuum_time.pl', + 't/017_vacuum_failsafe.pl', ], # The injection points are cluster-wide, so disable installcheck 'runningcheck': false, diff --git a/src/test/modules/test_misc/t/017_vacuum_failsafe.pl b/src/test/modules/test_misc/t/017_vacuum_failsafe.pl new file mode 100644 index 00000000000..7738650e278 --- /dev/null +++ b/src/test/modules/test_misc/t/017_vacuum_failsafe.pl @@ -0,0 +1,71 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test that vacuums entering the wraparound failsafe mode are counted in +# pg_stat_all_tables.vacuum_failsafe_count and aggregated per database in +# pg_stat_database.vacuum_failsafe_count. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +# The failsafe cutoff is clamped to 1.05 * autovacuum_freeze_max_age, so use +# the minimum allowed value to keep the number of XIDs to burn small. +$node->append_conf( + 'postgresql.conf', qq[ +autovacuum = off +autovacuum_freeze_max_age = 100000 +]); +$node->start; + +$node->safe_psql( + 'postgres', qq[ + CREATE TABLE tab_failsafe (i int); + INSERT INTO tab_failsafe SELECT generate_series(1, 100); +]); + +# A vacuum without failsafe pressure must not bump the counter. +$node->safe_psql('postgres', 'VACUUM tab_failsafe;'); +my $count = $node->safe_psql('postgres', + q[SELECT vacuum_failsafe_count FROM pg_stat_all_tables WHERE relname = 'tab_failsafe';] +); +is($count, '0', 'normal vacuum does not count as failsafe'); + +# Age the table past 1.05 * autovacuum_freeze_max_age: burn XIDs with +# aborted subtransactions (each aborted subxact consumes an assigned XID). +$node->safe_psql( + 'postgres', qq[ + CREATE TABLE burn_xids (i int); + DO \$\$ + BEGIN + FOR i IN 1..110000 LOOP + BEGIN + INSERT INTO burn_xids VALUES (1); + RAISE EXCEPTION 'burn'; + EXCEPTION WHEN OTHERS THEN + END; + END LOOP; + END \$\$; +]); + +# vacuum_failsafe_age = 0 makes the (clamped) cutoff kick in immediately. +$node->safe_psql( + 'postgres', qq[ + SET vacuum_failsafe_age = 0; + SET vacuum_multixact_failsafe_age = 0; + VACUUM tab_failsafe; +]); + +$count = $node->safe_psql('postgres', + q[SELECT vacuum_failsafe_count FROM pg_stat_all_tables WHERE relname = 'tab_failsafe';] +); +is($count, '1', 'failsafe vacuum counted in pg_stat_all_tables'); + +my $db_count = $node->safe_psql('postgres', + q[SELECT vacuum_failsafe_count FROM pg_stat_database WHERE datname = 'postgres';] +); +is($db_count, '1', 'failsafe vacuum counted in pg_stat_database'); + +done_testing(); diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index bd9eca0dcef..40d652e41f0 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1852,6 +1852,7 @@ pg_stat_all_tables| SELECT c.oid AS relid, pg_stat_get_total_autoanalyze_time(c.oid) AS total_autoanalyze_time, pg_stat_get_total_vacuum_delay_time(c.oid) AS total_vacuum_delay_time, pg_stat_get_total_autovacuum_delay_time(c.oid) AS total_autovacuum_delay_time, + pg_stat_get_vacuum_failsafe_count(c.oid) AS vacuum_failsafe_count, pg_stat_get_stat_reset_time(c.oid) AS stats_reset FROM ((pg_class c LEFT JOIN pg_index i ON ((c.oid = i.indrelid))) @@ -1923,6 +1924,7 @@ pg_stat_database| SELECT oid AS datid, pg_stat_get_db_total_autovacuum_time(oid) AS total_autovacuum_time, pg_stat_get_db_total_vacuum_delay_time(oid) AS total_vacuum_delay_time, pg_stat_get_db_total_autovacuum_delay_time(oid) AS total_autovacuum_delay_time, + pg_stat_get_db_vacuum_failsafe_count(oid) AS vacuum_failsafe_count, pg_stat_get_db_session_time(oid) AS session_time, pg_stat_get_db_active_time(oid) AS active_time, pg_stat_get_db_idle_in_transaction_time(oid) AS idle_in_transaction_time, @@ -2384,6 +2386,7 @@ pg_stat_sys_tables| SELECT relid, total_autoanalyze_time, total_vacuum_delay_time, total_autovacuum_delay_time, + vacuum_failsafe_count, stats_reset FROM pg_stat_all_tables WHERE ((schemaname = ANY (ARRAY['pg_catalog'::name, 'information_schema'::name])) OR (schemaname ~ '^pg_toast'::text)); @@ -2445,6 +2448,7 @@ pg_stat_user_tables| SELECT relid, total_autoanalyze_time, total_vacuum_delay_time, total_autovacuum_delay_time, + vacuum_failsafe_count, stats_reset FROM pg_stat_all_tables WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text)); -- 2.39.5 (Apple Git-154)