Skip to content

Commit c81ed19

Browse files
shinyaaaCommitfest Bot
authored andcommitted
Report oldest xmin source when autovacuum cannot remove tuples
Autovacuum logging now tells which backend or mechanism kept OldestXmin from advancing when dead tuples remain, improving diagnosis of bloating and wraparound hazards. ProcArray now records an OldestXminSource for each computed horizon, and VACUUM retrieves it through the new GetOldestNonRemovableTransactionIdWithReason() helper. The log output names active transactions (with pid), hot standby feedback, prepared transactions, replication slots, or otherwise labels the cause as "other". Author: Shinya Kato <shinya11.kato@gmail.com> Reviewed-by: Discussion: https://postgr.es/m/
1 parent b237f54 commit c81ed19

File tree

7 files changed

+388
-30
lines changed

7 files changed

+388
-30
lines changed

src/backend/access/heap/vacuumlazy.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,43 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
10471047
appendStringInfo(&buf,
10481048
_("removable cutoff: %u, which was %d XIDs old when operation ended\n"),
10491049
vacrel->cutoffs.OldestXmin, diff);
1050+
/*
1051+
* If there are dead tuples not yet removable, report what
1052+
* determined the OldestXmin horizon to aid diagnostics.
1053+
*/
1054+
if (vacrel->recently_dead_tuples > 0)
1055+
{
1056+
int pid = vacrel->cutoffs.oldest_xmin_info.backend_pid;
1057+
bool include_pid = pid > 0;
1058+
1059+
switch (vacrel->cutoffs.oldest_xmin_info.source)
1060+
{
1061+
case OLDESTXMIN_SOURCE_ACTIVE_TRANSACTION:
1062+
msgfmt = include_pid ?
1063+
_("oldest xmin source: active transaction (pid=%d)\n") :
1064+
_("oldest xmin source: active transaction\n");
1065+
break;
1066+
case OLDESTXMIN_SOURCE_HOT_STANDBY_FEEDBACK:
1067+
msgfmt = include_pid ?
1068+
_("oldest xmin source: hot standby feedback (pid=%d)\n") :
1069+
_("oldest xmin source: hot standby feedback\n");
1070+
break;
1071+
case OLDESTXMIN_SOURCE_PREPARED_TRANSACTION:
1072+
msgfmt = _("oldest xmin source: prepared transaction\n");
1073+
break;
1074+
case OLDESTXMIN_SOURCE_REPLICATION_SLOT:
1075+
msgfmt = _("oldest xmin source: replication slot\n");
1076+
break;
1077+
default:
1078+
msgfmt = _("oldest xmin source: other\n");
1079+
break;
1080+
}
1081+
1082+
if (include_pid)
1083+
appendStringInfo(&buf, msgfmt, pid);
1084+
else
1085+
appendStringInfoString(&buf, msgfmt);
1086+
}
10501087
if (frozenxid_updated)
10511088
{
10521089
diff = (int32) (vacrel->NewRelfrozenXid -

src/backend/commands/vacuum.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1133,7 +1133,9 @@ vacuum_get_cutoffs(Relation rel, const VacuumParams params,
11331133
* that only one vacuum process can be working on a particular table at
11341134
* any time, and that each vacuum is always an independent transaction.
11351135
*/
1136-
cutoffs->OldestXmin = GetOldestNonRemovableTransactionId(rel);
1136+
cutoffs->OldestXmin =
1137+
GetOldestNonRemovableTransactionIdWithReason(rel,
1138+
&cutoffs->oldest_xmin_info);
11371139

11381140
Assert(TransactionIdIsNormal(cutoffs->OldestXmin));
11391141

src/backend/storage/ipc/procarray.c

Lines changed: 136 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ typedef struct ComputeXidHorizonsResult
241241
* session's temporary tables.
242242
*/
243243
TransactionId temp_oldest_nonremovable;
244+
245+
/* Identify what forced each of the horizons above. */
246+
OldestXminInfo shared_oldest_nonremovable_info;
247+
OldestXminInfo catalog_oldest_nonremovable_info;
248+
OldestXminInfo data_oldest_nonremovable_info;
249+
OldestXminInfo temp_oldest_nonremovable_info;
244250
} ComputeXidHorizonsResult;
245251

246252
/*
@@ -1622,6 +1628,46 @@ TransactionIdIsInProgress(TransactionId xid)
16221628
return false;
16231629
}
16241630

1631+
/*
1632+
* Store horizon provenance in *info if caller requested it.
1633+
*
1634+
* Callers pass NULL when they are not interested in tracking the source.
1635+
*/
1636+
static inline void
1637+
OldestXminInfoSet(OldestXminInfo *info, OldestXminSource source,
1638+
int backend_pid)
1639+
{
1640+
if (info == NULL)
1641+
return;
1642+
1643+
info->source = source;
1644+
info->backend_pid = backend_pid;
1645+
}
1646+
1647+
/*
1648+
* Update a tracked OldestXmin horizon with a candidate xid and source.
1649+
*
1650+
* If the candidate is older than *target, adopt it and remember why.
1651+
*/
1652+
static inline void
1653+
UpdateOldestXmin(TransactionId *target, OldestXminInfo *info,
1654+
TransactionId candidate, OldestXminSource source,
1655+
int backend_pid)
1656+
{
1657+
TransactionId old;
1658+
TransactionId new_horizon;
1659+
1660+
if (!TransactionIdIsValid(candidate))
1661+
return;
1662+
1663+
old = *target;
1664+
new_horizon = TransactionIdOlder(old, candidate);
1665+
if (TransactionIdEquals(new_horizon, old))
1666+
return;
1667+
1668+
*target = new_horizon;
1669+
OldestXminInfoSet(info, source, backend_pid);
1670+
}
16251671

16261672
/*
16271673
* Determine XID horizons.
@@ -1689,6 +1735,8 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h)
16891735

16901736
/* inferred after ProcArrayLock is released */
16911737
h->catalog_oldest_nonremovable = InvalidTransactionId;
1738+
OldestXminInfoSet(&h->catalog_oldest_nonremovable_info,
1739+
OLDESTXMIN_SOURCE_OTHER, 0);
16921740

16931741
LWLockAcquire(ProcArrayLock, LW_SHARED);
16941742

@@ -1710,6 +1758,10 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h)
17101758
h->oldest_considered_running = initial;
17111759
h->shared_oldest_nonremovable = initial;
17121760
h->data_oldest_nonremovable = initial;
1761+
OldestXminInfoSet(&h->shared_oldest_nonremovable_info,
1762+
OLDESTXMIN_SOURCE_OTHER, 0);
1763+
OldestXminInfoSet(&h->data_oldest_nonremovable_info,
1764+
OLDESTXMIN_SOURCE_OTHER, 0);
17131765

17141766
/*
17151767
* Only modifications made by this backend affect the horizon for
@@ -1724,9 +1776,17 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h)
17241776
* latestCompletedXid.
17251777
*/
17261778
if (TransactionIdIsValid(MyProc->xid))
1779+
{
17271780
h->temp_oldest_nonremovable = MyProc->xid;
1781+
OldestXminInfoSet(&h->temp_oldest_nonremovable_info,
1782+
OLDESTXMIN_SOURCE_ACTIVE_TRANSACTION, MyProcPid);
1783+
}
17281784
else
1785+
{
17291786
h->temp_oldest_nonremovable = initial;
1787+
OldestXminInfoSet(&h->temp_oldest_nonremovable_info,
1788+
OLDESTXMIN_SOURCE_OTHER, 0);
1789+
}
17301790
}
17311791

