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
20 changes: 7 additions & 13 deletions src/backend/access/transam/xact.c
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
#include "cluster/cluster_itl_touch.h" /* PGRAC: spec-3.4a D6 pre-commit/abort */
#include "cluster/cluster_tt_2pc.h" /* PGRAC: spec-3.15 PREPARE record */
#include "cluster/cluster_subtrans.h" /* PGRAC: spec-3.5 D7 subxact lifecycle hook */
#include "cluster/cluster_undo_record_api.h" /* PGRAC: spec-3.7 D16 PREPARE guard */
#include "cluster/cluster_undo_record_api.h" /* PGRAC: undo xact lifecycle */
#include "cluster/cluster_touched_peers.h" /* PGRAC: spec-5.14 D1 per-tx reset */
#include "cluster/cluster_visibility_resolve.h" /* PGRAC: spec-6.14 D8 depth reset */
#include "cluster/cluster_relmap_lock.h" /* PGRAC: spec-6.14 D5 abort release */
Expand Down Expand Up @@ -2958,14 +2958,9 @@ CommitTransaction(void)
s->state = TRANS_DEFAULT;

#ifdef USE_PGRAC_CLUSTER
/* PGRAC (spec-4.12a D6): release this backend's active-write undo boundary
* at commit so a committed transaction's first_undo_scn stops pinning the
* record-segment drain boundary. The full cluster_undo_record_xact_reset()
* runs only on the PREPARE (PrepareTransaction) and ABORT (CleanupTransaction)
* paths -- a persistent (pooled) connection that commits and stays open would
* otherwise keep its first transaction registered for the life of the backend
* (re-opening the spec-4.13 undo-pool leak). 8.A-safe: only enables
* ACTIVE->COMMITTED; the retention horizon still gates the actual reclaim. */
/* PGRAC: release the active-write boundary and clear O(1) transaction-local
* undo bookkeeping. Preserve backend caches; PREPARE/ABORT use the full
* teardown. Spec: spec-4.12a-undo-record-segment-reclaim.md */
cluster_undo_record_xact_commit_release();
#endif

Expand Down Expand Up @@ -3296,8 +3291,8 @@ PrepareTransaction(void)
s->state = TRANS_DEFAULT;

#ifdef USE_PGRAC_CLUSTER
/* PGRAC (spec-3.7 D16): reset per-backend undo touched flag at
* end of xact (commit path). Backend reuse → next xact starts fresh. */
/* PGRAC: PostPrepare transfers undo ownership and fully tears down the
* backend-local PREPARE state. */
cluster_undo_record_xact_reset();
/* PGRAC (spec-5.14 D1): clear the touched_peers bitmap on commit. */
cluster_touched_peers_reset();
Expand Down Expand Up @@ -3547,8 +3542,7 @@ CleanupTransaction(void)
AtEOXact_Snapshot(false, true); /* and release the transaction's snapshots */

#ifdef USE_PGRAC_CLUSTER
/* PGRAC (spec-3.7 D16): reset per-backend undo touched flag at
* end of xact (abort path). Backend reuse → next xact starts fresh. */
/* PGRAC: fully tear down backend-local undo state on abort. */
cluster_undo_record_xact_reset();
/* PGRAC (spec-5.14 D1): clear the touched_peers bitmap on abort/cleanup. */
cluster_touched_peers_reset();
Expand Down
13 changes: 6 additions & 7 deletions src/backend/cluster/cluster_tt_2pc.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
#include "cluster/cluster_tt_local.h" /* binding export/reset */
#include "cluster/cluster_tt_slot.h" /* protected-slot map (D6/V-4) */
#include "cluster/cluster_tt_status.h" /* SUBCOMMITTED overlay rebuild */
#include "cluster/cluster_undo_record_api.h" /* xact_reset (PostPrepare) */
#include "cluster/cluster_undo_record_api.h" /* full undo teardown (PostPrepare) */
#include "cluster/cluster_tt_durable.h" /* durable 0x30 commit (prefinish) */
#include "cluster/cluster_tt_status_hint.h" /* cross-node hint emit (prefinish) */
#include "cluster/storage/cluster_undo_alloc.h" /* CLUSTER_UNDO_SEGS_PER_INSTANCE */
Expand Down Expand Up @@ -111,7 +111,7 @@ AtPrepare_ClusterTT(void)
/*
* spec-4.8 D7-A: capture each binding's latest undo-chain head into the v2
* heads[] section. The head is backend-local (cluster_undo_record.c) and is
* reset at xact end, so it must be read here at PREPARE while still live;
* cleared by PostPrepare, so it must be read here at PREPARE while still live;
* the durable TT slot header does not persist it (deferred). A binding with
* no undo this xact gets InvalidUba (-> D7 physical rollback no-op for it).
*/
Expand Down Expand Up @@ -145,10 +145,9 @@ AtPrepare_ClusterTT(void)
* PostPrepare_ClusterTT -- ownership transfer after EndPrepare succeeded.
*
* The 2PC record is now the single authority (C-P4): drop the backend-
* local TT bindings, the SUBCOMMITTED link list, and the ITL touch
* list (V-2: droppable -- overlay/durable TT are authoritative and the
* 3.4c lazy cleanout re-stamps page ITLs on later reads). Nothing
* here touches shmem or durable state.
* local TT bindings, the SUBCOMMITTED link list, the ITL touch list, and the
* backend-local undo lifecycle state. Undo teardown unregisters the active
* writer from shmem; no durable state is changed here.
*/
void
PostPrepare_ClusterTT(void)
Expand All @@ -159,7 +158,7 @@ PostPrepare_ClusterTT(void)
cluster_tt_local_reset_binding();
cluster_subtrans_reset_local_links();
cluster_itl_touch_reset_at_end_xact();
cluster_undo_record_xact_reset(); /* touched-undo list + D16 flag */
cluster_undo_record_xact_reset(); /* full backend-local undo teardown */
cluster_vis_bump_twopc_postprepare_transfers();
}

