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
543 changes: 508 additions & 35 deletions src/backend/access/heap/heapam.c

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions src/backend/access/heap/heapam_visibility.c
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,31 @@ SetHintBits(HeapTupleHeader tuple, Buffer buffer, uint16 infomask, TransactionId
MarkBufferDirtyHint(buffer, true);
}

#ifdef USE_PGRAC_CLUSTER
/*
* cluster_heap_stamp_released_xmax_invalid -- authority-backed XMAX_INVALID.
*
* spec-7.1a D0/D2: SetHintBits above deliberately suppresses xmax hints
* whose xid is another node's class (spec-6.15 D7) because ordinary hint
* stamping is backed by native CLOG/ProcArray answers that are void for a
* foreign xid. The cluster writer/locker paths however must still
* physically clear a RELEASED (lock-only terminal) or ABORTED remote xmax
* once CLUSTER authority (TT / live-IC verdict) proved it gone -- leaving
* it in place makes compute_new_xmax_infomask treat the dead remote xid as
* a live locker and fold it into a node-local MultiXact that aliases on
* every peer (L349). This is the one sanctioned bypass: the fact comes
* from cluster authority, never from a native reading of the foreign xid,
* and marking released/aborted needs no commit-LSN interlock (see the
* SetHintBits header: aborted hints are always safe).
*/
void
cluster_heap_stamp_released_xmax_invalid(HeapTupleHeader tuple, Buffer buffer)
{
tuple->t_infomask |= HEAP_XMAX_INVALID;
MarkBufferDirtyHint(buffer, true);
}
#endif