17321792
/*
@@ -1744,6 +1804,8 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h)
17441804
int8 statusFlags = ProcGlobal->statusFlags[index];
17451805
TransactionId xid;
17461806
TransactionId xmin;
1807+
OldestXminSource candidate_source;
1808+
int candidate_pid;
17471809

17481810
/* Fetch xid just once - see GetNewTransactionId */
17491811
xid = UINT32_ACCESS_ONCE(other_xids[index]);
@@ -1768,8 +1830,8 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h)
17681830
* backends are protected even without this check, it can't hurt to
17691831
* include them here as well..
17701832
*/
1771-
h->oldest_considered_running =
1772-
TransactionIdOlder(h->oldest_considered_running, xmin);
1833+
UpdateOldestXmin(&h->oldest_considered_running, NULL, xmin,
1834+
OLDESTXMIN_SOURCE_OTHER, 0);
17731835

17741836
/*
17751837
* Skip over backends either vacuuming (which is ok with rows being
@@ -1780,8 +1842,17 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h)
17801842
continue;
17811843

17821844
/* shared tables need to take backends in all databases into account */
1783-
h->shared_oldest_nonremovable =
1784-
TransactionIdOlder(h->shared_oldest_nonremovable, xmin);
1845+
candidate_pid = proc->pid;
1846+
if (proc->pid == 0)
1847+
candidate_source = OLDESTXMIN_SOURCE_PREPARED_TRANSACTION;
1848+
else if (statusFlags & PROC_AFFECTS_ALL_HORIZONS)
1849+
candidate_source = OLDESTXMIN_SOURCE_HOT_STANDBY_FEEDBACK;
1850+
else
1851+
candidate_source = OLDESTXMIN_SOURCE_ACTIVE_TRANSACTION;
1852+
1853+
UpdateOldestXmin(&h->shared_oldest_nonremovable,
1854+
&h->shared_oldest_nonremovable_info,
1855+
xmin, candidate_source, candidate_pid);
17851856

