From 71989a7bdc2f330f9d6f8aff07a4fba216df9005 Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Wed, 17 Jun 2026 21:32:19 +0300 Subject: [PATCH 5/9] Extended vacuum statistics: interrupted vacuums and the wraparound failsafe Expose two database-/relation-level reliability counters with documentation and regression coverage: wraparound_failsafe whether (per relation) or how many times (per database) a vacuum engaged the wraparound failsafe interrupts_count number of times a vacuum of a table in the database was interrupted by an error (database aggregate only) The wraparound failsafe is recorded as a per-relation flag and summed into a count at the database level. interrupts_count is reported from the vacuum error callback straight to shared memory, because an interrupted vacuum aborts its transaction and a pending entry might never be flushed; it is a database-wide counter only. The positive wraparound_failsafe path is covered by a TAP test under src/test/modules/xid_wraparound and the interrupted-vacuum path by a TAP test under src/test/modules/test_misc. --- doc/src/sgml/system-views.sgml | 25 +++++++ src/backend/access/heap/vacuumlazy.c | 20 ++++-- src/backend/catalog/system_views.sql | 5 +- src/backend/utils/activity/pgstat_vacuum.c | 46 ++++++++++++ src/backend/utils/adt/pgstatfuncs.c | 8 ++- src/backend/utils/error/elog.c | 17 +++++ src/include/catalog/pg_proc.dat | 12 ++-- src/include/pgstat.h | 7 ++ src/include/utils/elog.h | 1 + src/test/modules/test_misc/meson.build | 1 + .../t/015_vacuum_stats_interrupts.pl | 71 +++++++++++++++++++ src/test/modules/xid_wraparound/meson.build | 1 + .../t/005_vacuum_stats_failsafe.pl | 66 +++++++++++++++++ src/test/regress/expected/rules.out | 9 ++- src/test/regress/expected/vacuum_stats.out | 18 ++++- src/test/regress/sql/vacuum_stats.sql | 8 +++ 16 files changed, 295 insertions(+), 20 deletions(-) create mode 100644 src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl create mode 100644 src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml index 7c95dcea6b..8bca17f3ef 100644 --- a/doc/src/sgml/system-views.sgml +++ b/doc/src/sgml/system-views.sgml @@ -5892,6 +5892,14 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx Number of dead tuples that the vacuum could not remove because it failed to acquire a cleanup lock on their page. + + + wraparound_failsafe integer + + + Number of times the failsafe mechanism was triggered to prevent transaction ID wraparound during the vacuum. + + wal_records bigint @@ -6089,6 +6097,14 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx Number of vacuum operations in this database that failed with an error. + + + wraparound_failsafe integer + + + Number of vacuum operations in this database that engaged the wraparound failsafe mechanism. + + wal_records bigint @@ -6113,6 +6129,15 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx Total amount of WAL generated by vacuum operations in this database, in bytes. + + + interrupts_count integer + + + Number of times a vacuum of a table in this database was interrupted by + an error. + + diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index c06c538f57..5089656c18 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -412,9 +412,8 @@ typedef struct LVRelState */ BlockNumber eager_scan_remaining_fails; - int32 wraparound_failsafe_count; /* number of emergency vacuums to - * prevent anti-wraparound - * shutdown */ + bool wraparound_failsafe; /* did this vacuum engage the + * wraparound failsafe? */ PgStat_VacuumRelationCounts extVacReportIdx; @@ -630,6 +629,8 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount extVacStats->table.recently_dead_tuples = vacrel->recently_dead_tuples; extVacStats->table.missed_dead_tuples = vacrel->missed_dead_tuples; extVacStats->table.missed_dead_pages = vacrel->missed_dead_pages; + extVacStats->common.wraparound_failsafe_count = vacrel->wraparound_failsafe; + extVacStats->common.wal_bytes -= vacrel->extVacReportIdx.common.wal_bytes; extVacStats->common.wal_fpi -= vacrel->extVacReportIdx.common.wal_fpi; extVacStats->common.wal_records -= vacrel->extVacReportIdx.common.wal_records; @@ -1030,7 +1031,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params, vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs); vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel); vacrel->vistest = GlobalVisTestFor(rel); - vacrel->wraparound_failsafe_count = 0; + vacrel->wraparound_failsafe = false; /* Initialize state used to track oldest extant XID/MXID */ vacrel->NewRelfrozenXid = vacrel->cutoffs.OldestXmin; @@ -3160,7 +3161,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel) int64 progress_val[3] = {0, 0, PROGRESS_VACUUM_MODE_FAILSAFE}; VacuumFailsafeActive = true; - vacrel->wraparound_failsafe_count++; + vacrel->wraparound_failsafe = true; /* * Abandon use of a buffer access strategy to allow use of all of @@ -4113,6 +4114,15 @@ 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. + */ + if (errinfo->rel != NULL && geterrlevel() == ERROR) + pgstat_report_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 47d6aa6aea..837e78d292 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1573,6 +1573,7 @@ CREATE VIEW pg_stat_vacuum_tables AS S.recently_dead_tuples AS recently_dead_tuples, S.missed_dead_pages AS missed_dead_pages, S.missed_dead_tuples AS missed_dead_tuples, + S.wraparound_failsafe AS wraparound_failsafe, S.wal_records AS wal_records, S.wal_fpi AS wal_fpi, S.wal_bytes AS wal_bytes @@ -1611,9 +1612,11 @@ CREATE VIEW pg_stat_vacuum_database AS S.errors AS errors, + S.wraparound_failsafe AS wraparound_failsafe, S.wal_records AS wal_records, S.wal_fpi AS wal_fpi, - S.wal_bytes AS wal_bytes + S.wal_bytes AS wal_bytes, + S.interrupts_count AS interrupts_count FROM pg_database D, LATERAL pg_stat_get_vacuum_database(D.oid) S; diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c index 99c4932ded..8e099f3ade 100644 --- a/src/backend/utils/activity/pgstat_vacuum.c +++ b/src/backend/utils/activity/pgstat_vacuum.c @@ -43,6 +43,7 @@ pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *sr ACCUMULATE_FIELD(wal_bytes); ACCUMULATE_FIELD(tuples_deleted); + ACCUMULATE_FIELD(interrupts_count); } /* @@ -64,6 +65,13 @@ pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst, pgstat_accumulate_common(&dst->common, &src->common); + /* + * The wraparound failsafe is a per-relation flag (0/1), not a running + * count: reflect whether the latest vacuum of this relation engaged it, + * rather than summing across vacuums. + */ + dst->common.wraparound_failsafe_count = src->common.wraparound_failsafe_count; + if (dst->type == PGSTAT_EXTVAC_TABLE) { ACCUMULATE_SUBFIELD(table, pages_scanned); @@ -90,6 +98,12 @@ pgstat_accumulate_extvac_stats_db(PgStat_VacuumDBCounts *dst, return; pgstat_accumulate_common(&dst->common, &src->common); + + /* + * At the database level the failsafe is a count: how many relation vacuums + * engaged the wraparound failsafe. + */ + dst->common.wraparound_failsafe_count += src->common.wraparound_failsafe_count; dst->errors += src->errors; } @@ -126,6 +140,38 @@ pgstat_report_vacuum_extstats(Oid tableoid, bool shared, dboid, InvalidOid, NULL); dbpending = (PgStat_VacuumDBCounts *) entry_ref->pending; pgstat_accumulate_common(&dbpending->common, ¶ms->common); + /* count this relation's failsafe flag into the database-wide total */ + dbpending->common.wraparound_failsafe_count += params->common.wraparound_failsafe_count; +} + +/* + * Report that a vacuum was interrupted by an error. + * + * This is a database-wide counter only: an interrupted vacuum aborts its + * transaction, so reporting per-relation would require creating a relation + * stats entry from the error path (which the aborting transaction may roll + * back). Called from the vacuum error callback, we therefore update shared + * memory directly rather than going through pending entries, which might never + * be flushed. + * + * The database id is InvalidOid for shared relations, just as in + * pgstat_report_vacuum_extstats(); it must not be hard-coded to MyDatabaseId. + */ +void +pgstat_report_vacuum_error(bool shared) +{ + PgStat_EntryRef *entry_ref; + PgStatShared_VacuumDB *shdbentry; + Oid dboid = (shared ? InvalidOid : MyDatabaseId); + + if (!pgstat_track_vacuum_statistics) + return; + + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_VACUUM_DB, + dboid, InvalidOid, false); + shdbentry = (PgStatShared_VacuumDB *) entry_ref->shared_stats; + shdbentry->stats.common.interrupts_count++; + pgstat_unlock_entry(entry_ref); } /* diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index aad4427469..7927668bf8 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS) Datum pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS) { -#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 11 +#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 12 Oid relid = PG_GETARG_OID(0); PgStat_VacuumRelationCounts *extvacuum; @@ -2408,6 +2408,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS) values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples); values[i++] = Int64GetDatum(extvacuum->table.missed_dead_pages); values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples); + values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count); values[i++] = Int64GetDatum(extvacuum->common.wal_records); values[i++] = Int64GetDatum(extvacuum->common.wal_fpi); snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes); @@ -2476,7 +2477,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS) Datum pg_stat_get_vacuum_database(PG_FUNCTION_ARGS) { -#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 5 +#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 7 Oid dbid = PG_GETARG_OID(0); PgStat_VacuumDBCounts *extvacuum; @@ -2500,6 +2501,8 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS) values[i++] = Int32GetDatum(extvacuum->errors); + values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count); + values[i++] = Int64GetDatum(extvacuum->common.wal_records); values[i++] = Int64GetDatum(extvacuum->common.wal_fpi); snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes); @@ -2507,6 +2510,7 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS) CStringGetDatum(buf), ObjectIdGetDatum(0), Int32GetDatum(-1)); + values[i++] = Int32GetDatum(extvacuum->common.interrupts_count); Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS); /* Returns the record as Datum */ diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c index a6936a0c66..04f5b0a819 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -1815,6 +1815,23 @@ getinternalerrposition(void) return edata->internalpos; } +/* + * 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; +} + /* * Functions to allow construction of error message strings separately from diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 3dd3d81a13..51cd0e5ed1 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12643,9 +12643,9 @@ proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f', proretset => 't', proargtypes => 'oid', - proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,numeric}', - proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}', - proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,wal_records,wal_fpi,wal_bytes}', + proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}', + proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}', prosrc => 'pg_stat_get_vacuum_tables' } # oid8 related functions @@ -12727,8 +12727,8 @@ proname => 'pg_stat_get_vacuum_database', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f', proretset => 't', proargtypes => 'oid', - proallargtypes => '{oid,oid,int4,int8,int8,numeric}', - proargmodes => '{i,o,o,o,o,o}', - proargnames => '{dbid,dboid,errors,wal_records,wal_fpi,wal_bytes}', + proallargtypes => '{oid,oid,int4,int4,int8,int8,numeric,int4}', + proargmodes => '{i,o,o,o,o,o,o,o}', + proargnames => '{dbid,dboid,errors,wraparound_failsafe,wal_records,wal_fpi,wal_bytes,interrupts_count}', prosrc => 'pg_stat_get_vacuum_database' }, ] diff --git a/src/include/pgstat.h b/src/include/pgstat.h index f704a15003..d041359e72 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -182,6 +182,12 @@ typedef struct PgStat_CommonCounts /* tuples */ int64 tuples_deleted; + + /* failsafe */ + int32 wraparound_failsafe_count; + + /* number of times a vacuum of the object was interrupted by an error */ + int32 interrupts_count; } PgStat_CommonCounts; /* ---------- @@ -934,6 +940,7 @@ extern void pgstat_vacuum_relation_delete_pending_cb(Oid relid); extern void pgstat_report_vacuum_extstats(Oid tableoid, bool shared, PgStat_VacuumRelationCounts * params); +extern void pgstat_report_vacuum_error(bool shared); extern PgStat_VacuumRelationCounts * pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid); extern PgStat_VacuumDBCounts * pgstat_fetch_stat_vacuum_dbentry(Oid dbid); diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h index 6ae376ba00..7c1369524e 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 805c6c2c39..8761d79d70 100644 --- a/src/test/modules/test_misc/meson.build +++ b/src/test/modules/test_misc/meson.build @@ -23,6 +23,7 @@ tests += { 't/012_ddlutils.pl', 't/013_temp_obj_multisession.pl', 't/014_vacuum_stats.pl', + 't/015_vacuum_stats_interrupts.pl', 't/016_vacuum_stats_parallel.pl', ], # The injection points are cluster-wide, so disable installcheck diff --git a/src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl b/src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl new file mode 100644 index 0000000000..80dd2b7b3c --- /dev/null +++ b/src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl @@ -0,0 +1,71 @@ +# Copyright (c) 2024-2026, PostgreSQL Global Development Group + +# Test the interrupts_count counter of pg_stat_vacuum_database. +# +# interrupts_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 +track_vacuum_statistics = on +]); +$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 interrupts_count > 0 FROM pg_stat_vacuum_database WHERE dbname = current_database()]), + 't', + 'interrupts_count advanced in pg_stat_vacuum_database'); + +$node->stop; +done_testing(); diff --git a/src/test/modules/xid_wraparound/meson.build b/src/test/modules/xid_wraparound/meson.build index 97ce670f9a..9224e59d1d 100644 --- a/src/test/modules/xid_wraparound/meson.build +++ b/src/test/modules/xid_wraparound/meson.build @@ -31,6 +31,7 @@ tests += { 't/002_limits.pl', 't/003_wraparounds.pl', 't/004_notify_freeze.pl', + 't/005_vacuum_stats_failsafe.pl', ], }, } diff --git a/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl b/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl new file mode 100644 index 0000000000..eff77fe876 --- /dev/null +++ b/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl @@ -0,0 +1,66 @@ +# Copyright (c) 2024-2026, PostgreSQL Global Development Group + +# Test that the wraparound failsafe counter exposed by the extended vacuum +# statistics views advances when a VACUUM engages the wraparound failsafe. +# +# The failsafe only triggers once a relation's age exceeds +# max(vacuum_failsafe_age, autovacuum_freeze_max_age * 1.05), which cannot be +# reached by an ordinary regression test. Here we lower +# autovacuum_freeze_max_age to its minimum and use the xid_wraparound +# extension to burn enough transaction IDs to age the table past that +# threshold, then check that the wraparound_failsafe counters advance. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bxid_wraparound\b/) +{ + plan skip_all => "test xid_wraparound not enabled in PG_TEST_EXTRA"; +} + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq[ +autovacuum = off +track_vacuum_statistics = on +# Lower the wraparound-failsafe threshold as far as possible so that a modest +# number of consumed XIDs is enough to engage the failsafe. +autovacuum_freeze_max_age = 100000 +vacuum_failsafe_age = 0 +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION xid_wraparound'); + +# A table whose relfrozenxid age we will push past the failsafe threshold. +$node->safe_psql( + 'postgres', qq[ +CREATE TABLE fs_tab (id int) WITH (autovacuum_enabled = off); +INSERT INTO fs_tab SELECT generate_series(1, 1000); +]); + +# Advance the XID counter well past the failsafe threshold. +$node->safe_psql('postgres', 'SELECT consume_xids(200000)'); + +# This VACUUM must engage the wraparound failsafe. +$node->safe_psql('postgres', 'VACUUM fs_tab'); + +# The per-table view records that the failsafe was engaged for this relation. +my $tab = $node->safe_psql( + 'postgres', qq[ +SELECT wraparound_failsafe > 0 + FROM pg_stat_vacuum_tables WHERE relname = 'fs_tab']); +is($tab, 't', 'wraparound_failsafe advanced in pg_stat_vacuum_tables'); + +# The per-database aggregate counts the failsafe as well. +my $db = $node->safe_psql( + 'postgres', qq[ +SELECT wraparound_failsafe > 0 + FROM pg_stat_vacuum_database WHERE dbname = current_database()]); +is($db, 't', 'wraparound_failsafe advanced in pg_stat_vacuum_database'); + +$node->stop; +done_testing(); diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 9b35f0779e..467c2e1843 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -2424,11 +2424,13 @@ pg_stat_user_tables| SELECT relid, pg_stat_vacuum_database| SELECT d.oid AS dboid, d.datname AS dbname, s.errors, + s.wraparound_failsafe, s.wal_records, s.wal_fpi, - s.wal_bytes + s.wal_bytes, + s.interrupts_count FROM pg_database d, - LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, wal_records, wal_fpi, wal_bytes); + LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, wraparound_failsafe, wal_records, wal_fpi, wal_bytes, interrupts_count); pg_stat_vacuum_indexes| SELECT c.oid AS relid, i.oid AS indexrelid, n.nspname AS schemaname, @@ -2455,12 +2457,13 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname, s.recently_dead_tuples, s.missed_dead_pages, s.missed_dead_tuples, + s.wraparound_failsafe, s.wal_records, s.wal_fpi, s.wal_bytes FROM (pg_class c JOIN pg_namespace n ON ((n.oid = c.relnamespace))), - LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, wal_records, wal_fpi, wal_bytes) + LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, wraparound_failsafe, wal_records, wal_fpi, wal_bytes) WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"])); pg_stat_wal| SELECT wal_records, wal_fpi, diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out index 64639c75dc..c3024e5fc4 100644 --- a/src/test/regress/expected/vacuum_stats.out +++ b/src/test/regress/expected/vacuum_stats.out @@ -122,6 +122,17 @@ SELECT wal_records > 0 AS wal_records, (1 row) DROP TABLE vacstat_fpi; +-- wraparound failsafe. A normal vacuum does not engage the wraparound +-- failsafe (wraparound_failsafe = 0). The positive path requires reaching the +-- failsafe XID age and is covered by a separate TAP test under +-- src/test/modules/xid_wraparound. +SELECT wraparound_failsafe = 0 AS wraparound_failsafe + FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t'; + wraparound_failsafe +--------------------- + t +(1 row) + -- per-index view: the primary key index is processed by the same VACUUM. -- No btree leaf empties out (interleaved deletions), so pages_deleted = 0, -- while every index entry for a removed heap tuple is deleted. @@ -171,13 +182,14 @@ DROP TABLE vacstat_idxdel; -- per-database aggregate view: no vacuum errors occurred in this database, and -- the vacuums in this database emit WAL (wal_records > 0). SELECT errors = 0 AS errors, + wraparound_failsafe = 0 AS wraparound_failsafe, wal_records > 0 AS wal_records, wal_fpi >= 0 AS wal_fpi, wal_bytes > 0 AS wal_bytes FROM pg_stat_vacuum_database WHERE dbname = current_database(); - errors | wal_records | wal_fpi | wal_bytes ---------+-------------+---------+----------- - t | t | t | t + errors | wraparound_failsafe | wal_records | wal_fpi | wal_bytes +--------+---------------------+-------------+---------+----------- + t | t | t | t | t (1 row) -- parallel index vacuum: index statistics must be captured for indexes diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql index 13491b87f0..8ce60dac77 100644 --- a/src/test/regress/sql/vacuum_stats.sql +++ b/src/test/regress/sql/vacuum_stats.sql @@ -80,6 +80,13 @@ SELECT wal_records > 0 AS wal_records, FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi'; DROP TABLE vacstat_fpi; +-- wraparound failsafe. A normal vacuum does not engage the wraparound +-- failsafe (wraparound_failsafe = 0). The positive path requires reaching the +-- failsafe XID age and is covered by a separate TAP test under +-- src/test/modules/xid_wraparound. +SELECT wraparound_failsafe = 0 AS wraparound_failsafe + FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t'; + -- per-index view: the primary key index is processed by the same VACUUM. -- No btree leaf empties out (interleaved deletions), so pages_deleted = 0, -- while every index entry for a removed heap tuple is deleted. @@ -111,6 +118,7 @@ DROP TABLE vacstat_idxdel; -- per-database aggregate view: no vacuum errors occurred in this database, and -- the vacuums in this database emit WAL (wal_records > 0). SELECT errors = 0 AS errors, + wraparound_failsafe = 0 AS wraparound_failsafe, wal_records > 0 AS wal_records, wal_fpi >= 0 AS wal_fpi, wal_bytes > 0 AS wal_bytes -- 2.39.5 (Apple Git-154)