vacuum_freeze_terminate_blockers_pid.patch
application/octet-stream
Filename: vacuum_freeze_terminate_blockers_pid.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: unified
| File | + | − |
|---|---|---|
| src/backend/commands/vacuum.c | 110 | 3 |
| src/backend/storage/ipc/procarray.c | 141 | 0 |
| src/backend/utils/misc/guc_parameters.dat | 7 | 0 |
| src/backend/utils/misc/postgresql.conf.sample | 1 | 0 |
| src/include/commands/vacuum.h | 1 | 0 |
| src/include/storage/procarray.h | 3 | 0 |
| src/test/isolation/expected/vacuum-freeze-terminate-blockers.out | 45 | 0 |
| src/test/isolation/isolation_schedule | 1 | 0 |
| src/test/isolation/specs/vacuum-freeze-terminate-blockers.spec | 85 | 0 |
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 99d0db82ed7..49844fe0b5a 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -50,6 +50,7 @@
#include "postmaster/interrupt.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "storage/pmsignal.h"
#include "storage/proc.h"
#include "storage/procarray.h"
@@ -58,6 +59,7 @@
#include "utils/guc.h"
#include "utils/guc_hooks.h"
#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"
@@ -80,6 +82,7 @@ int vacuum_multixact_freeze_table_age;
int vacuum_failsafe_age;
int vacuum_multixact_failsafe_age;
double vacuum_max_eager_freeze_failure_rate;
+bool vacuum_freeze_terminate_blockers_pid;
bool track_cost_delay_timing;
bool vacuum_truncate;
@@ -128,6 +131,10 @@ static void vac_truncate_clog(TransactionId frozenXID,
MultiXactId lastSaneMinMulti);
static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
BufferAccessStrategy bstrategy, bool isTopLevel);
+static void vacuum_maybe_terminate_freeze_pid(Relation rel,
+ struct VacuumCutoffs *cutoffs,
+ TransactionId freezeLimit,
+ TransactionId nextXID);
static double compute_parallel_delay(void);
static VacOptValue get_vacoptval_from_boolean(DefElem *def);
static bool vac_tid_reaped(ItemPointer itemptr, void *state);
@@ -1108,6 +1115,7 @@ vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
effective_multixact_freeze_max_age;
TransactionId nextXID,
safeOldestXmin,
+ unconstrainedFreezeLimit,
aggressiveXIDCutoff;
MultiXactId nextMXID,
safeOldestMxact,
@@ -1186,9 +1194,14 @@ vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
Assert(freeze_min_age >= 0);
/* Compute FreezeLimit, being careful to generate a normal XID */
- cutoffs->FreezeLimit = nextXID - freeze_min_age;
- if (!TransactionIdIsNormal(cutoffs->FreezeLimit))
- cutoffs->FreezeLimit = FirstNormalTransactionId;
+ unconstrainedFreezeLimit = nextXID - freeze_min_age;
+ if (!TransactionIdIsNormal(unconstrainedFreezeLimit))
+ unconstrainedFreezeLimit = FirstNormalTransactionId;
+
+ vacuum_maybe_terminate_freeze_pid(rel, cutoffs,
+ unconstrainedFreezeLimit, nextXID);
+
+ cutoffs->FreezeLimit = unconstrainedFreezeLimit;
/* FreezeLimit must always be <= OldestXmin */
if (TransactionIdPrecedes(cutoffs->OldestXmin, cutoffs->FreezeLimit))
cutoffs->FreezeLimit = cutoffs->OldestXmin;
@@ -1258,6 +1271,100 @@ vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
return false;
}
+/*
+ * Terminate active backends that are holding back VACUUM's ability to advance
+ * FreezeLimit, when explicitly enabled by the user.
+ */
+static void
+vacuum_maybe_terminate_freeze_pid(Relation rel,
+ struct VacuumCutoffs *cutoffs,
+ TransactionId freezeLimit,
+ TransactionId nextXID)
+{
+ VirtualTransactionId *vxids;
+ VirtualTransactionId *vxid;
+ TransactionId freezeTerminateLimit;
+ TransactionId freezeTerminateAgeXids;
+ double freezeTerminateAge;
+ int terminated = 0;
+ int i;
+ Oid dbOid;
+
+ if (!vacuum_freeze_terminate_blockers_pid)
+ return;
+
+ if (vacuum_failsafe_age <= 0)
+ return;
+
+ /*
+ * Determine the freeze termination age to use. Normally this scales
+ * vacuum_failsafe_age by autovacuum_freeze_score_weight. When the weight
+ * is zero, use vacuum_failsafe_age directly.
+ */
+ if (likely(autovacuum_freeze_score_weight > 0.0))
+ freezeTerminateAge =
+ (double) vacuum_failsafe_age / autovacuum_freeze_score_weight;
+ else
+ freezeTerminateAge = (double) vacuum_failsafe_age;
+
+ if (freezeTerminateAge >= (double) MaxTransactionId)
+ return;
+
+ freezeTerminateAgeXids = (TransactionId) floor(freezeTerminateAge);
+ freezeTerminateLimit = nextXID - freezeTerminateAgeXids;
+ if (!TransactionIdIsNormal(freezeTerminateLimit))
+ freezeTerminateLimit = FirstNormalTransactionId;
+
+ /* Only act once the table age has passed the termination age. */
+ if (!TransactionIdPrecedes(cutoffs->relfrozenxid, freezeTerminateLimit))
+ return;
+
+ /* If OldestXmin is not holding back FreezeLimit, nobody blocks freeze. */
+ if (!TransactionIdPrecedes(cutoffs->OldestXmin, freezeLimit))
+ return;
+
+ dbOid = rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId;
+ vxids = GetVirtualXIDsBlockingVacuumFreeze(freezeLimit, dbOid);
+
+ for (vxid = vxids; VirtualTransactionIdIsValid(*vxid); vxid++)
+ {
+ int pid = 0;
+
+ if (TerminateBackendWithVirtualXID(*vxid, &pid))
+ {
+ char *nspname;
+
+ vxids[terminated++] = *vxid;
+ nspname = get_namespace_name(RelationGetNamespace(rel));
+
+ ereport(LOG,
+ (errmsg("terminating backend with PID %d because it blocks vacuum freeze of table \"%s.%s\"",
+ pid, nspname, RelationGetRelationName(rel)),
+ errdetail("The table age is greater than the freeze termination age derived from vacuum_failsafe_age and autovacuum_freeze_score_weight."),
+ errhint("Disable configuration parameter \"vacuum_freeze_terminate_blockers_pid\" to prevent VACUUM from terminating blocking sessions.")));
+
+ pfree(nspname);
+ }
+ }
+
+ /*
+ * Terminate blockers before waiting, matching the recovery-conflict
+ * pattern of identifying blockers by VXID and then waiting for each
+ * signaled VXID to disappear. Once they are gone, recompute OldestXmin so
+ * this VACUUM can use the less conservative freeze cutoff immediately.
+ */
+ if (terminated > 0)
+ {
+ for (i = 0; i < terminated; i++)
+ VirtualXactLock(vxids[i], true);
+
+ cutoffs->OldestXmin = GetOldestNonRemovableTransactionId(rel);
+ Assert(TransactionIdIsNormal(cutoffs->OldestXmin));
+ }
+
+ pfree(vxids);
+}
+
/*
* vacuum_xid_failsafe_check() -- Used by VACUUM's wraparound failsafe
* mechanism to determine if its table's relfrozenxid and relminmxid are now
diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index 9299bcebbda..a71ce8c6e45 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -3350,6 +3350,92 @@ GetCurrentVirtualXIDs(TransactionId limitXmin, bool excludeXmin0,
return vxids;
}
+/*
+ * GetVirtualXIDsBlockingVacuumFreeze -- returns active VXIDs holding back
+ * VACUUM's freeze horizon.
+ *
+ * The caller supplies the freeze cutoff that it wanted to use before it was
+ * constrained by OldestXmin. We return regular client backends whose xmin/xid
+ * horizon is older than that cutoff and whose database scope matches the
+ * relation being vacuumed.
+ *
+ * Replication slots, hot standby feedback, and prepared transactions can also
+ * hold back horizons, but they are not ordinary long-running client
+ * transactions, so this routine deliberately ignores them.
+ *
+ * The result is palloc'd and terminated with an invalid VXID.
+ */
+VirtualTransactionId *
+GetVirtualXIDsBlockingVacuumFreeze(TransactionId limitXmin, Oid dbOid)
+{
+ VirtualTransactionId *vxids;
+ ProcArrayStruct *arrayP = procArray;
+ TransactionId *other_xids = ProcGlobal->xids;
+ int count = 0;
+ int index;
+
+ Assert(TransactionIdIsValid(limitXmin));
+
+ vxids = palloc_array(VirtualTransactionId, arrayP->maxProcs + 1);
+
+ LWLockAcquire(ProcArrayLock, LW_SHARED);
+
+ for (index = 0; index < arrayP->numProcs; index++)
+ {
+ int pgprocno = arrayP->pgprocnos[index];
+ PGPROC *proc = &allProcs[pgprocno];
+ uint8 statusFlags = ProcGlobal->statusFlags[index];
+ TransactionId xid;
+ TransactionId xmin;
+
+ if (proc == MyProc)
+ continue;
+
+ /* Prepared transactions can block horizons, but have no session PID. */
+ if (proc->pid == 0)
+ continue;
+
+ /* Only ordinary client backends are actionable here. */
+ if (proc->backendType != B_BACKEND)
+ continue;
+
+ /* Hot standby feedback affects all horizons, but is not a client xact. */
+ if (statusFlags & PROC_AFFECTS_ALL_HORIZONS)
+ continue;
+
+ /*
+ * Match ComputeXidHorizons(): lazy VACUUMs and logical decoding
+ * backends do not hold back VACUUM's non-removable horizon here.
+ */
+ if (statusFlags & (PROC_IN_VACUUM | PROC_IN_LOGICAL_DECODING))
+ continue;
+
+ if (OidIsValid(dbOid) && proc->databaseId != dbOid)
+ continue;
+
+ xid = UINT32_ACCESS_ONCE(other_xids[index]);
+ xmin = UINT32_ACCESS_ONCE(proc->xmin);
+ xmin = TransactionIdOlder(xmin, xid);
+
+ if (TransactionIdIsValid(xmin) &&
+ TransactionIdPrecedes(xmin, limitXmin))
+ {
+ VirtualTransactionId vxid;
+
+ GET_VXID_FROM_PGPROC(vxid, *proc);
+ if (VirtualTransactionIdIsValid(vxid))
+ vxids[count++] = vxid;
+ }
+ }
+
+ LWLockRelease(ProcArrayLock);
+
+ vxids[count].procNumber = INVALID_PROC_NUMBER;
+ vxids[count].localTransactionId = InvalidLocalTransactionId;
+
+ return vxids;
+}
+
/*
* GetConflictingVirtualXIDs -- returns an array of currently active VXIDs.
*
@@ -3454,6 +3540,61 @@ GetConflictingVirtualXIDs(TransactionId limitXmin, Oid dbOid)
return vxids;
}
+/*
+ * TerminateBackendWithVirtualXID -- terminate the backend still owning a VXID.
+ *
+ * This follows the recovery-conflict convention of resolving the target by
+ * VXID under ProcArrayLock before signaling it. The target PID is returned
+ * to the caller for reporting.
+ */
+bool
+TerminateBackendWithVirtualXID(VirtualTransactionId vxid, int *pid)
+{
+ ProcArrayStruct *arrayP = procArray;
+ pid_t target_pid = 0;
+ int index;
+
+ Assert(VirtualTransactionIdIsValid(vxid));
+
+ LWLockAcquire(ProcArrayLock, LW_SHARED);
+
+ for (index = 0; index < arrayP->numProcs; index++)
+ {
+ int pgprocno = arrayP->pgprocnos[index];
+ PGPROC *proc = &allProcs[pgprocno];
+ uint8 statusFlags = ProcGlobal->statusFlags[index];
+ VirtualTransactionId procvxid;
+
+ GET_VXID_FROM_PGPROC(procvxid, *proc);
+
+ if (procvxid.procNumber == vxid.procNumber &&
+ procvxid.localTransactionId == vxid.localTransactionId)
+ {
+ if (proc->backendType == B_BACKEND &&
+ !(statusFlags & PROC_AFFECTS_ALL_HORIZONS))
+ target_pid = proc->pid;
+ break;
+ }
+ }
+
+ LWLockRelease(ProcArrayLock);
+
+ if (pid)
+ *pid = target_pid;
+
+ if (target_pid == 0)
+ return false;
+
+#ifdef HAVE_SETSID
+ if (kill(-target_pid, SIGTERM))
+#else
+ if (kill(target_pid, SIGTERM))
+#endif
+ return false;
+
+ return true;
+}
+
/*
* SignalRecoveryConflict -- signal that a process is blocking recovery
*
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 83af594d4af..796d21b639b 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3385,6 +3385,13 @@
max => '2000000000',
},
+{ name => 'vacuum_freeze_terminate_blockers_pid', type => 'bool', context => 'PGC_SUSET', group => 'VACUUM_FREEZING',
+ short_desc => 'Terminates client sessions that block VACUUM from advancing its freeze cutoff.',
+ long_desc => 'When enabled, VACUUM terminates regular client sessions whose transaction horizon blocks freezing once the table age is greater than vacuum_failsafe_age divided by autovacuum_freeze_score_weight, or greater than vacuum_failsafe_age when autovacuum_freeze_score_weight is zero.',
+ variable => 'vacuum_freeze_terminate_blockers_pid',
+ boot_val => 'false',
+},
+
{ name => 'vacuum_max_eager_freeze_failure_rate', type => 'real', context => 'PGC_USERSET', group => 'VACUUM_FREEZING',
short_desc => 'Fraction of pages in a relation vacuum can scan and fail to freeze before disabling eager scanning.',
long_desc => 'A value of 0.0 disables eager scanning and a value of 1.0 will eagerly scan up to 100 percent of the all-visible pages in the relation. If vacuum successfully freezes these pages, the cap is lower than 100 percent, because the goal is to amortize page freezing across multiple vacuums.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ac38cddaaf9..1ca1d6ce6c5 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -774,6 +774,7 @@
#vacuum_freeze_table_age = 150000000
#vacuum_freeze_min_age = 50000000
#vacuum_failsafe_age = 1600000000
+#vacuum_freeze_terminate_blockers_pid = off
#vacuum_multixact_freeze_table_age = 150000000
#vacuum_multixact_freeze_min_age = 5000000
#vacuum_multixact_failsafe_age = 1600000000
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..a48f3aea7f8 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -329,6 +329,7 @@ extern PGDLLIMPORT int vacuum_multixact_freeze_min_age;
extern PGDLLIMPORT int vacuum_multixact_freeze_table_age;
extern PGDLLIMPORT int vacuum_failsafe_age;
extern PGDLLIMPORT int vacuum_multixact_failsafe_age;
+extern PGDLLIMPORT bool vacuum_freeze_terminate_blockers_pid;
extern PGDLLIMPORT bool track_cost_delay_timing;
extern PGDLLIMPORT bool vacuum_truncate;
diff --git a/src/include/storage/procarray.h b/src/include/storage/procarray.h
index ec89c448220..e84218b5d61 100644
--- a/src/include/storage/procarray.h
+++ b/src/include/storage/procarray.h
@@ -73,8 +73,11 @@ extern bool IsBackendPid(int pid);
extern VirtualTransactionId *GetCurrentVirtualXIDs(TransactionId limitXmin,
bool excludeXmin0, bool allDbs, int excludeVacuum,
int *nvxids);
+extern VirtualTransactionId *GetVirtualXIDsBlockingVacuumFreeze(TransactionId limitXmin,
+ Oid dbOid);
extern VirtualTransactionId *GetConflictingVirtualXIDs(TransactionId limitXmin, Oid dbOid);
+extern bool TerminateBackendWithVirtualXID(VirtualTransactionId vxid, int *pid);
extern bool SignalRecoveryConflict(PGPROC *proc, pid_t pid, RecoveryConflictReason reason);
extern bool SignalRecoveryConflictWithVirtualXID(VirtualTransactionId vxid, RecoveryConflictReason reason);
extern void SignalRecoveryConflictWithDatabase(Oid databaseid, RecoveryConflictReason reason);
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 1578ba191c8..3447d467e77 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -98,6 +98,7 @@ test: create-trigger
test: sequence-ddl
test: async-notify
test: vacuum-no-cleanup-lock
+test: vacuum-freeze-terminate-blockers
test: timeouts
test: vacuum-concurrent-drop
test: vacuum-conflict
diff --git a/src/test/isolation/specs/vacuum-freeze-terminate-blockers.spec b/src/test/isolation/specs/vacuum-freeze-terminate-blockers.spec
new file mode 100644
index 00000000000..5e7d5814dd1
--- /dev/null
+++ b/src/test/isolation/specs/vacuum-freeze-terminate-blockers.spec
@@ -0,0 +1,85 @@
+# Test vacuum_freeze_terminate_blockers_pid.
+#
+# A transaction with an old XID can hold back VACUUM's freeze cutoff. Once
+# table age passes the freeze termination age derived from vacuum_failsafe_age
+# and autovacuum_freeze_score_weight, enabling the GUC should make VACUUM
+# terminate the backend that owns that blocker XID.
+
+setup
+{
+ CREATE TABLE vacuum_freeze_blocker_tab (id int)
+ WITH (autovacuum_enabled = off);
+ INSERT INTO vacuum_freeze_blocker_tab VALUES (1);
+ CREATE TABLE vacuum_freeze_blocker_pid (pid int);
+ CREATE TABLE vacuum_freeze_xid_burner (id int);
+}
+
+# Unsafe xid_wraparound tests can consume billions of XIDs. This default
+# isolation test keeps runtime practical by using a small vacuum_failsafe_age
+# and burning enough XIDs to cross the same age threshold formula.
+# Each setup block runs separately.
+setup { INSERT INTO vacuum_freeze_xid_burner DEFAULT VALUES; }
+setup { INSERT INTO vacuum_freeze_xid_burner DEFAULT VALUES; }
+setup { INSERT INTO vacuum_freeze_xid_burner DEFAULT VALUES; }
+setup { INSERT INTO vacuum_freeze_xid_burner DEFAULT VALUES; }
+setup { INSERT INTO vacuum_freeze_xid_burner DEFAULT VALUES; }
+setup { INSERT INTO vacuum_freeze_xid_burner DEFAULT VALUES; }
+
+teardown
+{
+ DROP TABLE IF EXISTS vacuum_freeze_blocker_tab;
+ DROP TABLE IF EXISTS vacuum_freeze_blocker_pid;
+ DROP TABLE IF EXISTS vacuum_freeze_xid_burner;
+}
+
+session blocker
+step blocker_record_pid
+{
+ INSERT INTO vacuum_freeze_blocker_pid SELECT pg_backend_pid();
+}
+step blocker_begin
+{
+ BEGIN;
+}
+step blocker_assign_xid
+{
+ SELECT txid_current() IS NOT NULL AS xid_assigned;
+}
+
+session vacuumer
+setup
+{
+ SET client_min_messages = error;
+ SET vacuum_failsafe_age = 6;
+ SET vacuum_freeze_min_age = 0;
+ SET vacuum_freeze_terminate_blockers_pid = on;
+}
+step vacuum_run
+{
+ VACUUM vacuum_freeze_blocker_tab;
+}
+step vacuum_check_age_past_threshold
+{
+ SELECT age(relfrozenxid)::float8 >
+ CASE WHEN current_setting('autovacuum_freeze_score_weight')::float8 > 0.0
+ THEN current_setting('vacuum_failsafe_age')::float8 /
+ current_setting('autovacuum_freeze_score_weight')::float8
+ ELSE current_setting('vacuum_failsafe_age')::float8
+ END AS past_termination_age
+ FROM pg_class
+ WHERE oid = 'vacuum_freeze_blocker_tab'::regclass;
+}
+step vacuum_check_blocker_gone
+{
+ SELECT count(*) = 0 AS blocker_gone
+ FROM pg_stat_activity
+ WHERE pid = (SELECT pid FROM vacuum_freeze_blocker_pid);
+}
+
+permutation
+ blocker_record_pid
+ blocker_begin
+ blocker_assign_xid
+ vacuum_check_age_past_threshold
+ vacuum_run
+ vacuum_check_blocker_gone
diff --git a/src/test/isolation/expected/vacuum-freeze-terminate-blockers.out b/src/test/isolation/expected/vacuum-freeze-terminate-blockers.out
new file mode 100644
index 00000000000..2f8d3851639
--- /dev/null
+++ b/src/test/isolation/expected/vacuum-freeze-terminate-blockers.out
@@ -0,0 +1,45 @@
+Parsed test spec with 2 sessions
+
+starting permutation: blocker_record_pid blocker_begin blocker_assign_xid vacuum_check_age_past_threshold vacuum_run vacuum_check_blocker_gone
+step blocker_record_pid:
+ INSERT INTO vacuum_freeze_blocker_pid SELECT pg_backend_pid();
+
+step blocker_begin:
+ BEGIN;
+
+step blocker_assign_xid:
+ SELECT txid_current() IS NOT NULL AS xid_assigned;
+
+xid_assigned
+------------
+t
+(1 row)
+
+step vacuum_check_age_past_threshold:
+ SELECT age(relfrozenxid)::float8 >
+ CASE WHEN current_setting('autovacuum_freeze_score_weight')::float8 > 0.0
+ THEN current_setting('vacuum_failsafe_age')::float8 /
+ current_setting('autovacuum_freeze_score_weight')::float8
+ ELSE current_setting('vacuum_failsafe_age')::float8
+ END AS past_termination_age
+ FROM pg_class
+ WHERE oid = 'vacuum_freeze_blocker_tab'::regclass;
+
+past_termination_age
+--------------------
+t
+(1 row)
+
+step vacuum_run:
+ VACUUM vacuum_freeze_blocker_tab;
+
+step vacuum_check_blocker_gone:
+ SELECT count(*) = 0 AS blocker_gone
+ FROM pg_stat_activity
+ WHERE pid = (SELECT pid FROM vacuum_freeze_blocker_pid);
+
+blocker_gone
+------------
+t
+(1 row)
+