17861857
/*
17871858
* Normally sessions in other databases are ignored for anything but
@@ -1807,8 +1878,9 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h)
18071878
(statusFlags & PROC_AFFECTS_ALL_HORIZONS) ||
18081879
in_recovery)
18091880
{
1810-
h->data_oldest_nonremovable =
1811-
TransactionIdOlder(h->data_oldest_nonremovable, xmin);
1881+
UpdateOldestXmin(&h->data_oldest_nonremovable,
1882+
&h->data_oldest_nonremovable_info,
1883+
xmin, candidate_source, candidate_pid);
18121884
}
18131885
}
18141886

@@ -1827,12 +1899,14 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h)
18271899

18281900
if (in_recovery)
18291901
{
1830-
h->oldest_considered_running =
1831-
TransactionIdOlder(h->oldest_considered_running, kaxmin);
1832-
h->shared_oldest_nonremovable =
1833-
TransactionIdOlder(h->shared_oldest_nonremovable, kaxmin);
1834-
h->data_oldest_nonremovable =
1835-
TransactionIdOlder(h->data_oldest_nonremovable, kaxmin);
1902+
UpdateOldestXmin(&h->oldest_considered_running, NULL,
1903+
kaxmin, OLDESTXMIN_SOURCE_OTHER, 0);
1904+
UpdateOldestXmin(&h->shared_oldest_nonremovable,
1905+
&h->shared_oldest_nonremovable_info,
1906+
kaxmin, OLDESTXMIN_SOURCE_OTHER, 0);
1907+
UpdateOldestXmin(&h->data_oldest_nonremovable,
1908+
&h->data_oldest_nonremovable_info,
1909+
kaxmin, OLDESTXMIN_SOURCE_OTHER, 0);
18361910
/* temp relations cannot be accessed in recovery */
18371911
}
18381912

@@ -1844,10 +1918,12 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h)
18441918
/*
18451919
* Check whether there are replication slots requiring an older xmin.
18461920
*/
1847-
h->shared_oldest_nonremovable =
1848-
TransactionIdOlder(h->shared_oldest_nonremovable, h->slot_xmin);
1849-
h->data_oldest_nonremovable =
1850-
TransactionIdOlder(h->data_oldest_nonremovable, h->slot_xmin);
1921+
UpdateOldestXmin(&h->shared_oldest_nonremovable,
1922+
&h->shared_oldest_nonremovable_info,
1923+
h->slot_xmin, OLDESTXMIN_SOURCE_REPLICATION_SLOT, 0);
1924+
UpdateOldestXmin(&h->data_oldest_nonremovable,
1925+
&h->data_oldest_nonremovable_info,
1926+
h->slot_xmin, OLDESTXMIN_SOURCE_REPLICATION_SLOT, 0);
18511927

18521928
/*
18531929
* The only difference between catalog / data horizons is that the slot's
@@ -1857,13 +1933,16 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h)
18571933
* that also can contain catalogs.
18581934
*/
18591935
h->shared_oldest_nonremovable_raw = h->shared_oldest_nonremovable;
1860-
h->shared_oldest_nonremovable =
1861-
TransactionIdOlder(h->shared_oldest_nonremovable,
1862-
h->slot_catalog_xmin);
1936+
UpdateOldestXmin(&h->shared_oldest_nonremovable,
1937+
&h->shared_oldest_nonremovable_info,
1938+
h->slot_catalog_xmin,
1939+
OLDESTXMIN_SOURCE_REPLICATION_SLOT, 0);
18631940
h->catalog_oldest_nonremovable = h->data_oldest_nonremovable;
1864-
h->catalog_oldest_nonremovable =
1865-
TransactionIdOlder(h->catalog_oldest_nonremovable,
1866-
h->slot_catalog_xmin);
1941+
h->catalog_oldest_nonremovable_info = h->data_oldest_nonremovable_info;
1942+
UpdateOldestXmin(&h->catalog_oldest_nonremovable,
1943+
&h->catalog_oldest_nonremovable_info,
1944+
h->slot_catalog_xmin,
1945+
OLDESTXMIN_SOURCE_REPLICATION_SLOT, 0);
18671946