Expand Down
110 changes: 58 additions & 52 deletions src/backend/cluster/cluster_undo_record.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@
* - cluster_undo_record_alloc() — write one undo record + durable flush
* - cluster_undo_get_record() — read one undo record by UBA
* - cluster_undo_record_shmem_register/size/init
* - cluster_undo_record_xact_reset() — end-of-xact hook
* - cluster_undo_record_is_touched() — for D16 PREPARE guard
* - cluster_undo_record_xact_commit_release() — normal COMMIT cleanup
* - cluster_undo_record_xact_reset() — PREPARE/ABORT full teardown
* - cluster_undo_record_is_touched() — legacy write marker accessor
*
* Concurrency model(MVP):
* - single per-instance LWLock(`cursor_lock`)protects active segment
* cursor advance(active_segment_id / current_block / free_offset /
* slot_count).多 backend 写同 instance → serialize on this lock.
* - per-backend `cluster_undo_touched` static bool tracks D16 state;
* reset on xact end.
* - per-backend transaction-local state is reset by the normal-COMMIT
* hook or the PREPARE/ABORT full teardown.
*
* Block boundary contract(per spec-3.7 §3.2):record 不跨 block。
* block 不够时 advance 到下一 block;segment 不够时 ereport 53R9D
Expand Down Expand Up @@ -59,7 +60,7 @@
#include <sys/stat.h>

#include "miscadmin.h"
#include "access/twophase.h" /* spec-4.12a D1: prepared-xact guard (硬门 6) */
#include "access/twophase.h" /* spec-4.12a D1: prepared-xact retention guard */
#include "access/xact.h"
#include "port/atomics.h"
#include "storage/fd.h"
Expand Down Expand Up @@ -238,28 +239,28 @@ static ClusterUndoRecordShared *UndoRecordShared = NULL;
*/
static SCN *cluster_undo_write_registry = NULL;

/* This backend already registered first_undo_scn for the current top-xact
* (idempotent guard; cleared in cluster_undo_record_xact_reset). */
/* This backend already registered first_undo_scn for the current top-xact.
* Cleared by normal-COMMIT cleanup or PREPARE/ABORT full teardown. */
static bool cluster_undo_write_registered = false;

/* spec-4.12a Hardening v1.0.1 (P0 8.A): this backend already revalidated its
* carried-over residual extent for the current top-xact. The carried residual
* is only stale at the txn's first undo alloc (another backend may have rolled
* the segment away since the previous xact); after the one locked revalidation
* the extent is current-xact-owned and reused lock-free. Cleared in
* cluster_undo_record_xact_reset. */
* the extent is current-xact-owned and reused lock-free. Re-armed by
* normal-COMMIT cleanup or PREPARE/ABORT full teardown. */
static bool cluster_undo_residual_validated_this_xact = false;

/* Per-backend state(D16 PREPARE guard support). */
/* Legacy write marker retained as transaction-local API state. */
static bool cluster_undo_touched_in_xact = false;

