From b1af9724b6553a17ba42bab4df5f595428bcdbf8 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 22 Apr 2023 13:04:13 -0700
Subject: [PATCH v1 8/9] Overhaul freezing and wraparound docs.

This is almost a complete rewrite.  "Preventing Transaction ID
Wraparound Failures" becomes "Freezing to manage the transaction ID
space".  This is follow-up work to commit 1de58df4, which added
page-level freezing to VACUUM.

The emphasis is now on the physical work of freezing pages.  This flows
a little better than it otherwise would due to recent structural
cleanups to maintenance.sgml; discussion about freezing now immediately
follows discussion of cleanup of dead tuples.  We still talk about the
problem of the system activating xidStopLimit protections in the same
section, but we use much less alarmist language about data corruption,
and are no longer overly concerned about the very worst case.  We don't
rescind the recommendation that users recover from an xidStopLimit
outage by using single user mode, though that seems like something we
should aim to do in the near future.

There is no longer a separate sect3 to discuss MultiXactId related
issues.  VACUUM now performs exactly the same processing steps when it
freezes a page, independent of the trigger condition.

Also describe the page-level freezing FPI optimization added by commit
1de58df4.  This is expected to trigger the majority of all freezing with
many types of workloads.
---
 doc/src/sgml/config.sgml                  |  24 +-
 doc/src/sgml/logicaldecoding.sgml         |   2 +-
 doc/src/sgml/maintenance.sgml             | 602 ++++++++++++++--------
 doc/src/sgml/ref/create_table.sgml        |   2 +-
 doc/src/sgml/ref/prepare_transaction.sgml |   2 +-
 doc/src/sgml/ref/vacuum.sgml              |   6 +-
 doc/src/sgml/ref/vacuumdb.sgml            |   4 +-
 doc/src/sgml/xact.sgml                    |   4 +-
 8 files changed, 399 insertions(+), 247 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 091a79d4f..4c1cc0e8e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8394,7 +8394,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         Note that even when this parameter is disabled, the system
         will launch autovacuum processes if necessary to
         prevent transaction ID wraparound.  See <xref
-        linkend="vacuum-for-wraparound"/> for more information.
+        linkend="freezing-xid-space"/> for more information.
        </para>
       </listitem>
      </varlistentry>
@@ -8583,7 +8583,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         This parameter can only be set at server start, but the setting
         can be reduced for individual tables by
         changing table storage parameters.
-        For more information see <xref linkend="vacuum-for-wraparound"/>.
+        For more information see <xref linkend="freezing-xid-space"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -8612,7 +8612,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         400 million multixacts.
         This parameter can only be set at server start, but the setting can
         be reduced for individual tables by changing table storage parameters.
-        For more information see <xref linkend="vacuum-for-multixact-wraparound"/>.
+        For more information see <xref linkend="aggressive-strategy"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9319,7 +9319,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         periodic manual <command>VACUUM</command> has a chance to run before an
         anti-wraparound autovacuum is launched for the table. For more
         information see
-        <xref linkend="vacuum-for-wraparound"/>.
+        <xref linkend="freezing-xid-space"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9341,7 +9341,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         the value of <xref linkend="guc-autovacuum-freeze-max-age"/>, so
         that there is not an unreasonably short time between forced
         autovacuums.  For more information see <xref
-        linkend="vacuum-for-wraparound"/>.
+        linkend="freezing-xid-space"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9357,8 +9357,9 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         Specifies the maximum age (in transactions) that a table's
         <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>
         field can attain before <command>VACUUM</command> takes
-        extraordinary measures to avoid system-wide transaction ID
-        wraparound failure.  This is <command>VACUUM</command>'s
+        extraordinary measures to avoid
+        <link linkend="xid-stop-limit">having the system refuse to
+         allocate new transaction IDs</link>.  This is <command>VACUUM</command>'s
         strategy of last resort.  The failsafe typically triggers
         when an autovacuum to prevent transaction ID wraparound has
         already been running for some time, though it's possible for
@@ -9402,7 +9403,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         <xref linkend="guc-autovacuum-multixact-freeze-max-age"/>, so that a
         periodic manual <command>VACUUM</command> has a chance to run before an
         anti-wraparound is launched for the table.
-        For more information see <xref linkend="vacuum-for-multixact-wraparound"/>.
+        For more information see <xref linkend="aggressive-strategy"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9423,7 +9424,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         the value of <xref linkend="guc-autovacuum-multixact-freeze-max-age"/>,
         so that there is not an unreasonably short time between forced
         autovacuums.
-        For more information see <xref linkend="vacuum-for-multixact-wraparound"/>.
+        For more information see <xref linkend="freezing-xid-space"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9439,8 +9440,9 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         Specifies the maximum age (in multixacts) that a table's
         <structname>pg_class</structname>.<structfield>relminmxid</structfield>
         field can attain before <command>VACUUM</command> takes
-        extraordinary measures to avoid system-wide multixact ID
-        wraparound failure.  This is <command>VACUUM</command>'s
+        extraordinary measures to avoid
+        <link linkend="xid-stop-limit">having the system refuse to
+         allocate new MultiXactIDs</link>.  This is <command>VACUUM</command>'s
         strategy of last resort.  The failsafe typically triggers when
         an autovacuum to prevent transaction ID wraparound has already
         been running for some time, though it's possible for the
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cbd3aa804..80dade3be 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -353,7 +353,7 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       because neither required WAL nor required rows from the system catalogs
       can be removed by <command>VACUUM</command> as long as they are required by a replication
       slot.  In extreme cases this could cause the database to shut down to prevent
