From 8f79d916423be1fe8ae925861c4154b40257b929 Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Mon, 20 Jul 2026 16:59:26 +0300 Subject: [PATCH v43 4/9] Count vacuums interrupted by errors in pg_stat_database. Add a vacuum_interrupt_count column to pg_stat_database: the number of times a vacuum in the database was interrupted by an ERROR. An interrupted vacuum never reaches the end-of-vacuum instrumentation or pgstat_report_vacuum(), so this is the only place the event becomes visible. The count is taken in the vacuum error callback, which runs inside the error handler, so it must not do anything that could fail or block. The geterrlevel() helper lets the callback tell a genuine ERROR apart from lower-severity reports that merely carry the vacuum error context. Authors: Alena Rybakina , Andrei Lepikhov , Andrei Zubkov --- doc/src/sgml/monitoring.sgml | 9 +++ src/backend/access/heap/vacuumlazy.c | 11 +++ src/backend/catalog/system_views.sql | 1 + src/backend/utils/activity/pgstat_database.c | 55 +++++++++++++- src/backend/utils/adt/pgstatfuncs.c | 3 + src/backend/utils/error/elog.c | 17 +++++ src/include/catalog/pg_proc.dat | 4 ++ src/include/pgstat.h | 4 ++ src/include/utils/elog.h | 1 + src/test/modules/test_misc/meson.build | 1 + .../test_misc/t/018_vacuum_interrupts.pl | 71 +++++++++++++++++++ src/test/regress/expected/rules.out | 1 + 12 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/test_misc/t/018_vacuum_interrupts.pl diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index cf0ffc644d6..28630e3d67a 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -4283,6 +4283,15 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + + vacuum_interrupt_count bigint + + + Number of times a vacuum in this database was interrupted by an error + + + session_time double precision diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 34f33593d94..241a10c0805 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -3851,6 +3851,17 @@ vacuum_error_callback(void *arg) { LVRelState *errinfo = arg; + /* + * If an actual ERROR (not a lower-severity report that merely carries + * this vacuum error context) is being raised while we have a relation in + * hand, record at the database level that a vacuum was interrupted. Any + * error here aborts the vacuum, so the exact phase does not matter. + * We are inside the error handler, so this only bumps a counter; the + * statistics are updated once the transaction ends. + */ + if (errinfo->rel != NULL && geterrlevel() == ERROR) + pgstat_count_vacuum_error(errinfo->rel->rd_rel->relisshared); + switch (errinfo->phase) { case VACUUM_ERRCB_PHASE_SCAN_HEAP: diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 934e688eb62..6130fdae3f8 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1184,6 +1184,7 @@ CREATE VIEW pg_stat_database AS 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_vacuum_interrupt_count(D.oid) AS vacuum_interrupt_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 3d9f90d1970..e21e85e8300 100644 --- a/src/backend/utils/activity/pgstat_database.c +++ b/src/backend/utils/activity/pgstat_database.c @@ -36,6 +36,13 @@ static int pgStatXactCommit = 0; static int pgStatXactRollback = 0; static PgStat_Counter pgLastSessionReportTime = 0; +/* + * Vacuums interrupted by an error, counted from the vacuum error callback + * and folded into the pending database entries at transaction end. + */ +static PgStat_Counter pgStatVacuumErrors = 0; +static PgStat_Counter pgStatSharedVacuumErrors = 0; + /* * Remove entry for the database being dropped. @@ -291,6 +298,20 @@ pgstat_fetch_stat_dbentry(Oid dboid) pgstat_fetch_entry(PGSTAT_KIND_DATABASE, dboid, InvalidOid, NULL); } +/* + * Add the vacuum errors counted for a database to its pending entry, and + * reset the counter. + */ +static void +pgstat_fold_vacuum_errors(Oid dboid, PgStat_Counter *count) +{ + if (*count == 0) + return; + + pgstat_prep_database_pending(dboid)->vacuum_interrupt_count += *count; + *count = 0; +} + void AtEOXact_PgStat_Database(bool isCommit, bool parallel) { @@ -306,6 +327,15 @@ AtEOXact_PgStat_Database(bool isCommit, bool parallel) else pgStatXactRollback++; } + + /* + * Fold in the vacuums that pgstat_count_vacuum_error() saw failing. We + * are past LWLockReleaseAll() of the aborting transaction here, so it is + * safe to touch the pending entries, which the next pgstat_report_stat() + * flushes to shared memory as usual. + */ + pgstat_fold_vacuum_errors(MyDatabaseId, &pgStatVacuumErrors); + pgstat_fold_vacuum_errors(InvalidOid, &pgStatSharedVacuumErrors); } /* @@ -390,8 +420,30 @@ pgstat_should_report_connstat(void) } /* - * Find or create a local PgStat_StatDBEntry entry for dboid. + * Count a vacuum that was interrupted by an error. + * + * This is called from the vacuum error callback, that is from inside the + * error handler, so it must not do anything that could fail or block: the + * error may well have been raised by the locking code itself, possibly + * while holding an LWLock. Just bump a counter here; the counter reaches + * the pending database entry in AtEOXact_PgStat_Database(), once the + * transaction end has released the locks we might have been holding. + * + * Vacuums of shared relations are counted in the InvalidOid entry, matching + * how their other stats are accounted. */ +void +pgstat_count_vacuum_error(bool shared) +{ + if (!pgstat_track_counts) + return; + + if (shared) + pgStatSharedVacuumErrors++; + else + pgStatVacuumErrors++; +} + PgStat_StatDBEntry * pgstat_prep_database_pending(Oid dboid) { @@ -485,6 +537,7 @@ pgstat_database_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) PGSTAT_ACCUM_DBCOUNT(total_vacuum_delay_time); PGSTAT_ACCUM_DBCOUNT(total_autovacuum_delay_time); PGSTAT_ACCUM_DBCOUNT(vacuum_failsafe_count); + PGSTAT_ACCUM_DBCOUNT(vacuum_interrupt_count); PGSTAT_ACCUM_DBCOUNT(sessions); PGSTAT_ACCUM_DBCOUNT(session_time); diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 5b47e6fc066..1af522c4d8a 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -1192,6 +1192,9 @@ PG_STAT_GET_DBENTRY_INT64(conflict_tablespace) /* pg_stat_get_db_deadlocks */ PG_STAT_GET_DBENTRY_INT64(deadlocks) +/* pg_stat_get_db_vacuum_interrupt_count */ +PG_STAT_GET_DBENTRY_INT64(vacuum_interrupt_count) + /* pg_stat_get_db_sessions */ PG_STAT_GET_DBENTRY_INT64(sessions) diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c index b9d2c96b97a..28a419ca16d 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -1781,6 +1781,23 @@ geterrcode(void) return edata->sqlerrcode; } +/* + * geterrlevel --- return the elevel of the error currently being constructed + * + * This is only intended for use in error callback subroutines, where it lets + * a callback tell a genuine error apart from a lower-severity report. + */ +int +geterrlevel(void) +{ + ErrorData *edata = &errordata[errordata_stack_depth]; + + /* we don't bother incrementing recursion_depth */ + CHECK_STACK_DEPTH(); + + return edata->elevel; +} + /* * geterrposition --- return the currently set error position (0 if none) * diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 63366ffd8ff..a482ed97a43 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5923,6 +5923,10 @@ proname => 'pg_stat_get_db_blk_write_time', provolatile => 's', proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', prosrc => 'pg_stat_get_db_blk_write_time' }, +{ oid => '8684', descr => 'statistics: number of vacuums interrupted by errors in database', + proname => 'pg_stat_get_db_vacuum_interrupt_count', provolatile => 's', + proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', + prosrc => 'pg_stat_get_db_vacuum_interrupt_count' }, { oid => '8685', descr => 'total vacuum delay time, in milliseconds', proname => 'pg_stat_get_total_vacuum_delay_time', provolatile => 's', proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 4e4f4f53bfd..218a9bfd93b 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -473,6 +473,9 @@ typedef struct PgStat_StatDBEntry */ PgStat_Counter vacuum_failsafe_count; + /* # of vacuums in this database interrupted by errors */ + PgStat_Counter vacuum_interrupt_count; + TimestampTz stat_reset_timestamp; } PgStat_StatDBEntry; @@ -835,6 +838,7 @@ extern void pgstat_report_index_vacuum_time(Relation rel, PgStat_Counter elapsedtime, PgStat_Counter delaytime, bool is_autovacuum); +extern void pgstat_count_vacuum_error(bool shared); extern void pgstat_report_analyze(Relation rel, PgStat_Counter livetuples, PgStat_Counter deadtuples, bool resetcounter, TimestampTz starttime); diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h index 6ae376ba001..7c1369524ef 100644 --- a/src/include/utils/elog.h +++ b/src/include/utils/elog.h @@ -230,6 +230,7 @@ extern int internalerrquery(const char *query); extern int err_generic_string(int field, const char *str); extern int geterrcode(void); +extern int geterrlevel(void); extern int geterrposition(void); extern int getinternalerrposition(void); diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build index 69713f835f8..7f259382d15 100644 --- a/src/test/modules/test_misc/meson.build +++ b/src/test/modules/test_misc/meson.build @@ -26,6 +26,7 @@ tests += { 't/015_temp_schema_exit_deferrable.pl', 't/016_index_vacuum_time.pl', 't/017_vacuum_failsafe.pl', + 't/018_vacuum_interrupts.pl', ], # The injection points are cluster-wide, so disable installcheck 'runningcheck': false, diff --git a/src/test/modules/test_misc/t/018_vacuum_interrupts.pl b/src/test/modules/test_misc/t/018_vacuum_interrupts.pl new file mode 100644 index 00000000000..370b7d18a87 --- /dev/null +++ b/src/test/modules/test_misc/t/018_vacuum_interrupts.pl @@ -0,0 +1,71 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test the vacuum_interrupt_count counter of pg_stat_database. +# +# vacuum_interrupt_count records how many times a vacuum in the database was +# interrupted by an error. We provoke that by starting a vacuum that sleeps +# at its cost-based delay points and canceling it, with pg_cancel_backend(), +# while it is still running. + +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; +$node->append_conf( + 'postgresql.conf', qq[ +autovacuum = off +]); +$node->start; + +# fillfactor = 10 spreads the rows over many pages so the vacuum hits enough +# cost-delay points to stay running until we cancel it. +$node->safe_psql( + 'postgres', qq[ +CREATE TABLE vacstat_int (id int) WITH (autovacuum_enabled = off, fillfactor = 10); +INSERT INTO vacstat_int SELECT generate_series(1, 1000); +DELETE FROM vacstat_int; +]); + +# Start a vacuum that sleeps at every cost-delay point, in the background. The +# \echo lets query_until() return as soon as the VACUUM has been launched. +my $appname = 'vacuum_interrupt_test'; +my $vac = $node->background_psql('postgres', on_error_stop => 0); +$vac->query_until( + qr/start/, qq[ +SET application_name = '$appname'; +SET vacuum_cost_delay = '100ms'; +SET vacuum_cost_limit = 1; +\\echo start +VACUUM vacstat_int; +]); + +# Wait until the vacuum is actually running, then cancel it. +$node->poll_query_until( + 'postgres', qq[ +SELECT count(*) = 1 FROM pg_stat_activity + WHERE application_name = '$appname' AND query LIKE 'VACUUM%' AND state = 'active']) + or die "timed out waiting for the vacuum to start"; + +my $cancelled = $node->safe_psql( + 'postgres', qq[ +SELECT pg_cancel_backend(pid) FROM pg_stat_activity + WHERE application_name = '$appname' AND query LIKE 'VACUUM%']); +is($cancelled, 't', 'canceled the running vacuum'); + +$vac->quit; +like($vac->{stderr}, qr/canceling statement due to user request/, + 'vacuum canceled by user request'); + +is( $node->safe_psql( + 'postgres', qq[ +SELECT vacuum_interrupt_count > 0 FROM pg_stat_database WHERE datname = current_database()]), + 't', + 'vacuum_interrupt_count advanced in pg_stat_database'); + +$node->stop; + +done_testing(); diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index f40c2d9e578..54ded539bba 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1925,6 +1925,7 @@ pg_stat_database| SELECT oid AS datid, 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_vacuum_interrupt_count(oid) AS vacuum_interrupt_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, -- 2.50.1 (Apple Git-155)