/*
* spec-3.18 D3: the backend's currently-held undo extent. segment_id ==
* CLUSTER_UNDO_EXTENT_NONE means "no extent held" (claim one on next write).
* The cursor (cur_block / cur_free_offset / cur_slot_count) advances lock-free
* within the extent; only claiming a new extent touches lifecycle_lock. Reset
* to NONE at end of xact (cluster_undo_record_xact_reset) -- residual blocks
* are dropped (D3.3 will add the Q2 hybrid residual cache).
* within the extent; only claiming a new extent touches lifecycle_lock. A
* residual extent is deliberately carried across transactions and revalidated
* under lifecycle_lock before the next top-xact reuses it.
*/
static ClusterUndoExtent cluster_undo_current_extent = { 0 };

Expand Down Expand Up @@ -322,7 +323,8 @@ static bool cluster_undo_pending_flush_internal(bool error_on_fail);
*
* Top-xact aggregation: subxact undo writes append to the same per-backend
* list and are flushed by the parent's precommit (no per-savepoint fsync).
* Reset (clear) happens at end of top-xact in cluster_undo_record_xact_reset.
* Normal COMMIT clears the live count in its O(1) cleanup hook. PREPARE and
* ABORT clear it in cluster_undo_record_xact_reset().
* Overflow (> MAX distinct segments in one xact — pathological) degrades to an
* inline fsync of the overflowing segment, never back to per-record fsync.
*/
Expand Down Expand Up @@ -380,8 +382,9 @@ cluster_undo_record_touched_segment(uint32 segment_id, uint8 owner_instance)
* undo UBA. Existing spec-3.4b DML callers still pass the TT-only UBA as
* `prev_uba` on every operation, so the record allocator must maintain the
* true per-xact undo chain locally until later specs persist the TT head in
* the undo segment header. This state is reset at xact end together with
* cluster_undo_touched_in_xact.
* the undo segment header. Only entries below cluster_undo_local_head_count
* are live; normal COMMIT resets that count in O(1), while PREPARE/ABORT also
* zero the backing array during full teardown.
*/
typedef struct ClusterUndoLocalHead {
uint16 tt_slot_segment_id;
Expand Down Expand Up @@ -433,7 +436,7 @@ cluster_undo_local_head_ensure(uint16 tt_slot_segment_id, uint16 tt_slot_offset,
* undo-chain head for a TT slot (segment_id, slot_offset), or InvalidUba if
* this backend wrote no undo for that slot this transaction. Used by
* AtPrepare_ClusterTT to capture the head into the 2PC record while it is
* still in memory (the local head array is reset at xact end).
* still in memory, before PostPrepare performs the full local teardown.
*/
UBA
cluster_undo_local_head_get(uint16 tt_slot_segment_id, uint16 tt_slot_offset)
Expand Down Expand Up @@ -606,11 +609,10 @@ cluster_undo_active_write_register(SCN first_undo_scn)

/*
* cluster_undo_active_write_unregister -- drop this backend's registry entry at
* end of top-xact (commit / abort / PREPARE; all route through
* cluster_undo_record_xact_reset). On PREPARE the prepared-xact guard (硬门
* 6) takes over keeping the undo alive -- see cluster_undo_any_unresolved_
* prepared(); the GlobalTransaction is already in TwoPhaseState (EndPrepare)
* before PostPrepare_ClusterTT reaches here, so the handoff has no gap.
* end of top-xact. Normal COMMIT calls the dedicated cleanup hook; PREPARE
* and ABORT call the full reset. On PREPARE, the prepared-xact guard takes
* over retention after EndPrepare and before PostPrepare_ClusterTT unregisters
* the active writer, so the handoff has no gap.
*/
static void
cluster_undo_active_write_unregister(void)
Expand All @@ -633,8 +635,7 @@ cluster_undo_active_write_unregister(void)
}

/*
* cluster_undo_record_xact_commit_release -- spec-4.12a D6: release this
* backend's active-write boundary slot at COMMIT.
* cluster_undo_record_xact_commit_release -- normal-COMMIT undo cleanup.
*
* The active-write boundary (cluster_undo_active_write_boundary) keeps a record
* segment retained from the ACTIVE -> COMMITTED drain only while an in-flight
Expand All @@ -643,27 +644,32 @@ cluster_undo_active_write_unregister(void)
* still gates COMMITTED -> RECYCLABLE), so the boundary entry must drop here --
* otherwise it pins the drain boundary for the life of the backend.
*
* This is the missing commit-path release. cluster_undo_record_xact_reset (the
* full per-xact undo teardown) only runs on the PREPARE (PrepareTransaction) and
* ABORT (CleanupTransaction) paths, NOT CommitTransaction -- so a short-lived
* connection was saved only by backend exit, while a persistent (pooled)
* connection that COMMITs and stays open kept its first transaction's
* first_undo_scn registered forever (register is idempotent per top-xact), which
* pinned the boundary and re-opened the spec-4.13 leak for every long-lived
* connection. Commit needs only the boundary release, not the fd-cache /
* local-head teardown, so this is a dedicated minimal hook (avoids fd-cache
* churn on the commit hot path).
* The full reset only runs on PREPARE and ABORT. Normal COMMIT therefore
* releases the active-write registry entry and clears all O(1) top-transaction
* bookkeeping here: the touched marker, live local-head count, touched-segment
* count, and residual-revalidation gate. The residual extent and fd cache are
* backend caches, not transaction state, and remain available for reuse.
*
* 8.A-safe: dropping the boundary only enables ACTIVE -> COMMITTED; the horizon
* still gates the actual reclaim (COMMITTED -> RECYCLABLE), so a committed xact's
* undo that an older snapshot still needs stays retained until the horizon
* passes. Mirrors the PREPARE handoff, where the boundary also drops and the
* prepared-xact guard (硬门 6) takes over keeping the undo alive.
* passes. PREPARE uses the full reset after its prepared-xact retention guard
* has taken over.
*
* Spec: spec-4.12a-undo-record-segment-reclaim.md
*/
void
cluster_undo_record_xact_commit_release(void)
{
/* Precommit is the release-build fail-closed boundary; keep a debug check at
* the post-commit cleanup site to catch lifecycle regressions early. */
Assert(!cluster_undo_pending.active);

cluster_undo_active_write_unregister();
cluster_undo_touched_in_xact = false;
cluster_undo_local_head_count = 0;
cluster_undo_touched_seg_count = 0;

/* spec-4.12a Hardening v1.0.1: re-arm residual revalidation for the next
* top-xact. xact_reset (which also clears this) runs only on PREPARE/ABORT,
* so the commit path must re-arm it here -- otherwise a persistent connection
Expand Down Expand Up @@ -1725,7 +1731,7 @@ cluster_undo_record_alloc(uint8 record_type, const ClusterUndoRecordTarget *targ

pg_atomic_fetch_add_u64(&UndoRecordShared->record_alloc_count, 1);

/* Mark backend touched for D16 PREPARE guard. */
/* Mark transaction-local undo state as touched. */
cluster_undo_touched_in_xact = true;

/* P0 perf hardening: record the dirtied undo segment for a single per-xact
Expand Down Expand Up @@ -1848,8 +1854,9 @@ cluster_undo_get_record(UBA uba, void *out_buffer, size_t buffer_size)
* critical section, so the xact aborts cleanly (its un-fsync'd undo blocks are
* irrelevant to an aborted xact) — never a silent half-durable commit.
*
* No-op for a xact that wrote no undo (DDL-only / read-only). The touched
* list is cleared by cluster_undo_record_xact_reset at end of xact.
* No-op for a xact that wrote no undo (DDL-only / read-only). Normal COMMIT
* clears the touched list in its O(1) cleanup hook; PREPARE/ABORT use the full
* reset.
*/
void
cluster_undo_xact_precommit_flush(void)
Expand All @@ -1863,6 +1870,11 @@ cluster_undo_xact_precommit_flush(void)
* pending image exists exactly on the write-back path.
*/
(void)cluster_undo_pending_flush_internal(true);
if (unlikely(cluster_undo_pending.active))
ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR),
errmsg("cluster undo block remained pending before transaction finish"),
errhint("Retry the transaction and inspect the server log for an undo WAL "
"flush failure.")));