-      transaction ID wraparound (see <xref linkend="vacuum-for-wraparound"/>).
+      transaction ID wraparound (see <xref linkend="freezing-xid-space"/>).
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index 7476e5922..169c6f41a 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -275,15 +275,21 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu
     </listitem>
 
     <listitem>
-     <simpara>To protect against loss of very old data due to
-     <firstterm>transaction ID wraparound</firstterm> or
-     <firstterm>multixact ID wraparound</firstterm>.</simpara>
+     <simpara>To maintain the system's ability to allocated new
+     transaction IDs (and new multixact IDs) through freezing.</simpara>
     </listitem>
 
     <listitem>
      <simpara>To update the visibility map, which speeds
      up <link linkend="indexes-index-only-scans">index-only
-     scans</link>.</simpara>
+     scans</link>, and helps the next <command>VACUUM</command>
+     operation avoid needlessly scanning pages that are already
+     frozen</simpara>
+    </listitem>
+
+    <listitem>
+     <simpara>To truncate obsolescent transaction status information,
+     when possible</simpara>
     </listitem>
 
     <listitem>
@@ -432,10 +438,10 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu
    </tip>
   </sect2>
 
-  <sect2 id="vacuum-for-wraparound">
-   <title>Preventing Transaction ID Wraparound Failures</title>
+  <sect2 id="freezing-xid-space">
+   <title>Freezing to manage the transaction ID space</title>
 
-   <indexterm zone="vacuum-for-wraparound">
+   <indexterm zone="freezing-xid-space">
     <primary>transaction ID</primary>
     <secondary>wraparound</secondary>
    </indexterm>
@@ -461,274 +467,364 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu
    </para>
 
    <para>