/*
* HeapTupleSetHintBits --- exported version of SetHintBits()
*
Expand Down Expand Up @@ -852,12 +877,15 @@ cluster_satisfies_update_fork(HeapTuple htup, Buffer buffer, TM_Result *res)
if (r.evidence == CLUSTER_VIS_EVIDENCE_REMOTE) {
switch (cluster_vis_update_xmax_verdict(r.status, is_delete)) {
case CVV_VISIBLE:
cluster_vis_bump_xmax_resolved_count(); /* spec-7.1a D6 */
*res = TM_Ok;
return true;
case CVV_GONE_UPDATED:
cluster_vis_bump_xmax_resolved_count(); /* spec-7.1a D6 */
*res = TM_Updated;
return true;
case CVV_GONE_DELETED:
cluster_vis_bump_xmax_resolved_count(); /* spec-7.1a D6 */
*res = TM_Deleted;
return true;
case CVV_BEING_MODIFIED:
Expand Down Expand Up @@ -1566,8 +1594,10 @@ cluster_remote_live_xmax_keeps_visible(Buffer buffer, HeapTupleHeader tuple, Sna

switch (cluster_vis_cr_xmax_verdict(xr.status, scn_decision)) {
case CVV_VISIBLE:
cluster_vis_bump_xmax_resolved_count(); /* spec-7.1a D6 */
return 1;
case CVV_INVISIBLE:
cluster_vis_bump_xmax_resolved_count(); /* spec-7.1a D6 */
return 0;
default:
return -1; /* unknown / unresolved -> fail closed */
Expand Down
1 change: 1 addition & 0 deletions src/backend/cluster/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ OBJS = \
cluster_undo_cleaner.o \
cluster_visibility_resolve.o \
cluster_visibility_verdict.o \
cluster_writer_chain.o \
cluster_undo_record.o \
cluster_undo_retention.o \
cluster_undo_srf.o \
Expand Down
50 changes: 45 additions & 5 deletions src/backend/cluster/cluster_cr_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,10 @@ lms_undo_fetch_serve(ClusterLmsCrSlot *slot)
slot->undo_auth.origin_epoch = cluster_epoch_get_current();
slot->undo_auth.live_hwm_lsn = GetFlushRecPtr(NULL);
slot->undo_auth.tt_generation = cluster_undo_tt_retention_rollover_count();
/* PGRAC: spec-7.1a D3 -- co-sample the origin SCN clock with the same
* pre-content-read ordering (a stamp landing after the sample only makes
* the content newer than claimed; additive and safe). */
slot->undo_auth.authority_scn = cluster_scn_current();

/* Serve only SELF-owned undo: the owner derives from this node's own
* id, never from the wire (a forged request cannot redirect the read). */
Expand Down Expand Up @@ -409,6 +413,10 @@ lms_undo_verdict_serve(ClusterLmsCrSlot *slot)
slot->undo_auth.origin_epoch = cluster_epoch_get_current();
slot->undo_auth.live_hwm_lsn = GetFlushRecPtr(NULL);
slot->undo_auth.tt_generation = cluster_undo_tt_retention_rollover_count();
/* PGRAC: spec-7.1a D3 -- co-sample the origin SCN clock with the same
* pre-content-read ordering (a stamp landing after the sample only makes
* the content newer than claimed; additive and safe). */
slot->undo_auth.authority_scn = cluster_scn_current();

memset(slot->result_page, 0, BLCKSZ);
v->magic = CLUSTER_GCS_UNDO_VERDICT_MAGIC;
Expand All @@ -422,11 +430,41 @@ lms_undo_verdict_serve(ClusterLmsCrSlot *slot)
case CLUSTER_TT_DURABLE_RESOLVED_SCN:
if (!TransactionIdDidCommit(xid))
return false; /* C1b: stamped-then-crashed is in-doubt */
if (!cluster_cr_accept_resolved_scn(scn))
return false; /* wrap-suspect -> refuse */
v->verdict = (uint8)CLUSTER_GCS_UNDO_VERDICT_COMMITTED_EXACT;
v->commit_scn = scn;
v->wrap = wrap;
if (cluster_cr_accept_resolved_scn(scn)) {
v->verdict = (uint8)CLUSTER_GCS_UNDO_VERDICT_COMMITTED_EXACT;
v->commit_scn = scn;
v->wrap = wrap;
return true;
}

/*
* PGRAC: spec-7.1a hardening -- wrap-suspect stamped scn (below the
* retention horizon), reached routinely now that the requester
* finalizes EVERY shipped COMMITTED stamp here instead of concluding
* on the fetch fast leg. The EXACT value cannot be shipped (a
* same-valued xid recurrence across TT wrap could own a different
* scn), but "committed at/below a FROZEN bound" still can:
* - a LIVE recurrence would be a second by-value match ->
* AMBIGUOUS_WRAP (refused below), so the single live match has
* no live rival;
* - a RECYCLED recurrence's lost scn is at/below the max gated-
* recycle horizon (the zero-match arm's own bound);
* - this slot's own stamped scn bounds itself.
* Ship BELOW_HORIZON over the max of both candidates -- the same
* frozen, non-clock-chasing consumer contract as the zero-match arm
* (never cached; judged against the requester's read_scn, leg (e)).
* No gated recycle this incarnation -> the recycled-recurrence
* candidate is unboundable -> refuse, exactly like zero-match.
*/
if (!cluster_cr_retention_proof_origin_legs(&horizon))
return false;
horizon = cluster_tt_slot_max_recycle_horizon();
if (!SCN_VALID(horizon))
return false;
if (scn_time_cmp(scn, horizon) > 0)
horizon = scn;
v->verdict = (uint8)CLUSTER_GCS_UNDO_VERDICT_COMMITTED_BELOW_HORIZON;
v->horizon_scn = horizon;
return true;

case CLUSTER_TT_DURABLE_RECYCLED_ZERO_MATCH:
Expand Down Expand Up @@ -660,6 +698,8 @@ cluster_lms_cr_ship_ready(void)
hdr->page_lsn = slot->undo_auth.live_hwm_lsn;
memcpy(buf + header_len, slot->result_page, BLCKSZ);
ClusterGcsUndoAuthTrailerSetTtGeneration(trailer, slot->undo_auth.tt_generation);
ClusterGcsUndoAuthTrailerSetAuthorityScn(trailer,
(uint64)slot->undo_auth.authority_scn);
}
hdr->checksum = cluster_gcs_block_compute_checksum(buf + header_len);

Expand Down
11 changes: 11 additions & 0 deletions src/backend/cluster/cluster_debug.c
Original file line number Diff line number Diff line change
Expand Up @@ -2411,6 +2411,17 @@ dump_visibility(ReturnSetInfo *rsinfo)
fmt_int64((int64)cluster_vis_get_prune_remote_keep_count()));
emit_row(rsinfo, "visibility", "vis_variant_unknown_failclosed_count",
fmt_int64((int64)cluster_vis_get_vis_variant_unknown_failclosed_count()));
/* PGRAC: spec-7.1a D6 -- write-write chaining counters. */
emit_row(rsinfo, "visibility", "writer_chain_resolved_count",
fmt_int64((int64)cluster_vis_get_writer_chain_resolved_count()));
emit_row(rsinfo, "visibility", "writer_chain_failclosed_count",
fmt_int64((int64)cluster_vis_get_writer_chain_failclosed_count()));
emit_row(rsinfo, "visibility", "xmax_resolved_count",
fmt_int64((int64)cluster_vis_get_xmax_resolved_count()));
emit_row(rsinfo, "visibility", "overlay_refresh_count",
fmt_int64((int64)cluster_vis_get_overlay_refresh_count()));
emit_row(rsinfo, "visibility", "covers_scn_refuse_count",
fmt_int64((int64)cluster_vis_get_covers_scn_refuse_count()));
}


