Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions src/backend/cluster/cluster_tt_slot.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

#include "access/transam.h"
#include "cluster/cluster_guc.h" /* cluster_undo_retention_horizon_enabled */
#include "cluster/cluster_mode.h" /* cluster_peer_mode_enabled */
#include "cluster/cluster_scn.h" /* SCN_MAX_VALID_NODE_ID */
#include "cluster/cluster_shmem.h" /* ClusterShmemRegion */
#include "cluster/cluster_tt_slot.h"
Expand Down Expand Up @@ -322,6 +323,12 @@ tt_slot_entry_recycle_locked(ClusterTTSlotAllocEntry *e, TransactionId new_owner
* the horizon
* (cluster_tt_slot_recyclable); ABORTED is always eligible (C7). When the
* retention GUC is off, the gate is bypassed (spec-3.11 immediate recycle, C6).
* In peer mode, allocator Pass 2 never recycles COMMITTED directly: the local
* ProcArray horizon does not cover remote snapshots. The cluster undo cleaner
* is the sole COMMITTED recycler there; it folds every required peer report
* into an epoch-paired cluster floor and fences each COMMITTED -> FREE mutation.
* Allocator Pass 1 may then consume that proven FREE slot. ABORTED remains
* immediately reusable because it is invisible to every snapshot.
*
* Returns INVALID_TT_SLOT_OFFSET when no slot can be handed out. In that
* case *out_retained_pressure (when non-NULL) distinguishes the two reasons:
Expand All @@ -342,6 +349,7 @@ cluster_tt_slot_alloc_ext(uint32 segment_id, TransactionId top_xid, bool *out_re
int free_idx = -1;
bool retained_pressure = false;
bool gate_enabled;
bool peer_mode;
SCN horizon = InvalidScn;
uint16 chosen;
uint64 retain_skip_seen = 0; /* spec-3.12 D5 */
Expand All @@ -359,6 +367,7 @@ cluster_tt_slot_alloc_ext(uint32 segment_id, TransactionId top_xid, bool *out_re
* GUC is off we skip the scan entirely and bypass the gate.
*/
gate_enabled = cluster_undo_retention_horizon_enabled;
peer_mode = cluster_peer_mode_enabled();
if (gate_enabled) {
horizon = cluster_undo_retention_horizon();
/* spec-3.12 D5 / C16: sample the horizon gauge at the decision point. */
Expand All @@ -382,9 +391,20 @@ cluster_tt_slot_alloc_ext(uint32 segment_id, TransactionId top_xid, bool *out_re
if (free_idx < 0 && !cluster_tt_slot_is_protected(segment_id, (uint16)i))
free_idx = i;
} else if (e->status == CTS_COMMITTED || e->status == CTS_ABORTED) {
bool recyclable = gate_enabled
? cluster_tt_slot_recyclable(e->status, e->commit_scn, horizon)
: true; /* GUC off: spec-3.11 immediate recycle (C6) */
/*
* A local ProcArray horizon cannot authorize destruction of evidence
* still needed by a peer snapshot. In peer mode COMMITTED reclamation
* is delegated to cluster_tt_slot_gc_current_pass(), whose production
* caller supplies the epoch-paired proven cluster floor and rechecks
* the epoch at the mutation. This also overrides the diagnostic GUC-off
* bypass: without a cluster proof the safe outcome is rollover/fail-closed.
*/
bool recyclable
= (e->status == CTS_COMMITTED && peer_mode)
? false
: (gate_enabled
? cluster_tt_slot_recyclable(e->status, e->commit_scn, horizon)
: true); /* single-node GUC off: immediate recycle (C6) */

/* D5: wrap-retired entries are never re-selected this generation;
* they read as pressure so the caller can roll over (C3b family). */
Expand All @@ -398,7 +418,7 @@ cluster_tt_slot_alloc_ext(uint32 segment_id, TransactionId top_xid, bool *out_re
if (reusable_idx < 0 && !cluster_tt_slot_is_protected(segment_id, (uint16)i))
reusable_idx = i;
} else {
/* COMMITTED and not older than horizon: retention keeps it alive. */
/* COMMITTED lacks a cluster recycle proof, or is not older than horizon. */
retained_pressure = true;
retain_skip_seen++; /* C16: per skip event, not de-duped */
}
Expand Down
47 changes: 42 additions & 5 deletions src/backend/cluster/cluster_undo_cleaner.c
Original file line number Diff line number Diff line change
Expand Up @@ -353,15 +353,28 @@ undo_cleaner_advance_liveness_tick(void)
* - storage-mode gate: no cluster storage mode -> no work (HC4);
* - cluster.undo_cleaner_enabled=off -> no work (diagnostic
* parity with 3.12 lazy-only mode).
*
* Returns true when the recycle stage could NOT run to completion
* against a proven cluster floor (fold stalled, member set unstable,
* or the F-D2 epoch fence aborted the pass). These causes normally
* clear at the horizon-report cadence (~one LMON tick: epoch views
* converge and every peer re-publishes), so the caller retries the
* pass at that cadence instead of sleeping a full recycle interval —
* the S3 undo-pool exhaustion amplifier (a ~1s transient froze
* cluster-wide COMMITTED recycling for 30-60s). Rule 8.A unchanged:
* a retry still recycles ONLY on a proven floor; a persistent cause
* keeps every retry stalled.
*/
static void
static bool
undo_cleaner_run_pass(void)
{
ClusterUndoCleanerPassStats stats;
bool floor_retry_needed = false;

if (!cluster_undo_cleaner_enabled)
return;
return false;
if (!cluster_storage_mode_enabled())
return;
return false;

/*
* D2 (step 3): horizon ONCE per pass, BEFORE any seg->lock (C17).
Expand Down Expand Up @@ -410,6 +423,7 @@ undo_cleaner_run_pass(void)
}

if (stalled) {
floor_retry_needed = true;
cluster_undo_horizon_note_stall();
if (stall_blame >= 0 && stall_blame != cluster_node_id)
cluster_undo_horizon_note_peer_stale();
Expand All @@ -434,6 +448,7 @@ undo_cleaner_run_pass(void)
cluster_undo_horizon_note_floor(floor.scn);

if (!cluster_tt_slot_gc_current_pass(horizon, floor.epoch, &stats)) {
floor_retry_needed = true;
cluster_undo_horizon_note_pass_abort();
goto pass_account; /* F-D2: epoch moved mid-scan; abort the pass */
}
Expand Down Expand Up @@ -522,8 +537,10 @@ undo_cleaner_run_pass(void)
*/
undo_cleaner_state->scan_resume_seg = seg;

if (fence_aborted)
if (fence_aborted) {
floor_retry_needed = true;
cluster_undo_horizon_note_pass_abort();
}
}
}

Expand Down Expand Up @@ -559,6 +576,8 @@ undo_cleaner_run_pass(void)
pinned_logged = false;
}
}

return floor_retry_needed;
}