-    <xref linkend="guc-vacuum-freeze-min-age"/>
-    controls how old an XID value has to be before rows bearing that XID will be
-    frozen.  Increasing this setting may avoid unnecessary work if the
-    rows that would otherwise be frozen will soon be modified again,
-    but decreasing this setting increases
-    the number of transactions that can elapse before the table must be
-    vacuumed again.
+    <command>VACUUM</command> marks pages as
+    <emphasis>frozen</emphasis>, indicating that all eligible rows on
+    the page were inserted by a transaction that committed
+    sufficiently far in the past that the effects of the inserting
+    transaction are certain to be visible to all current and future
+    transactions.  Frozen rows are treated as if the inserting
+    transaction (the heap tuple's <structfield>xmin</structfield> XID)
+    committed <quote>infinitely far in the past</quote>, making the
+    effects of the transaction visible to all current and future MVCC
+    snapshots, forever.  Once frozen, pages are <quote>self
+     contained</quote> in the sense that all of its rows can be read
+    without ever having to consult externally stored transaction status
+    metadata (for example, transaction commit status information from
+    <filename>pg_xact</filename> is not required).
    </para>
 
    <para>
-    <command>VACUUM</command> uses the <link linkend="storage-vm">visibility map</link>
-    to determine which pages of a table must be scanned.  Normally, it
-    will skip pages that don't have any dead row versions even if those pages
-    might still have row versions with old XID values.  Therefore, normal
-    <command>VACUUM</command>s won't always freeze every old row version in the table.
-    When that happens, <command>VACUUM</command> will eventually need to perform an
-    <firstterm>aggressive vacuum</firstterm>, which will freeze all eligible unfrozen
-    XID and MXID values, including those from all-visible but not all-frozen pages.
-    In practice most tables require periodic aggressive vacuuming.
-    <xref linkend="guc-vacuum-freeze-table-age"/>
-    controls when <command>VACUUM</command> does that: all-visible but not all-frozen
-    pages are scanned if the number of transactions that have passed since the
-    last such scan is greater than <varname>vacuum_freeze_table_age</varname> minus
-    <varname>vacuum_freeze_min_age</varname>. Setting
-    <varname>vacuum_freeze_table_age</varname> to 0 forces <command>VACUUM</command> to
-    always use its aggressive strategy.
+    Without freezing, the system will eventually <link
+     linkend="xid-stop-limit">refuse to allocate new transaction
+     IDs</link>, making <acronym>DML</acronym> statements throw errors
+    until such time as <command>VACUUM</command> can restore the
+    system's ability to allocate new transaction IDs.  The system is
+    unable to recognize distances of more than about 2.1 billion
+    transactions between any two unfrozen XIDs.  The only safe option
+    that remains is to disallow allocating new XIDs until
+    <command>VACUUM</command> has performed the steps required to make
+    sure that the <quote>distance</quote> invariant is never violated.
    </para>
 
    <para>
-    The maximum time that a table can go unvacuumed is two billion
-    transactions minus the <varname>vacuum_freeze_min_age</varname> value at
-    the time of the last aggressive vacuum. If it were to go
-    unvacuumed for longer than
-    that, data loss could result.  To ensure that this does not happen,
-    autovacuum is invoked on any table that might contain unfrozen rows with
-    XIDs older than the age specified by the configuration parameter <xref
-    linkend="guc-autovacuum-freeze-max-age"/>.  (This will happen even if
-    autovacuum is disabled.)
+    <xref linkend="guc-vacuum-freeze-min-age"/> can be used to control
+    when freezing takes place.  When <command>VACUUM</command> scans a
+    heap page containing even one XID that already attained an age
+    exceeding this value, the page is frozen.  All tuples from the page
+    are frozen, making the page suitable for long-term storage (their
+    is no longer any possible need to interpret the contents of the
+    page using external transaction status information).  Increasing
+    this setting may avoid unnecessary work if the pages that would
+    otherwise be frozen will soon be modified again, but decreasing
+    this setting makes it more likely that some future
+    <command>VACUUM</command> operation will need to perform an
+    excessive amount of <quote>catch-up freezing</quote>, all in one go.
+   </para>
+
+   <indexterm>
+    <primary>MultiXactId</primary>
+   </indexterm>
+
+   <indexterm>
+    <primary>freezing</primary>
+    <secondary>of multixact IDs</secondary>
+   </indexterm>
+
+   <para>
+    <firstterm>Multixact IDs</firstterm> are used to support row
+    locking by multiple transactions.  Since there is only limited
+    space in a tuple header to store lock information, that
+    information is encoded as a <quote>multiple transaction
+     ID</quote>, or multixact ID for short, whenever there is more
+    than one transaction concurrently locking a row.  Information
+    about which transaction IDs are included in any particular
+    multixact ID is stored separately, and only the multixact ID
+    appears in the <structfield>xmax</structfield> field in the tuple
+    header.  Like transaction IDs, multixact IDs are implemented as a
+    32-bit counter and corresponding storage.
+    <quote>Freezing</quote> of multixact IDs is also required, though
+    this actually means setting <structfield>xmax</structfield> to
+    <literal>InvalidTransactionId</literal>.
+    <!-- The following parenthetical statement isn't strictly true,
+    but it is true in spirit.  VACUUM practically always processes
+    xmax fields this way in practice. -->
+    (Actually, <emphasis>any</emphasis> <structfield>xmax</structfield>
+    field is processed in the same way when its page is frozen,
+    including <structfield>xmax</structfield> fields that contain
+    simple XIDs.)
    </para>
 
    <para>
-    This implies that if a table is not otherwise vacuumed,
-    autovacuum will be invoked on it approximately once every
-    <varname>autovacuum_freeze_max_age</varname> minus
-    <varname>vacuum_freeze_min_age</varname> transactions.
-    For tables that are regularly vacuumed for space reclamation purposes,
-    this is of little importance.  However, for static tables
-    (including tables that receive inserts, but no updates or deletes),
-    there is no need to vacuum for space reclamation, so it can
-    be useful to try to maximize the interval between forced autovacuums
-    on very large static tables.  Obviously one can do this either by
-    increasing <varname>autovacuum_freeze_max_age</varname> or decreasing
-    <varname>vacuum_freeze_min_age</varname>.
+    <xref linkend="guc-vacuum-multixact-freeze-min-age"/> can also
+    influence which pages <command>VACUUM</command> freezes.  Like
+    <varname>vacuum_freeze_min_age</varname>, the setting triggers
+    page-level freezing.  However,
+    <varname>vacuum_multixact_freeze_min_age</varname> uses
+    MultiXactIds (not XIDs) to measure <quote>age</quote>.
+   </para>
+
+   <note>
+    <para>
+     In <productname>PostgreSQL</productname> versions before 16,
+     freezing was triggered at the level of individual
+     <structfield>xmin</structfield> and
+     <structfield>xmax</structfield> fields.  The decision to freeze
+     (or not freeze) is now made at the level of whole heap pages, at
+     the point that they are scanned by <command>VACUUM</command>.
+    </para>
+   </note>
+
+   <para>
+    In general, the major cost of freezing is the additional
+    <acronym>WAL</acronym> volume.  That's why
+    <command>VACUUM</command> doesn't just freeze every eligible tuple
+    at the earliest opportunity: freezing will go to waste in cases
+    where a recently frozen tuple soon goes on to be deleted anyway.
+    Managing the added <acronym>WAL</acronym> volume from freezing
+    over time is an important consideration for
+    <command>VACUUM</command>.
    </para>
 
    <para>
-    The effective maximum for <varname>vacuum_freeze_table_age</varname> is 0.95 *
-    <varname>autovacuum_freeze_max_age</varname>; a setting higher than that will be
-    capped to the maximum. A value higher than
-    <varname>autovacuum_freeze_max_age</varname> wouldn't make sense because an
-    anti-wraparound autovacuum would be triggered at that point anyway, and
-    the 0.95 multiplier leaves some breathing room to run a manual
-    <command>VACUUM</command> before that happens.  As a rule of thumb,
-    <command>vacuum_freeze_table_age</command> should be set to a value somewhat
-    below <varname>autovacuum_freeze_max_age</varname>, leaving enough gap so that
-    a regularly scheduled <command>VACUUM</command> or an autovacuum triggered by
-    normal delete and update activity is run in that window.  Setting it too
-    close could lead to anti-wraparound autovacuums, even though the table
-    was recently vacuumed to reclaim space, whereas lower values lead to more
-    frequent aggressive vacuuming.
+    <command>VACUUM</command> also triggers freezing of pages in cases
+    where it already proved necessary to write out an
+    <acronym>FPI</acronym> to the <acronym>WAL</acronym> as torn page
+    protection (as part of removing dead tuples).  The extra
+    <acronym>WAL</acronym> volume from proactive freezing is
+    insignificant compared to the cost of the <acronym>FPI</acronym>.
+    It is very likely (though not quite certain) that the overall
+    volume of <acronym>WAL</acronym> will be lower in the long term
+    with tables that have most freezing triggered by the
+    <acronym>FPI</acronym> mechanism, since (at least on average)
+    future <command>VACUUM</command>s shouldn't have to write a second
+    <acronym>FPI</acronym> out much later on, when freezing becomes
+    strictly necessary.  The <acronym>FPI</acronym> freezing mechanism
+    is just an alternative trigger criteria for freezing all eligible
+    tuples on the page.  In general, <command>VACUUM</command> freezes
+    pages without regard to the condition that triggered freezing.
+    The physical modifications to the page (how tuples are processed)
+    is decoupled from the mechanism that decides if those
+    modifications should happen at all.
    </para>
 
    <para>
-    The sole disadvantage of increasing <varname>autovacuum_freeze_max_age</varname>
-    (and <varname>vacuum_freeze_table_age</varname> along with it) is that
-    the <filename>pg_xact</filename> and <filename>pg_commit_ts</filename>
-    subdirectories of the database cluster will take more space, because it
-    must store the commit status and (if <varname>track_commit_timestamp</varname> is
-    enabled) timestamp of all transactions back to
-    the <varname>autovacuum_freeze_max_age</varname> horizon.  The commit status uses
-    two bits per transaction, so if
-    <varname>autovacuum_freeze_max_age</varname> is set to its maximum allowed value
-    of two billion, <filename>pg_xact</filename> can be expected to grow to about half
-    a gigabyte and <filename>pg_commit_ts</filename> to about 20GB.  If this
-    is trivial compared to your total database size,
-    setting <varname>autovacuum_freeze_max_age</varname> to its maximum allowed value
-    is recommended.  Otherwise, set it depending on what you are willing to
-    allow for <filename>pg_xact</filename> and <filename>pg_commit_ts</filename> storage.
-    (The default, 200 million transactions, translates to about 50MB
-    of <filename>pg_xact</filename> storage and about 2GB of <filename>pg_commit_ts</filename>
-    storage.)
+    <command>VACUUM</command> may not be able to freeze every tuple's
+    <structfield>xmin</structfield> in relatively rare cases.  The
+    criteria that determines eligibility for freezing is exactly the
+    same as the one that determines if a deleted tuple should be
+    considered <literal>removable</literal> or merely <literal>dead
+     but not yet removable</literal> (namely, the XID-based
+    <literal>removable cutoff</literal>).
    </para>
 
-   <para>
-    One disadvantage of decreasing <varname>vacuum_freeze_min_age</varname> is that
-    it might cause <command>VACUUM</command> to do useless work: freezing a row
-    version is a waste of time if the row is modified
-    soon thereafter (causing it to acquire a new XID).  So the setting should
-    be large enough that rows are not frozen until they are unlikely to change
-    any more.
-   </para>
+   <sect3 id="aggressive-strategy">
+    <title><command>VACUUM</command>'s aggressive strategy</title>
+    <para>
+     To track the age of the oldest unfrozen XIDs in a database,
+     <command>VACUUM</command> stores XID statistics in the system
+     tables <structname>pg_class</structname> and
+     <structname>pg_database</structname>.  In particular, the
+     <structfield>relfrozenxid</structfield> column of a table's
+     <structname>pg_class</structname> row contains the oldest
+     remaining unfrozen XID at the end of the most recent
+     <command>VACUUM</command> that successfully advanced
+     <structfield>relfrozenxid</structfield>.  Similarly, the
+     <structfield>datfrozenxid</structfield> column of a database's
+     <structname>pg_database</structname> row is a lower bound on the
+     unfrozen XIDs appearing in that database &mdash; it is just the
+     minimum of the per-table <structfield>relfrozenxid</structfield>
+     values within the database.
+    </para>
 
-   <para>
-    To track the age of the oldest unfrozen XIDs in a database,
-    <command>VACUUM</command> stores XID
-    statistics in the system tables <structname>pg_class</structname> and
-    <structname>pg_database</structname>.  In particular,
-    the <structfield>relfrozenxid</structfield> column of a table's
-    <structname>pg_class</structname> row contains the oldest remaining unfrozen
-    XID at the end of the most recent <command>VACUUM</command> that successfully
-    advanced <structfield>relfrozenxid</structfield> (typically the most recent
-    aggressive VACUUM).  Similarly, the
-    <structfield>datfrozenxid</structfield> column of a database's
-    <structname>pg_database</structname> row is a lower bound on the unfrozen XIDs
-    appearing in that database &mdash; it is just the minimum of the
-    per-table <structfield>relfrozenxid</structfield> values within the database.
-    A convenient way to
-    examine this information is to execute queries such as:
+    <para>
+     <command>VACUUM</command> uses the <link
+      linkend="storage-vm">visibility map</link> to determine which
+     pages of a table must be scanned.  Normally, it will skip pages
+     that don't have any dead row versions even if those pages might
+     still have row versions with old XID values.  Therefore, normal
+     <command>VACUUM</command>s usually won't freeze
+     <emphasis>every</emphasis> page with an old row version in the
+     table.  Most individual tables will eventually need an
+     <firstterm>aggressive vacuum</firstterm>, which will reliably
+     freeze all pages with XID and MXID values older than
+     <varname>vacuum_freeze_min_age</varname>, including those from
+     all-visible but not all-frozen pages.  <xref
+      linkend="guc-vacuum-freeze-table-age"/> controls when
+     <command>VACUUM</command> must use its aggressive strategy.
+     Since the setting is applied against
+     <literal>age(relfrozenxid)</literal>, settings like
+     <varname>vacuum_freeze_min_age</varname> may influence the exact
+     cadence of aggressive vacuuming.  Setting
+     <varname>vacuum_freeze_table_age</varname> to 0 forces
+     <command>VACUUM</command> to always use its aggressive strategy.
+    </para>
+
+    <note>
+     <para>
+      In practice most tables require periodic aggressive vacuuming.
+      However, some individual non-aggressive
+      <command>VACUUM</command> operations may be able to advance
+      <structfield>relfrozenxid</structfield> and/or
+      <structfield>relminmxid</structfield>.  This is most common in
+      small, frequently modified tables, where
+      <command>VACUUM</command> happens to scan all pages (or at least
+      all pages not marked all-frozen) in the course of removing dead
+      tuples.
+     </para>
+    </note>
+
+    <para>
+     The maximum time that a table can go unvacuumed is about two
+     billion transactions. If it were to go unvacuumed for longer than
+     that, the system will <link linkend="xid-stop-limit">refuse to
+      allocate new transaction IDs</link>, temporarily rendering the
+     database read-only.  To ensure that this never happens,
+     autovacuum is invoked on any table that might contain unfrozen
+     rows with XIDs older than the age specified by the configuration
+     parameter <xref linkend="guc-autovacuum-freeze-max-age"/>.  (This
+     will happen even if autovacuum is disabled.)
+    </para>
+
+    <para>
+     <!-- This isn't strictly true, since anti-wraparound autovacuuming
+     merely implies aggressive mode -->
+     In practice all anti-wraparound autovacuums will use
+     <command>VACUUM</command>'s aggressive strategy.  This is assured
+     because the effective value of
+     <varname>vacuum_freeze_table_age</varname> is
+     <quote>clamped</quote> to a value no greater than 95% of the
+     current value of <varname>autovacuum_freeze_max_age</varname>.
+     As a rule of thumb, <command>vacuum_freeze_table_age</command>
+     should be set to a value somewhat below
+     <varname>autovacuum_freeze_max_age</varname>, leaving enough gap
+     so that a regularly scheduled <command>VACUUM</command> or an
+     autovacuum triggered by inserts, updates and deletes is run in
+     that window.  Anti-wraparound autovacuums can be avoided
+     altogether in tables that reliably receive
+     <emphasis>some</emphasis> <command>VACUUM</command>s that use the
+     aggressive strategy.
+    </para>
+
+    <note>
+     <para>
+      There is no fundamental difference between a
+      <command>VACUUM</command> run during anti-wraparound
+      autovacuuming and a <command>VACUUM</command> that happens to
+      use the aggressive strategy (whether run by autovacuum or
+      manually issued).
+     </para>
+    </note>
+
+    <para>
+     A convenient way to examine information about
+     <structfield>relfrozenxid</structfield> and
+     <structfield>relminmxid</structfield> is to execute queries such as:
 
 <programlisting>
 SELECT c.oid::regclass as table_name,
-       greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as age
+greatest(age(c.relfrozenxid),
+         age(t.relfrozenxid)) as xid_age,
+mxid_age(c.relminmxid)
 FROM pg_class c
 LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
 WHERE c.relkind IN ('r', 'm');
 
-SELECT datname, age(datfrozenxid) FROM pg_database;
+SELECT datname,
+age(datfrozenxid) as xid_age,
+mxid_age(datminmxid)
+FROM pg_database;
 </programlisting>
 
-    The <literal>age</literal> column measures the number of transactions from the
-    cutoff XID to the current transaction's XID.
-   </para>
-
-   <tip>
-    <para>
-     When the <command>VACUUM</command> command's <literal>VERBOSE</literal>
-     parameter is specified, <command>VACUUM</command> prints various
-     statistics about the table.  This includes information about how
-     <structfield>relfrozenxid</structfield> and
-     <structfield>relminmxid</structfield> advanced, and the number of
-     newly frozen pages.  The same details appear in the server log when
-     autovacuum logging (controlled by <xref
-      linkend="guc-log-autovacuum-min-duration"/>) reports on a
-     <command>VACUUM</command> operation executed by autovacuum.
+     The <literal>age</literal> column measures the number of
+     transactions from the cutoff XID to the next unallocated
+     transactions XID.  The <literal>mxid_age</literal> column
+     measures the number of MultiXactIds from the cutoff MultiXactId
+     to the next unallocated MultiXactId.
     </para>
-   </tip>
 
-   <para>
-    <command>VACUUM</command> normally only scans pages that have been modified
-    since the last vacuum, but <structfield>relfrozenxid</structfield> can only be
-    advanced when every page of the table
-    that might contain unfrozen XIDs is scanned.  This happens when
-    <structfield>relfrozenxid</structfield> is more than
-    <varname>vacuum_freeze_table_age</varname> transactions old, when
-    <command>VACUUM</command>'s <literal>FREEZE</literal> option is used, or when all
-    pages that are not already all-frozen happen to
-    require vacuuming to remove dead row versions. When <command>VACUUM</command>
-    scans every page in the table that is not already all-frozen, it should
-    set <literal>age(relfrozenxid)</literal> to a value just a little more than the
-    <varname>vacuum_freeze_min_age</varname> setting
-    that was used (more by the number of transactions started since the
-    <command>VACUUM</command> started).  <command>VACUUM</command>
-    will set <structfield>relfrozenxid</structfield> to the oldest XID
-    that remains in the table, so it's possible that the final value
-    will be much more recent than strictly required.
-    If no <structfield>relfrozenxid</structfield>-advancing
-    <command>VACUUM</command> is issued on the table until
-    <varname>autovacuum_freeze_max_age</varname> is reached, an autovacuum will soon
-    be forced for the table.
-   </para>
+    <tip>
+     <para>
+      When the <command>VACUUM</command> command's
+      <literal>VERBOSE</literal> parameter is specified,
+      <command>VACUUM</command> prints various statistics about the
+      table.  This includes information about how
+      <structfield>relfrozenxid</structfield> and
+      <structfield>relminmxid</structfield> advanced, and the number
+      of newly frozen pages.  The same details appear in the server
+      log when autovacuum logging (controlled by <xref
+       linkend="guc-log-autovacuum-min-duration"/>) reports on a
+      <command>VACUUM</command> operation executed by autovacuum.
+     </para>
+    </tip>
+   </sect3>
 
-   <para>
-    If for some reason autovacuum fails to clear old XIDs from a table, the
-    system will begin to emit warning messages like this when the database's
-    oldest XIDs reach forty million transactions from the wraparound point:
+   <sect3 id="xid-stop-limit">
+    <title><literal>xidStopLimit</literal> mode</title>
+    <para>
+     If for some reason autovacuum utterly fails to advance any
+     table's <structfield>relfrozenxid</structfield> or
+     <structfield>relminmxid</structfield>, the system will begin to
+     emit warning messages like this when the database's oldest XIDs
+     reach forty million transactions from the wraparound point:
 
 <programlisting>
 WARNING:  database "mydb" must be vacuumed within 39985967 transactions
 HINT:  To avoid a database shutdown, execute a database-wide VACUUM in that database.
 </programlisting>
 
-    (A manual <command>VACUUM</command> should fix the problem, as suggested by the
-    hint; but note that the <command>VACUUM</command> must be performed by a
-    superuser, else it will fail to process system catalogs and thus not
-    be able to advance the database's <structfield>datfrozenxid</structfield>.)
-    If these warnings are
-    ignored, the system will shut down and refuse to start any new
-    transactions once there are fewer than three million transactions left
-    until wraparound:
+     (A manual <command>VACUUM</command> should fix the problem, as suggested by the
+     hint; but note that the <command>VACUUM</command> must be performed by a
+     superuser, else it will fail to process system catalogs and thus not
+     be able to advance the database's <structfield>datfrozenxid</structfield>.)
+     If these warnings are ignored, the system will eventually refuse
+     to start any new transactions.  This happens at the point that
+     there are fewer than three million transactions left:
 
 <programlisting>
 ERROR:  database is not accepting commands to avoid wraparound data loss in database "mydb"
 HINT:  Stop the postmaster and vacuum that database in single-user mode.
 </programlisting>
 
-    The three-million-transaction safety margin exists to let the
-    administrator recover without data loss, by manually executing the
-    required <command>VACUUM</command> commands.  However, since the system will not
-    execute commands once it has gone into the safety shutdown mode,
-    the only way to do this is to stop the server and start the server in single-user
-    mode to execute <command>VACUUM</command>.  The shutdown mode is not enforced
-    in single-user mode.  See the <xref linkend="app-postgres"/> reference
-    page for details about using single-user mode.
-   </para>
-
-   <sect3 id="vacuum-for-multixact-wraparound">
-    <title>Multixacts and Wraparound</title>
-
-    <indexterm>
-     <primary>MultiXactId</primary>
-    </indexterm>
-
-    <indexterm>
-     <primary>wraparound</primary>
-     <secondary>of multixact IDs</secondary>
-    </indexterm>
-
-    <para>
-     <firstterm>Multixact IDs</firstterm> are used to support row locking by
-     multiple transactions.  Since there is only limited space in a tuple
-     header to store lock information, that information is encoded as
-     a <quote>multiple transaction ID</quote>, or multixact ID for short,
-     whenever there is more than one transaction concurrently locking a
-     row.  Information about which transaction IDs are included in any
-     particular multixact ID is stored separately in
-     the <filename>pg_multixact</filename> subdirectory, and only the multixact ID
-     appears in the <structfield>xmax</structfield> field in the tuple header.
-     Like transaction IDs, multixact IDs are implemented as a
-     32-bit counter and corresponding storage, all of which requires
-     careful aging management, storage cleanup, and wraparound handling.
-     There is a separate storage area which holds the list of members in
-     each multixact, which also uses a 32-bit counter and which must also
-     be managed.
+     The three-million-transaction safety margin exists to let the
+     administrator recover without data loss, by manually executing the
+     required <command>VACUUM</command> commands.  However, since the system will not
+     execute commands once it has gone into the safety shutdown mode,
+     the only way to do this is to stop the server and start the server in single-user
+     mode to execute <command>VACUUM</command>.  The shutdown mode is not enforced
+     in single-user mode.  See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
     </para>
 
     <para>
-     Whenever <command>VACUUM</command> scans any part of a table, it will replace
-     any multixact ID it encounters which is older than
-     <xref linkend="guc-vacuum-multixact-freeze-min-age"/>
-     by a different value, which can be the zero value, a single
-     transaction ID, or a newer multixact ID.  For each table,
-     <structname>pg_class</structname>.<structfield>relminmxid</structfield> stores the oldest
-     possible multixact ID still appearing in any tuple of that table.
-     If this value is older than
-     <xref linkend="guc-vacuum-multixact-freeze-table-age"/>, an aggressive
-     vacuum is forced.  As discussed in the previous section, an aggressive
-     vacuum means that only those pages which are known to be all-frozen will
-     be skipped.  <function>mxid_age()</function> can be used on
-     <structname>pg_class</structname>.<structfield>relminmxid</structfield> to find its age.
+     Anything that influences when and how
+     <structfield>relfrozenxid</structfield> and
+     <structfield>relminmxid</structfield> advance will also directly
+     affect the high watermark storage overhead from storing a great
+     deal of historical transaction status information.  The
+     additional <link linkend="vacuum-truncate-pg-xact">space
+      overhead</link> is usually of fairly minimal concern.  It is
+     noted as an additional downside of allowing the system to get
+     close to <literal>xidStopLimit</literal> for the sake of
+     completeness.
     </para>
 
-    <para>
-     Aggressive <command>VACUUM</command>s, regardless of what causes
-     them, are <emphasis>guaranteed</emphasis> to be able to advance
-     the table's <structfield>relminmxid</structfield>.
-     Eventually, as all tables in all databases are scanned and their
-     oldest multixact values are advanced, on-disk storage for older
-     multixacts can be removed.
-    </para>
+    <note>
+     <title>Historical Note</title>
+     <para>
+      The term <quote>wraparound</quote> is inaccurate.  Also, there
+      is no <quote>data loss</quote> here &mdash; the message is
+      simply wrong.
+     </para>
+     <para>
+      XXX: We really need to fix the situation with single user mode
+      to put things on a good footing.
+     </para>
+    </note>
 
     <para>
-     As a safety device, an aggressive vacuum scan will
-     occur for any table whose multixact-age is greater than <xref
-     linkend="guc-autovacuum-multixact-freeze-max-age"/>.  Also, if the
-     storage occupied by multixacts members exceeds 2GB, 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
-     scans will occur even if autovacuum is nominally disabled.
+     In emergencies, <command>VACUUM</command> will take extraordinary
+     measures to avoid <literal>xidStopLimit</literal> mode.  A
+     failsafe mechanism is triggered when thresholds controlled by
+     <xref linkend="guc-vacuum-failsafe-age"/> and <xref
+      linkend="guc-vacuum-multixact-failsafe-age"/> are reached.  The
+     failsafe prioritizes advancing
+     <structfield>relfrozenxid</structfield> and/or
+     <structfield>relminmxid</structfield> as quickly as possible.
+     Once the failsafe triggers, <command>VACUUM</command> bypasses
+     all remaining non-essential maintenance tasks, and stops applying
+     any cost-based delay that was in effect.  Any <glossterm
+     linkend="glossary-buffer-access-strategy">Buffer Access
+     Strategy</glossterm> in use will also be disabled.
     </para>
    </sect3>
   </sect2>
@@ -766,6 +862,58 @@ HINT:  Stop the postmaster and vacuum that database in single-user mode.
    </para>
   </sect2>
 
+  <sect2 id="vacuum-truncate-pg-xact">
+   <title>Truncating transaction status information</title>
+   <para>
+    As noted in <xref linkend="xid-stop-limit"/>, anything that
+    influences when and how <structfield>relfrozenxid</structfield> and
+    <structfield>relminmxid</structfield> advance will also directly
+    affect the high watermark storage overhead needed to store
+    historical transaction status information.  For example,
+    increasing <varname>autovacuum_freeze_max_age</varname> (and
+    <varname>vacuum_freeze_table_age</varname> along with it) will
+    make the <filename>pg_xact</filename> and
+    <filename>pg_commit_ts</filename> subdirectories of the database
+    cluster take more space, because they store the commit status and
+    (if <varname>track_commit_timestamp</varname> is enabled)
+    timestamp of all transactions back to the
+    <varname>datfrozenxid</varname> horizon (the earliest
+    <varname>datfrozenxid</varname> in the entire cluster).
+   </para>
+   <para>
+    The commit status uses two bits per transaction.  The default
+    <varname>autovacuum_freeze_max_age</varname> setting of 200
+    million transactions translates to about 50MB of
+    <filename>pg_xact</filename> storage.  When
+    <varname>track_commit_timestamp</varname> is enabled, about 2GB of
+    <filename>pg_commit_ts</filename> storage will also be required.
+   </para>
+   <para>
+    MultiXactId status information is implemented as two separate
+    <acronym>SLRU</acronym> storage areas:
+    <filename>pg_multixact/members</filename>, and
+    <filename>pg_multixact/offsets</filename>.  There is no simple
+    formula to determine the storage overhead per MultiXactId, since
+    MultiXactIds have a variable number of member XIDs.
+   </para>
+   <para>
+    Truncating of transaction status information is only possible at
+    the end of <command>VACUUM</command>s that advance
+    <structfield>relfrozenxid</structfield> (in the case of
+    <filename>pg_xact</filename> and
+    <filename>pg_commit_ts</filename>) or
+    <structfield>relminmxid</structfield> (in the case of
+    (<filename>pg_multixact/members</filename> and
+    <filename>pg_multixact/offsets</filename>) of whatever table
+    happened to have the oldest value in the cluster when the
+    <command>VACUUM</command> began.  This typically happens very
+    infrequently, often during <link
+     linkend="aggressive-strategy">aggressive strategy</link>
+    <command>VACUUM</command>s of one of the database's largest
+    tables.
+   </para>
+  </sect2>
+
   <sect2 id="vacuum-for-statistics">
    <title>Updating Planner Statistics</title>
 
@@ -881,7 +1029,7 @@ HINT:  Stop the postmaster and vacuum that database in single-user mode.
    </tip>
 
   </sect2>
-</sect1>
+ </sect1>
 
 
  <sect1 id="routine-reindex">
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 10ef699fa..8aa332fcf 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1515,7 +1515,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      and/or <command>ANALYZE</command> operations on this table following the rules
      discussed in <xref linkend="autovacuum"/>.
      If false, this table will not be autovacuumed, except to prevent
-     transaction ID wraparound. See <xref linkend="vacuum-for-wraparound"/> for
+     transaction ID wraparound. See <xref linkend="freezing-xid-space"/> for
      more about wraparound prevention.
      Note that the autovacuum daemon does not run at all (except to prevent
      transaction ID wraparound) if the <xref linkend="guc-autovacuum"/>
diff --git a/doc/src/sgml/ref/prepare_transaction.sgml b/doc/src/sgml/ref/prepare_transaction.sgml
index f4f6118ac..ede50d6f7 100644
--- a/doc/src/sgml/ref/prepare_transaction.sgml
+++ b/doc/src/sgml/ref/prepare_transaction.sgml
@@ -128,7 +128,7 @@ PREPARE TRANSACTION <replaceable class="parameter">transaction_id</replaceable>
     This will interfere with the ability of <command>VACUUM</command> to reclaim
     storage, and in extreme cases could cause the database to shut down
     to prevent transaction ID wraparound (see <xref
-    linkend="vacuum-for-wraparound"/>).  Keep in mind also that the transaction
+    linkend="freezing-xid-space"/>).  Keep in mind also that the transaction
     continues to hold whatever locks it held.  The intended usage of the
     feature is that a prepared transaction will normally be committed or
     rolled back as soon as an external transaction manager has verified that
diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index 57bc4c23e..0c28604a6 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -123,7 +123,9 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
     <term><literal>FREEZE</literal></term>
     <listitem>
      <para>
-      Selects aggressive <quote>freezing</quote> of tuples.
+      Makes <quote>freezing</quote> <emphasis>maximally</emphasis>
+      aggressive, and forces <command>VACUUM</command> to use its
+      <link linkend="aggressive-strategy">aggressive strategy</link>.
       Specifying <literal>FREEZE</literal> is equivalent to performing
       <command>VACUUM</command> with the
       <xref linkend="guc-vacuum-freeze-min-age"/> and
@@ -219,7 +221,7 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
       there are many dead tuples in the table.  This may be useful
       when it is necessary to make <command>VACUUM</command> run as
       quickly as possible to avoid imminent transaction ID wraparound
-      (see <xref linkend="vacuum-for-wraparound"/>).  However, the
+      (see <xref linkend="freezing-xid-space"/>).  However, the
       wraparound failsafe mechanism controlled by <xref
        linkend="guc-vacuum-failsafe-age"/>  will generally trigger
       automatically to avoid transaction ID wraparound failure, and
diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index da2393783..b61d523c2 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -233,7 +233,7 @@ PostgreSQL documentation
         ID age of at least <replaceable class="parameter">mxid_age</replaceable>.
         This setting is useful for prioritizing tables to process to prevent
         multixact ID wraparound (see
-        <xref linkend="vacuum-for-multixact-wraparound"/>).
+        <xref linkend="freezing-xid-space"/>).
        </para>
        <para>
         For the purposes of this option, the multixact ID age of a relation is
@@ -254,7 +254,7 @@ PostgreSQL documentation
         transaction ID age of at least
         <replaceable class="parameter">xid_age</replaceable>.  This setting
         is useful for prioritizing tables to process to prevent transaction
-        ID wraparound (see <xref linkend="vacuum-for-wraparound"/>).
+        ID wraparound (see <xref linkend="freezing-xid-space"/>).
        </para>
        <para>
         For the purposes of this option, the transaction ID age of a relation
diff --git a/doc/src/sgml/xact.sgml b/doc/src/sgml/xact.sgml
index b467660ee..e18ad8fd3 100644
--- a/doc/src/sgml/xact.sgml
+++ b/doc/src/sgml/xact.sgml
@@ -49,7 +49,7 @@
 
   <para>
    The internal transaction ID type <type>xid</type> is 32 bits wide
-   and <link linkend="vacuum-for-wraparound">wraps around</link> every
+   and <link linkend="freezing-xid-space">wraps around</link> every
    4 billion transactions. A 32-bit epoch is incremented during each
    wraparound. There is also a 64-bit type <type>xid8</type> which
    includes this epoch and therefore does not wrap around during the
@@ -100,7 +100,7 @@
    rows and can be inspected using the <xref linkend="pgrowlocks"/>
    extension.  Row-level read locks might also require the assignment
    of multixact IDs (<literal>mxid</literal>;  see <xref
-   linkend="vacuum-for-multixact-wraparound"/>).
+   linkend="freezing-xid-space"/>).
   </para>
  </sect1>
 
-- 
2.40.0