if (cluster_undo_touched_seg_count == 0)
return;
Expand Down Expand Up @@ -1901,14 +1913,11 @@ cluster_undo_xact_precommit_flush(void)


/*
* cluster_undo_record_xact_reset -- end-of-xact hook; reset per-backend
* touched flag. Called from xact.c CommitTransaction / AbortTransaction.
* cluster_undo_record_xact_reset -- PREPARE/ABORT full per-backend teardown.
*
* Also clears the per-xact touched-undo-segment list. On commit the list was
* already fsync'd by cluster_undo_xact_precommit_flush; on abort it is simply
* dropped (un-fsync'd undo of an aborted xact needs no durability) — this is
* the lazy/best-effort abort path. Clearing here prevents leakage into the
* next xact on this backend.
* PREPARE already flushed the touched undo segments before EndPrepare. On
* ABORT the list is simply dropped because aborted undo needs no durability.
* Normal COMMIT uses cluster_undo_record_xact_commit_release() instead.
*/
void
cluster_undo_record_xact_reset(void)
Expand All @@ -1933,9 +1942,7 @@ cluster_undo_record_xact_reset(void)
}

cluster_undo_touched_in_xact = false;
/* spec-4.12a D1: drop this backend's active-write registry entry (commit /
* abort / PREPARE all reach here). On PREPARE the prepared-xact guard (硬门
* 6) keeps the undo alive past this point. */
/* PREPARE's durable guard keeps its undo alive after this handoff. */
cluster_undo_active_write_unregister();
/* spec-4.12a Hardening v1.0.1: re-arm residual revalidation for the next
* top-xact (its carried residual must be re-checked under lifecycle_lock). */
Expand All @@ -1953,14 +1960,13 @@ cluster_undo_record_xact_reset(void)
* recycled, so a residual in it is always safe). A block may then hold
* records from several transactions -- correct (UBA addresses by row).
*/
/* P0 perf hardening: drop the cached undo segment fd at xact end to bound
* stale-fd exposure (e.g. a recycled segment) across transactions. */
/* Full PREPARE/ABORT teardown closes the cache. Normal COMMIT preserves it. */
cluster_undo_smgr_fd_cache_reset();
}