18681947
/*
18691948
* It's possible that slots backed up the horizons further than
@@ -1951,25 +2030,53 @@ GlobalVisHorizonKindForRel(Relation rel)
19512030
*/
19522031
TransactionId
19532032
GetOldestNonRemovableTransactionId(Relation rel)
2033+
{
2034+
/* Delegate to the WithReason variant to avoid duplicated logic */
2035+
return GetOldestNonRemovableTransactionIdWithReason(rel, NULL);
2036+
}
2037+
2038+
/*
2039+
* Return horizon like GetOldestNonRemovableTransactionId(), and also classify
2040+
* the reason that determined that horizon at the time of computation.
2041+
*/
2042+
TransactionId
2043+
GetOldestNonRemovableTransactionIdWithReason(Relation rel,
2044+
OldestXminInfo *out_info)
19542045
{
19552046
ComputeXidHorizonsResult horizons;
2047+
TransactionId target = InvalidTransactionId;
2048+
GlobalVisHorizonKind kind;
2049+
const OldestXminInfo *source_info = NULL;
2050+
2051+
if (out_info != NULL)
2052+
OldestXminInfoSet(out_info, OLDESTXMIN_SOURCE_OTHER, 0);
19562053

19572054
ComputeXidHorizons(&horizons);
19582055

1959-
switch (GlobalVisHorizonKindForRel(rel))
2056+
kind = GlobalVisHorizonKindForRel(rel);
2057+
switch (kind)
19602058
{
19612059
case VISHORIZON_SHARED:
1962-
return horizons.shared_oldest_nonremovable;
2060+
target = horizons.shared_oldest_nonremovable;
2061+
source_info = &horizons.shared_oldest_nonremovable_info;
2062+
break;
19632063
case VISHORIZON_CATALOG:
1964-
return horizons.catalog_oldest_nonremovable;
2064+
target = horizons.catalog_oldest_nonremovable;
2065+
source_info = &horizons.catalog_oldest_nonremovable_info;
2066+
break;
19652067
case VISHORIZON_DATA:
1966-
return horizons.data_oldest_nonremovable;
2068+
target = horizons.data_oldest_nonremovable;
2069+
source_info = &horizons.data_oldest_nonremovable_info;
2070+
break;
19672071
case VISHORIZON_TEMP:
1968-
return horizons.temp_oldest_nonremovable;
2072+
target = horizons.temp_oldest_nonremovable;
2073+
source_info = &horizons.temp_oldest_nonremovable_info;
2074+
break;
19692075
}
19702076

1971-
/* just to prevent compiler warnings */
1972-
return InvalidTransactionId;
2077+
if (out_info != NULL && TransactionIdIsValid(target) && source_info != NULL)
2078+
*out_info = *source_info;
2079+
return target;
19732080
}
19742081

19752082
/*

src/include/commands/vacuum.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "parser/parse_node.h"
2525
#include "storage/buf.h"
2626
#include "storage/lock.h"
27+
#include "storage/procarray.h"
2728
#include "utils/relcache.h"
2829

2930
/*
@@ -288,6 +289,9 @@ struct VacuumCutoffs
288289
*/
289290
TransactionId FreezeLimit;
290291
MultiXactId MultiXactCutoff;
292+
293+
/* What decided OldestXmin at acquisition time */
294+
OldestXminInfo oldest_xmin_info;
291295
};
292296

293297
/*

src/include/storage/procarray.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@
2020
#include "utils/snapshot.h"
2121

2222

23+
/*
24+
* Identifies what determined a relation's OldestXmin horizon.
25+
* Used by autovacuum to report why dead tuples were not removable.
26+
*/
27+
typedef enum OldestXminSource
28+
{
29+
OLDESTXMIN_SOURCE_ACTIVE_TRANSACTION,
30+
OLDESTXMIN_SOURCE_HOT_STANDBY_FEEDBACK,
31+
OLDESTXMIN_SOURCE_PREPARED_TRANSACTION,
32+
OLDESTXMIN_SOURCE_REPLICATION_SLOT,
33+
OLDESTXMIN_SOURCE_OTHER
34+
} OldestXminSource;
35+
36+
typedef struct OldestXminInfo
37+
{
38+
OldestXminSource source;
39+
int backend_pid;
40+
} OldestXminInfo;
41+
2342
extern Size ProcArrayShmemSize(void);
2443
extern void ProcArrayShmemInit(void);
2544
extern void ProcArrayAdd(PGPROC *proc);
@@ -54,6 +73,8 @@ extern RunningTransactions GetRunningTransactionData(void);
5473

5574
extern bool TransactionIdIsInProgress(TransactionId xid);
5675
extern TransactionId GetOldestNonRemovableTransactionId(Relation rel);
76+
extern TransactionId GetOldestNonRemovableTransactionIdWithReason(Relation rel,
77+
OldestXminInfo *info);
5778
extern TransactionId GetOldestTransactionIdConsideredRunning(void);
5879
extern TransactionId GetOldestActiveTransactionId(bool inCommitOnly,
5980
bool allDbs);

src/test/modules/test_misc/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ tests += {
1818
't/007_catcache_inval.pl',
1919
't/008_replslot_single_user.pl',
2020
't/009_log_temp_files.pl',
21+
't/010_autovacuum_oldest_xmin_reason.pl',
2122
],
2223
},
2324
}

0 commit comments

Comments
 (0)