Expand Down Expand Up @@ -614,6 +633,7 @@ UndoCleanerMain(void)
for (;;) {
int rc;
int timeout_ms;
bool floor_retry;

CHECK_FOR_INTERRUPTS();

Expand All @@ -629,13 +649,30 @@ UndoCleanerMain(void)

CLUSTER_INJECTION_POINT("undo-cleaner-main-loop-iter");

undo_cleaner_run_pass();
floor_retry = undo_cleaner_run_pass();

/*
* interval 0 = pressure-wakeup only (Q8): block without
* timeout; otherwise wake at the configured cadence.
*
* TT lane (P1#4): a pass that could not prove the cluster floor
* (stall / member churn / F-D2 abort) re-arms at the horizon-
* report cadence instead — that is when new reports and epoch
* convergence can actually arrive (one LMON tick). Sleeping the
* full recycle interval here turned ~1s transients into 30-60s
* cluster-wide COMMITTED-recycle freezes (S3 undo-pool
* exhaustion, 25,715 x 53R9E on node0). Fail-closed is
* untouched: the retried pass re-proves or re-stalls; this also
* bounds the interval=0 mode, whose stalled pass previously
* waited on a latch nobody was obliged to set.
*/
timeout_ms = cluster_undo_cleaner_interval_ms;
if (floor_retry) {
int retry_ms = Max(cluster_lmon_main_loop_interval, 200);

if (timeout_ms <= 0 || retry_ms < timeout_ms)
timeout_ms = retry_ms;
}
rc = WaitLatch(
MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH | (timeout_ms > 0 ? WL_TIMEOUT : 0),
timeout_ms > 0 ? timeout_ms : -1L, WAIT_EVENT_CLUSTER_BGPROC_UNDO_CLEANER_MAIN_LOOP);
Expand Down
3 changes: 3 additions & 0 deletions src/include/cluster/cluster_tt_slot.h
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ cluster_tt_slot_id_to_offset(uint32 tt_slot_id)
* 1) reuse a slot already owned by `top_xid` (idempotent)
* 2) take any FREE slot
* 3) recycle a COMMITTED / ABORTED slot, wrap++
* In peer mode, direct Pass-2 reuse is limited to ABORTED. COMMITTED
* evidence is first recycled to FREE by the epoch-fenced cluster-horizon
* cleaner, then consumed through Pass 1.
* Returns offset in [0, TT_SLOTS_PER_SEGMENT) on success, or
* INVALID_TT_SLOT_OFFSET when all slots are ACTIVE. Caller MUST be
* outside critical section (function takes LWLock).
Expand Down
Loading
Loading