/*
* cluster_undo_record_is_touched -- D16 PREPARE TRANSACTION guard helper.
* cluster_undo_record_is_touched -- legacy transaction-local write marker.
*/
bool
cluster_undo_record_is_touched(void)
Expand Down
12 changes: 6 additions & 6 deletions src/backend/cluster/storage/cluster_undo_smgr.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@
* (8.5 open + 8.5 close per TPC-B txn). Cache ONE O_RDWR fd for the
* most-recently-used (segment, owner) — O_RDWR serves both read_block and
* write_block. Self-heals on (segment, owner) mismatch (close old + open
* new), bounding stale-fd exposure (e.g. a recycled segment) further by an
* xact-end reset (cluster_undo_record_xact_reset -> _fd_cache_reset) and a
* before_shmem_exit close. Per-backend fds to a shared segment file are
* independent; each pwrites its own offset range, so no cross-backend hazard.
* new). Normal COMMIT preserves the cache; PREPARE/ABORT full teardown,
* cache-key mismatch, and before_shmem_exit close it. Per-backend fds to a
* shared segment file are independent; each pwrites its own offset range, so
* there is no cross-backend hazard.
*/
static uint32 cached_fd_segment = 0; /* 0 = empty */
static uint8 cached_fd_owner = 0;
Expand Down Expand Up @@ -128,8 +128,8 @@ get_segment_fd(ClusterUndoPathIntent intent, uint32 segment_id, uint8 owner_inst
}

/*
* cluster_undo_smgr_fd_cache_reset -- close the cached fd (xact end / segment
* recycle). Called from cluster_undo_record_xact_reset.
* cluster_undo_smgr_fd_cache_reset -- close the cached fd during full local
* teardown or explicit cache invalidation. Normal COMMIT does not call it.
*/
void
cluster_undo_smgr_fd_cache_reset(void)
Expand Down
6 changes: 3 additions & 3 deletions src/include/cluster/cluster_tt_2pc.h
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ extern bool cluster_tt_2pc_parse_record(const void *recdata, uint32 len, Cluster
* chains at step 4). AtPrepare serializes only -- NO state mutation
* (PG two-phase contract; an EndPrepare failure must still abort with
* the backend-local state intact). PostPrepare transfers ownership to
* the 2PC record: clears the backend-local TT bindings, sub-links and
* the ITL touch list (V-2: the touch list is droppable -- overlay /
* durable TT are authoritative and 3.4c lazy cleanout re-stamps pages).
* the 2PC record: clears the backend-local TT bindings, sub-links, ITL touch
* list, and undo lifecycle state. The undo reset also removes the active-write
* registry entry after the prepared-xact retention guard has taken over.
*/
extern void AtPrepare_ClusterTT(void);
extern void PostPrepare_ClusterTT(void);
Expand Down
Loading
Loading