From 180ef463808023fa117595ce554326028724cf43 Mon Sep 17 00:00:00 2001 From: Naga Appani Date: Mon, 4 Aug 2025 03:17:28 +0000 Subject: [PATCH v2] Add pg_get_multixact_stats() SQL function for monitoring MultiXact usage This patch adds pg_get_multixact_stats(), a SQL-callable function that returns MultiXact statistics to aid in monitoring wraparound risk and vacuum behavior. It reports: multixacts: the number of MultiXact IDs created since the oldest one still needed members: the number of MultiXact member entries that currently exist oldest_multixact: the oldest MultiXact ID still needed by any database The function modifies ReadMultiXactCounts() to expose the oldestMultiXactId and returns all three values in a composite record. This allows users to monitor MultiXact usage and identify potential wraparound issues, particularly useful when combined with pg_get_multixact_members() to investigate specific MultiXacts. Usage: SELECT * FROM pg_get_multixact_stats(); Documentation is added to: - "Transaction ID and Snapshot Information Functions" section in func.sgml - "Multixacts and Wraparound" section in maintenance.sgml (routine-vacuuming.html#VACUUM-FOR-MULTIXACT-WRAPAROUND) Isolation tests are added to verify: - Initial state with zero MultiXacts - MultiXact creation with overlapping shared locks - Correct counting of MultiXacts and members - Proper tracking of oldest MultiXact ID Author: Naga Appani Reviewed-by: Ashutosh Bapat Reviewed-by: Michael Paquier Discussion: https://www.postgresql.org/message-id/flat/CAM2BeoX%2BRasKfG6W8w4qYZZz4BnhyEQMA_y5cEDnKEY_z8o9Czg%40mail.gmail.com --- doc/src/sgml/func.sgml | 32 ++++++++ doc/src/sgml/maintenance.sgml | 37 ++++++++- src/backend/access/transam/multixact.c | 79 ++++++++++++++----- src/include/catalog/pg_proc.dat | 14 ++++ .../isolation/expected/multixact_stats.out | 59 ++++++++++++++ src/test/isolation/specs/multixact_stats.spec | 35 ++++++++ 6 files changed, 233 insertions(+), 23 deletions(-) create mode 100644 src/test/isolation/expected/multixact_stats.out create mode 100644 src/test/isolation/specs/multixact_stats.spec diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 74a16af04ad..0255a51cdad 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -27732,6 +27732,38 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} modify key columns. + + + + + pg_get_multixact_stats + pg_get_multixact_stats () + record + ( multixacts integer, + members bigint, + oldest_multixact integer ) + + + + Returns statistics about current multixact usage: + multixacts is the number of multixact IDs assigned, + members is the number of multixact member entries created, + and oldest_multixact is the oldest multixact ID still in use. + These values can be used to monitor multixact consumption and anticipate + autovacuum behavior. See + for further details on multixact wraparound. + + + + SELECT * FROM pg_get_multixact_stats(); + + multixacts | members | oldest_multixact +------------+-------------+------------------ + 182371396 | 2826221174 | 754321 + + + + diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index e7a9f58c015..d6bd305b0b0 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -813,12 +813,41 @@ HINT: Execute a database-wide VACUUM in that database. As a safety device, an aggressive vacuum scan will occur for any table whose multixact-age is greater than . Also, if the - storage occupied by multixacts members exceeds about 10GB, aggressive vacuum + linkend="guc-autovacuum-multixact-freeze-max-age"/>. Also, if either + the storage occupied by multixact members exceeds about 10GB or the number + of members created exceeds approximately 2 billion entries, aggressive vacuum scans will occur more often for all tables, starting with those that - have the oldest multixact-age. Both of these kinds of aggressive + have the oldest multixact-age. Both of these kinds of aggressive scans will occur even if autovacuum is nominally disabled. The members storage - area can grow up to about 20GB before reaching wraparound. + area can grow up to about 20GB or approximately 4 billion entries before + reaching wraparound. + + + + The pg_get_multixact_stats() function provides a way + to monitor multixact allocation and usage patterns in real time. By exposing + the age of the oldest multixact ID, number of member entries, and the oldest multixact ID still in use, it helps: + + + + Identify unusual multixact activity from concurrent row-level locks + or foreign key operations + + + + + Monitor progress toward wraparound thresholds that trigger aggressive + autovacuum (approximately 2 billion members or 10GB storage) + + + + + Verify whether autovacuum is effectively managing multixact cleanup + before reaching critical thresholds + + + + See for details. diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c index 3cb09c3d598..59e8fc17b7f 100644 --- a/src/backend/access/transam/multixact.c +++ b/src/backend/access/transam/multixact.c @@ -2863,28 +2863,27 @@ find_multixact_start(MultiXactId multi, MultiXactOffset *result) * exist. Return false if unable to determine. */ static bool -ReadMultiXactCounts(uint32 *multixacts, MultiXactOffset *members) +ReadMultiXactCounts(uint32 *multixacts, MultiXactOffset *members, MultiXactId *oldestMultiXactId) { - MultiXactOffset nextOffset; - MultiXactOffset oldestOffset; - MultiXactId oldestMultiXactId; - MultiXactId nextMultiXactId; - bool oldestOffsetKnown; + MultiXactOffset nextOffset; + MultiXactOffset oldestOffset; + MultiXactId nextMultiXactId; + bool oldestOffsetKnown; - LWLockAcquire(MultiXactGenLock, LW_SHARED); - nextOffset = MultiXactState->nextOffset; - oldestMultiXactId = MultiXactState->oldestMultiXactId; - nextMultiXactId = MultiXactState->nextMXact; - oldestOffset = MultiXactState->oldestOffset; - oldestOffsetKnown = MultiXactState->oldestOffsetKnown; - LWLockRelease(MultiXactGenLock); + LWLockAcquire(MultiXactGenLock, LW_SHARED); + nextOffset = MultiXactState->nextOffset; + *oldestMultiXactId = MultiXactState->oldestMultiXactId; /* Use the parameter directly */ + nextMultiXactId = MultiXactState->nextMXact; + oldestOffset = MultiXactState->oldestOffset; + oldestOffsetKnown = MultiXactState->oldestOffsetKnown; + LWLockRelease(MultiXactGenLock); - if (!oldestOffsetKnown) - return false; + if (!oldestOffsetKnown) + return false; - *members = nextOffset - oldestOffset; - *multixacts = nextMultiXactId - oldestMultiXactId; - return true; + *members = nextOffset - oldestOffset; + *multixacts = nextMultiXactId - *oldestMultiXactId; /* Use the parameter */ + return true; } /* @@ -2922,9 +2921,10 @@ MultiXactMemberFreezeThreshold(void) uint32 victim_multixacts; double fraction; int result; + MultiXactId oldestMultiXactId; /* If we can't determine member space utilization, assume the worst. */ - if (!ReadMultiXactCounts(&multixacts, &members)) + if (!ReadMultiXactCounts(&multixacts, &members, &oldestMultiXactId)) return 0; /* If member space utilization is low, no special action is required. */ @@ -3493,3 +3493,44 @@ multixactmemberssyncfiletag(const FileTag *ftag, char *path) { return SlruSyncFileTag(MultiXactMemberCtl, ftag, path); } + +/* + * pg_get_multixact_stats + * + * SQL-callable function to retrieve MultiXact statistics. + * + * Returns a composite row containing: + * - total number of MultiXact IDs created since startup, + * - total number of MultiXact members created, + * - the oldest existing MultiXact ID. + * + * This is primarily useful for monitoring MultiXact usage and ensuring + * appropriate wraparound protection. + */ +Datum +pg_get_multixact_stats(PG_FUNCTION_ARGS) +{ + TupleDesc tupdesc; + Datum values[3]; + bool nulls[3] = {false, false, false}; + MultiXactOffset members; + MultiXactId oldestMultiXactId; + uint32 multixacts; + HeapTuple tuple; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + ereport(ERROR, + (errmsg("return type must be a row type"))); + + if (!ReadMultiXactCounts(&multixacts, &members, &oldestMultiXactId)) + PG_RETURN_NULL(); + + values[0] = UInt32GetDatum(multixacts); + values[1] = UInt32GetDatum(members); + values[2] = UInt32GetDatum(oldestMultiXactId); + + tuple = heap_form_tuple(tupdesc, values, nulls); + + PG_RETURN_DATUM(HeapTupleGetDatum(tuple)); +} + diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 3ee8fed7e53..756ba39425c 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12576,4 +12576,18 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +{ + oid => '9001', + descr => 'get current multixact member and multixact ID counts and oldest multixact', + proname => 'pg_get_multixact_stats', + prorettype => 'record', + proargtypes => '', + proallargtypes => '{int4,int8,int4}', + proargmodes => '{o,o,o}', + proargnames => '{multixacts,members,oldest_multixact}', + provolatile => 'v', + proparallel => 's', + prosrc => 'pg_get_multixact_stats' +}, + ] diff --git a/src/test/isolation/expected/multixact_stats.out b/src/test/isolation/expected/multixact_stats.out new file mode 100644 index 00000000000..54e3238d727 --- /dev/null +++ b/src/test/isolation/expected/multixact_stats.out @@ -0,0 +1,59 @@ +Parsed test spec with 3 sessions + +starting permutation: stats_init check begin1 lock1 begin2 lock2 check commit1 commit2 check +step stats_init: + CREATE TEMP TABLE stats_before AS + SELECT multixacts, members, oldest_multixact FROM pg_get_multixact_stats(); + +step check: + SELECT + multixacts, + members, + oldest_multixact + FROM pg_get_multixact_stats(); + +multixacts|members|oldest_multixact +----------+-------+---------------- + 0| 0| 1 +(1 row) + +step begin1: BEGIN; +step lock1: SELECT * FROM multixact_test WHERE id = 1 FOR SHARE; +id|val +--+--- + 1| 10 +(1 row) + +step begin2: BEGIN; +step lock2: SELECT * FROM multixact_test WHERE id = 1 FOR SHARE; +id|val +--+--- + 1| 10 +(1 row) + +step check: + SELECT + multixacts, + members, + oldest_multixact + FROM pg_get_multixact_stats(); + +multixacts|members|oldest_multixact +----------+-------+---------------- + 1| 3| 1 +(1 row) + +step commit1: COMMIT; +step commit2: COMMIT; +step check: + SELECT + multixacts, + members, + oldest_multixact + FROM pg_get_multixact_stats(); + +multixacts|members|oldest_multixact +----------+-------+---------------- + 1| 3| 1 +(1 row) + diff --git a/src/test/isolation/specs/multixact_stats.spec b/src/test/isolation/specs/multixact_stats.spec new file mode 100644 index 00000000000..53fcad38c54 --- /dev/null +++ b/src/test/isolation/specs/multixact_stats.spec @@ -0,0 +1,35 @@ +setup +{ + CREATE TABLE multixact_test(id int PRIMARY KEY, val int); + INSERT INTO multixact_test VALUES (1, 10); +} + +teardown +{ + DROP TABLE multixact_test; +} + +session s1 +step begin1 { BEGIN; } +step lock1 { SELECT * FROM multixact_test WHERE id = 1 FOR SHARE; } +step commit1 { COMMIT; } + +session s2 +step begin2 { BEGIN; } +step lock2 { SELECT * FROM multixact_test WHERE id = 1 FOR SHARE; } +step commit2 { COMMIT; } + +session s3 +step stats_init { + CREATE TEMP TABLE stats_before AS + SELECT multixacts, members, oldest_multixact FROM pg_get_multixact_stats(); +} +step check { + SELECT + multixacts, + members, + oldest_multixact + FROM pg_get_multixact_stats(); +} + +permutation stats_init check begin1 lock1 begin2 lock2 check commit1 commit2 check -- 2.47.3