Expand Down
8 changes: 8 additions & 0 deletions src/backend/cluster/cluster_gcs_block.c
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ typedef struct ClusterGcsBlockOutstandingSlot {
* UNDO_TT_FETCH_RESULT reply (epoch / live_hwm ride the header). */
bool reply_undo_trailer_valid;
uint64 reply_undo_tt_generation;
uint64 reply_undo_authority_scn; /* PGRAC: spec-7.1a D3 (trailer SCN) */
ConditionVariable reply_cv;
/* PGRAC: spec-2.34 D3/D4 — HC100 stale-reply defense + epoch invalidation.
* request_epoch: snapshot of cluster_epoch at the time the
Expand Down Expand Up @@ -2511,6 +2512,7 @@ cluster_gcs_block_undo_tt_fetch_and_wait(int32 origin_node, uint32 segment_id, u
cluster_sf_dep_vec_reset(&slot->reply_sf_dep_vec);
slot->reply_undo_trailer_valid = false;
slot->reply_undo_tt_generation = 0;
slot->reply_undo_authority_scn = 0;
slot->request_epoch = cluster_epoch_get_current();
slot->expected_master_node = cluster_node_id;
slot->stale = false;
Expand Down Expand Up @@ -2570,6 +2572,7 @@ cluster_gcs_block_undo_tt_fetch_and_wait(int32 origin_node, uint32 segment_id, u
auth_out->origin_epoch = slot->reply_header.epoch;
auth_out->live_hwm_lsn = (XLogRecPtr)slot->reply_header.page_lsn;
auth_out->tt_generation = slot->reply_undo_tt_generation;
auth_out->authority_scn = (SCN)slot->reply_undo_authority_scn;
/* spec-5.14 D2: the authority is the origin's volatile
* co-sample — depend on it for fail-stop (D-i3). */
gcs_block_stamp_touched(origin_node, GCS_BLOCK_REPLY_NO_FORWARDING_MASTER);
Expand Down Expand Up @@ -2646,6 +2649,7 @@ cluster_gcs_block_undo_verdict_fetch_and_wait(int32 origin_node, uint32 segment_
cluster_sf_dep_vec_reset(&slot->reply_sf_dep_vec);
slot->reply_undo_trailer_valid = false;
slot->reply_undo_tt_generation = 0;
slot->reply_undo_authority_scn = 0;
slot->request_epoch = cluster_epoch_get_current();
slot->expected_master_node = cluster_node_id;
slot->stale = false;
Expand Down Expand Up @@ -2711,6 +2715,7 @@ cluster_gcs_block_undo_verdict_fetch_and_wait(int32 origin_node, uint32 segment_
auth_out->origin_epoch = slot->reply_header.epoch;
auth_out->live_hwm_lsn = (XLogRecPtr)slot->reply_header.page_lsn;
auth_out->tt_generation = slot->reply_undo_tt_generation;
auth_out->authority_scn = (SCN)slot->reply_undo_authority_scn;
/* spec-5.14 D2: the verdict is the origin's volatile
* co-sample — depend on it for fail-stop (D-i3). */
gcs_block_stamp_touched(origin_node, GCS_BLOCK_REPLY_NO_FORWARDING_MASTER);
Expand Down Expand Up @@ -5017,6 +5022,9 @@ cluster_gcs_handle_block_reply_envelope(const ClusterICEnvelope *env, const void
slot->reply_undo_tt_generation
= (undo_trailer != NULL) ? ClusterGcsUndoAuthTrailerGetTtGeneration(undo_trailer)
: 0;
slot->reply_undo_authority_scn
= (undo_trailer != NULL) ? ClusterGcsUndoAuthTrailerGetAuthorityScn(undo_trailer)
: 0;
slot->reply_received = true;
ConditionVariableSignal(&slot->reply_cv);
LWLockRelease(&blk->lock.lock);
Expand Down
16 changes: 16 additions & 0 deletions src/backend/cluster/cluster_guc.c
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,11 @@ bool cluster_gcs_block_local_cache = true;
* the spec-3.4d fail-closed (53R98) honest degradation. */
bool cluster_tx_enqueue_wait_enabled = true;
bool cluster_ic_duty_lazy = true; /* spec-7.2 D1 duty-chain on-demand gating */

/* PGRAC: spec-7.1a D0 -- cross-node write-write chaining (default off).
* Off keeps the pre-7.1a floor: a TERMINAL remote writer holder fails
* closed (SQLSTATE 53R9H) instead of chaining to a sound TM_Result. */
bool cluster_crossnode_write_write = false;
int cluster_gcs_block_dedup_max_entries = 1024;

/*
Expand Down Expand Up @@ -3895,6 +3900,17 @@ cluster_init_guc(void)
"iteration behavior (escape hatch). spec-7.2 D1. PGC_SIGHUP."),
&cluster_ic_duty_lazy, true, PGC_SIGHUP, 0, NULL, NULL, NULL);

DefineCustomBoolVariable(
"cluster.crossnode_write_write",
gettext_noop("Chain a local write past a terminal remote writer."),
gettext_noop("When on, a write that conflicts with a remote writer "
"that already committed or aborted maps the outcome onto "
"the native TM_Result contract (remote UPDATE -> chase the "
"new version via EvalPlanQual; remote DELETE -> deleted; "
"aborted -> proceed). Off (default) keeps the fail-closed "
"floor: SQLSTATE 53R9H, retry. Unprovable outcomes fail "
"closed either way. spec-7.1a D0. PGC_SUSET."),
&cluster_crossnode_write_write, false, PGC_SUSET, 0, NULL, NULL, NULL);
DefineCustomIntVariable(
"cluster.gcs_block_retransmit_initial_backoff_ms",
gettext_noop("Initial backoff before retry 1 (subsequent retries double)."),
Expand Down
Loading
Loading