diff --git a/src/backend/cluster/Makefile b/src/backend/cluster/Makefile index 5ea41237f7..77e4a0d4e3 100644 --- a/src/backend/cluster/Makefile +++ b/src/backend/cluster/Makefile @@ -183,6 +183,9 @@ OBJS = \ cluster_visibility_resolve.o \ cluster_visibility_verdict.o \ cluster_undo_record.o \ + cluster_undo_resid.o \ + cluster_undo_gcs.o \ + cluster_undo_gcs_grant.o \ cluster_undo_retention.o \ cluster_undo_srf.o \ cluster_cr.o \ diff --git a/src/backend/cluster/cluster_cr_server.c b/src/backend/cluster/cluster_cr_server.c index 7aafcf3e5a..034a76eec1 100644 --- a/src/backend/cluster/cluster_cr_server.c +++ b/src/backend/cluster/cluster_cr_server.c @@ -335,7 +335,8 @@ lms_undo_fetch_serve(ClusterLmsCrSlot *slot) /* 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). */ - return cluster_undo_smgr_read_block(slot->undo_segment_id, (uint8)(cluster_node_id + 1), + return cluster_undo_smgr_read_block(cluster_undo_intent_for_owner((uint8)(cluster_node_id + 1)), + slot->undo_segment_id, (uint8)(cluster_node_id + 1), slot->undo_block_no, slot->result_page); } diff --git a/src/backend/cluster/cluster_grd.c b/src/backend/cluster/cluster_grd.c index 33c5b2b7c9..836d825df4 100644 --- a/src/backend/cluster/cluster_grd.c +++ b/src/backend/cluster/cluster_grd.c @@ -53,6 +53,7 @@ #include "cluster/cluster_epoch.h" /* spec-4.6 D1 — accepted epoch reads */ #include "cluster/cluster_reconfig.h" /* spec-4.6 D1 — reconfig event consume */ #include "cluster/cluster_thread_recovery.h" /* spec-4.11 D3 — unfreeze gate */ +#include "cluster/cluster_undo_resid.h" /* spec-5.22a D1-5 — undo-class hash-route guard */ #include "storage/procsignal.h" /* spec-4.6 D3 — redeclare broadcast */ #include "storage/sinvaladt.h" /* spec-4.6 D3 — BackendIdGetProc */ #include "utils/timestamp.h" /* spec-4.6 D1 — barrier deadline */ @@ -993,6 +994,24 @@ cluster_grd_lookup_master(const ClusterResId *resid) uint32 shard_id; int32 master; + /* + * PGRAC: spec-5.22a D1-5 -- undo resids are owner-as-master resources; + * their master is the encoded owner (cluster_undo_resid_master), never a + * shard-hash node. A hash-derived master would place the undo authority + * at a node that does not own the undo, so no caller may hash-route the + * undo class. Fail closed: no data-plane path legitimately reaches here + * with an undo resid. + */ + if (resid != NULL && resid->type == CLUSTER_UNDO_RESID_TYPE) { + Assert(false); + ereport( + ERROR, + (errcode(ERRCODE_CLUSTER_UNDO_RESID_HASH_ROUTED), + errmsg("undo resource id must not be routed through the GRD hash master lookup"), + errhint( + "Undo resources are owner-as-master; route via cluster_undo_resid_master()."))); + } + Assert(cluster_grd_state != NULL); shard_id = cluster_grd_shard_for_resource(resid); diff --git a/src/backend/cluster/cluster_guc.c b/src/backend/cluster/cluster_guc.c index 3334422711..2f7978eeac 100644 --- a/src/backend/cluster/cluster_guc.c +++ b/src/backend/cluster/cluster_guc.c @@ -119,6 +119,11 @@ bool cluster_past_image = false; /* spec-6.12i: active-runtime cross-instance recycled-slot visibility * resolution via undo-block CF fetch (default OFF = 53R97). */ bool cluster_crossnode_runtime_visibility = false; +/* spec-5.22b D2-2: shared-undo GCS coherence master switch (default OFF = + * undo stays on the local DataDir, inert -- 6.12i unmastered fetch path; ON = + * own-instance runtime AND redo undo migrate to the shared cluster_fs root + * under owner-as-master GCS grant/PI). PGC_SIGHUP. */ +bool cluster_undo_gcs_coherence = false; /* spec-6.15 D1: xid space segmentation -- striped allocation (default * OFF = vanilla dense per-node xid allocation). */ bool cluster_xid_striping = false; @@ -985,6 +990,31 @@ check_cluster_shared_data_dir(char **newval, void **extra, GucSource source) return true; } +/* + * check_cluster_undo_gcs_coherence -- GUC check_hook for + * cluster.undo_gcs_coherence (spec-5.22b D2-2). + * + * Turning the shared-undo data plane ON physically migrates own-instance + * runtime + redo undo onto the shared cluster_fs root, so it REQUIRES + * cluster.shared_data_dir to name that root. Reject the ON transition when + * the root is unset: a clear config-time error beats a runtime read that has + * to fail closed with a generic SQLSTATE. (The runtime path additionally + * fails closed -- an unresolvable shared path yields the 53R97 visibility + * fail-close, never a bogus local fallback -- so this hook is operability, + * not the correctness backstop.) + */ +static bool +check_cluster_undo_gcs_coherence(bool *newval, void **extra, GucSource source) +{ + if (*newval && (cluster_shared_data_dir == NULL || cluster_shared_data_dir[0] == '\0')) { + GUC_check_errdetail( + "cluster.undo_gcs_coherence requires cluster.shared_data_dir to name the shared " + "cluster_fs mount (the same root used by shared_catalog and the recovery anchor)."); + return false; + } + return true; +} + static bool check_cluster_block_device_path(char **newval, void **extra, GucSource source) { @@ -1588,6 +1618,28 @@ cluster_init_guc(void) "(SQLSTATE 53R97)."), &cluster_crossnode_runtime_visibility, false, PGC_SUSET, 0, NULL, NULL, NULL); + /* + * cluster.undo_gcs_coherence -- spec-5.22b D2 master switch for the + * shared-undo block data plane. OFF (default): undo segments stay on the + * local DataDir and cross-instance undo reads take the 6.12i unmastered + * fetch path (fresh ref => 53R97) -- byte-identical to pre-D2, a safe + * rollback surface. ON: own-instance runtime AND redo undo writes migrate + * to the shared cluster_fs root (cluster.shared_data_dir), and undo blocks + * become owner-as-master GCS resources (grant / PI land in D2-3/D2-4). + * Gating physical migration on this switch (not merely peer-mode) keeps + * runtime and redo writes consistent -- both move only when it is on, so a + * default deployment never splits runtime-shared vs redo-local + * (Hardening v1.0.1 裁决 A). PGC_SIGHUP: flippable for the D6 end-to-end + * validation window; a default-ON flip is a separate decision after D3-D6. + */ + DefineCustomBoolVariable( + "cluster.undo_gcs_coherence", + gettext_noop("Enable the shared-undo block GCS data plane (spec-5.22b)."), + gettext_noop("Off keeps undo on the local data dir and cross-instance undo " + "fail-closed (SQLSTATE 53R97). Requires cluster.shared_data_dir when on."), + &cluster_undo_gcs_coherence, false, PGC_SIGHUP, 0, check_cluster_undo_gcs_coherence, NULL, + NULL); + /* * cluster.xid_striping -- spec-6.15 D1 (AD-012 exception 10 xid * space segmentation). When on, this node only issues 32-bit xids diff --git a/src/backend/cluster/cluster_tt_durable.c b/src/backend/cluster/cluster_tt_durable.c index de68784c52..0f96ab6c4e 100644 --- a/src/backend/cluster/cluster_tt_durable.c +++ b/src/backend/cluster/cluster_tt_durable.c @@ -183,7 +183,8 @@ tt_slot_write_committed(uint32 segment_id, uint8 owner, uint16 slot_offset, Tran cluster_tt_durable_io_wait_start(); - if (!cluster_undo_smgr_read_header_bytes(segment_id, owner, off, (char *)&slot, sizeof(slot))) { + if (!cluster_undo_smgr_read_header_bytes(cluster_undo_intent_for_owner(owner), segment_id, + owner, off, (char *)&slot, sizeof(slot))) { cluster_tt_durable_io_wait_end(); ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("cluster durable TT: cannot read slot %u of undo segment %u", @@ -196,8 +197,8 @@ tt_slot_write_committed(uint32 segment_id, uint8 owner, uint16 slot_offset, Tran slot.commit_scn = commit_scn; slot.first_undo_block = InvalidUbaVal; /* spec-4.8 D7-A (P1#1): no stale head */ - if (!cluster_undo_smgr_write_header_bytes(segment_id, owner, off, (const char *)&slot, - sizeof(slot))) { + if (!cluster_undo_smgr_write_header_bytes(cluster_undo_intent_for_owner(owner), segment_id, + owner, off, (const char *)&slot, sizeof(slot))) { cluster_tt_durable_io_wait_end(); ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("cluster durable TT: cannot write slot %u of undo segment %u", @@ -283,7 +284,8 @@ cluster_tt_slot_durable_abort(uint32 segment_id, uint16 slot_offset, Transaction cluster_tt_durable_io_wait_start(); - if (!cluster_undo_smgr_read_header_bytes(segment_id, owner, off, (char *)&slot, sizeof(slot))) { + if (!cluster_undo_smgr_read_header_bytes(cluster_undo_intent_for_owner(owner), segment_id, + owner, off, (char *)&slot, sizeof(slot))) { cluster_tt_durable_io_wait_end(); ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("cluster durable TT: cannot read slot %u of undo segment %u", @@ -296,8 +298,8 @@ cluster_tt_slot_durable_abort(uint32 segment_id, uint16 slot_offset, Transaction slot.commit_scn = InvalidScn; slot.first_undo_block = InvalidUbaVal; /* spec-4.8 D7-A (P1#1): cleared; 0x90 re-attaches */ - if (!cluster_undo_smgr_write_header_bytes(segment_id, owner, off, (const char *)&slot, - sizeof(slot))) { + if (!cluster_undo_smgr_write_header_bytes(cluster_undo_intent_for_owner(owner), segment_id, + owner, off, (const char *)&slot, sizeof(slot))) { cluster_tt_durable_io_wait_end(); ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("cluster durable TT: cannot write slot %u of undo segment %u", @@ -336,7 +338,8 @@ cluster_tt_slot_durable_set_head(uint32 segment_id, uint16 slot_offset, Transact cluster_tt_durable_io_wait_start(); - if (!cluster_undo_smgr_read_header_bytes(segment_id, owner, off, (char *)&slot, sizeof(slot))) { + if (!cluster_undo_smgr_read_header_bytes(cluster_undo_intent_for_owner(owner), segment_id, + owner, off, (char *)&slot, sizeof(slot))) { cluster_tt_durable_io_wait_end(); ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("cluster durable TT: cannot read slot %u of undo segment %u", @@ -347,8 +350,8 @@ cluster_tt_slot_durable_set_head(uint32 segment_id, uint16 slot_offset, Transact * (xid, wrap); a recycled slot belongs to a newer owner -> leave untouched. */ if (slot.xid == xid && slot.wrap == wrap) { slot.first_undo_block = first_undo_block; - if (!cluster_undo_smgr_write_header_bytes(segment_id, owner, off, (const char *)&slot, - sizeof(slot))) { + if (!cluster_undo_smgr_write_header_bytes(cluster_undo_intent_for_owner(owner), segment_id, + owner, off, (const char *)&slot, sizeof(slot))) { cluster_tt_durable_io_wait_end(); ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("cluster durable TT: cannot write slot %u of undo segment %u", @@ -375,7 +378,8 @@ cluster_tt_slot_durable_lookup(uint32 segment_id, uint16 slot_offset, Transactio off = tt_slot_file_offset(slot_offset); cluster_tt_durable_io_wait_start(); - if (!cluster_undo_smgr_read_header_bytes(segment_id, owner, off, (char *)&slot, sizeof(slot))) { + if (!cluster_undo_smgr_read_header_bytes(cluster_undo_intent_for_owner(owner), segment_id, + owner, off, (char *)&slot, sizeof(slot))) { cluster_tt_durable_io_wait_end(); cluster_tt_durable_count_lookup(false); return false; /* segment absent / I/O -> miss (caller fail-closes) */ @@ -416,8 +420,8 @@ cluster_tt_slot_durable_lookup_committed_stable(uint32 segment_id, uint16 slot_o off = tt_slot_file_offset(slot_offset); cluster_tt_durable_io_wait_start(); - if (!cluster_undo_smgr_read_header_bytes(segment_id, owner, off, (char *)&first, - sizeof(first))) { + if (!cluster_undo_smgr_read_header_bytes(cluster_undo_intent_for_owner(owner), segment_id, + owner, off, (char *)&first, sizeof(first))) { cluster_tt_durable_io_wait_end(); cluster_tt_durable_count_lookup(false); return false; @@ -436,8 +440,8 @@ cluster_tt_slot_durable_lookup_committed_stable(uint32 segment_id, uint16 slot_o } cluster_tt_durable_io_wait_start(); - if (!cluster_undo_smgr_read_header_bytes(segment_id, owner, off, (char *)&second, - sizeof(second))) { + if (!cluster_undo_smgr_read_header_bytes(cluster_undo_intent_for_owner(owner), segment_id, + owner, off, (char *)&second, sizeof(second))) { cluster_tt_durable_io_wait_end(); cluster_tt_durable_count_lookup(false); return false; @@ -665,7 +669,8 @@ cluster_tt_slot_durable_resolve_by_xid_origin(int origin_node, TransactionId xid UndoSegmentHeaderData *hdr; uint16 i; - if (!cluster_undo_smgr_read_block(segment_id, owner, 0, blockbuf.data)) { + if (!cluster_undo_smgr_read_block(cluster_undo_intent_for_owner(owner), segment_id, owner, + 0, blockbuf.data)) { /* absent segment -> sound skip; existing+unreadable -> incomplete. */ if (cluster_undo_segment_file_exists(owner, segment_id)) scan_complete = false; @@ -763,7 +768,8 @@ cluster_undo_segment_tt_header_scan_pass(uint32 segment_id, uint8 owner_instance /* Whole-block read mirrors the by-xid scan shape (one smgr surface). */ cluster_undo_cleaner_scan_wait_start(); - if (!cluster_undo_smgr_read_block(segment_id, owner_instance, 0, block.data)) { + if (!cluster_undo_smgr_read_block(cluster_undo_intent_for_owner(owner_instance), segment_id, + owner_instance, 0, block.data)) { cluster_undo_cleaner_scan_wait_end(); return false; /* absent / I/O: caller counts and moves on */ } diff --git a/src/backend/cluster/cluster_tt_recovery.c b/src/backend/cluster/cluster_tt_recovery.c index 228198b42b..f38a108218 100644 --- a/src/backend/cluster/cluster_tt_recovery.c +++ b/src/backend/cluster/cluster_tt_recovery.c @@ -157,7 +157,8 @@ cluster_tt_recovery_resolve_active_slots(void) UndoSegmentHeaderData *hdr; uint16 i; - if (!cluster_undo_smgr_read_block(segment_id, owner, 0, blockbuf.data)) + if (!cluster_undo_smgr_read_block(cluster_undo_intent_for_owner(owner), segment_id, owner, + 0, blockbuf.data)) continue; /* absent / unreadable -> skip (never false-abort) */ hdr = (UndoSegmentHeaderData *)blockbuf.data; @@ -235,7 +236,8 @@ cluster_tt_recovery_observe_scn_highwater(void) UndoSegmentHeaderData *hdr; uint16 i; - if (!cluster_undo_smgr_read_block(segment_id, owner, 0, blockbuf.data)) + if (!cluster_undo_smgr_read_block(cluster_undo_intent_for_owner(owner), segment_id, owner, + 0, blockbuf.data)) continue; /* absent / unreadable -> skip */ hdr = (UndoSegmentHeaderData *)blockbuf.data; @@ -483,7 +485,8 @@ cluster_tt_recovery_physical_rollback(void) UndoSegmentHeaderData *hdr; uint16 i; - if (!cluster_undo_smgr_read_block(segment_id, owner, 0, blockbuf.data)) + if (!cluster_undo_smgr_read_block(cluster_undo_intent_for_owner(owner), segment_id, owner, + 0, blockbuf.data)) continue; /* absent / unreadable -> skip */ hdr = (UndoSegmentHeaderData *)blockbuf.data; diff --git a/src/backend/cluster/cluster_undo_gcs.c b/src/backend/cluster/cluster_undo_gcs.c new file mode 100644 index 0000000000..09bfc0ad2f --- /dev/null +++ b/src/backend/cluster/cluster_undo_gcs.c @@ -0,0 +1,171 @@ +/*------------------------------------------------------------------------- + * + * cluster_undo_gcs.c + * Shared-undo block GCS integration -- owner-as-master routing + + * coherent grant / PI data plane (spec-5.22b D2). + * + * D2-1 (this increment) ships owner-as-master routing: the two routing + * predicates that keep undo resources off the GRD/GCS hash-master path + * (their authority lives at the owning instance). The grant / PI / + * serve-gate / physical-migration legs (D2-2 .. D2-5) land here as they + * are implemented. + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * Portions Copyright (c) 2026, pgrac contributors + * + * Author: SqlRush + * + * IDENTIFICATION + * src/backend/cluster/cluster_undo_gcs.c + * + * NOTES + * This is a pgrac-original file (no derivation from PostgreSQL). + * Spec: spec-5.22b-undo-block-gcs-integration.md (D2, §2.1) + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cluster/cluster_mode.h" /* cluster_node_id */ +#include "cluster/cluster_undo_gcs.h" +#include "cluster/cluster_undo_resid.h" + +/* + * cluster_undo_block_lookup_master -- owner-as-master routing entry. + * + * The master of an undo resource is the encoded owner_node (D1's + * cluster_undo_resid_master), never a shard hash: the undo authority lives + * at the owning instance. This is the single legal master-lookup entry for + * the undo class; cluster_grd_lookup_master / cluster_gcs_lookup_master fail + * closed on it (D1-5 guard, 53R9Q). + */ +int32 +cluster_undo_block_lookup_master(const ClusterResId *undo_resid) +{ + Assert(undo_resid != NULL); + Assert(cluster_undo_resid_is_undo(undo_resid)); + + return cluster_undo_resid_master(undo_resid); +} + +/* + * cluster_undo_block_master_is_self -- local fast-path routing gate. + * + * true iff this instance owns the undo resource (owner_node == + * cluster_node_id), so the owner can read/write its own undo without a + * network grant. The owner-incarnation epoch self-check that L364 requires + * before actually serving from the local fast path is applied on the + * grant/acquire path (D2-3), where the co-sampled live-authority triple is + * available; this predicate is the pure node-id half. + */ +bool +cluster_undo_block_master_is_self(const ClusterResId *undo_resid) +{ + Assert(undo_resid != NULL); + Assert(cluster_undo_resid_is_undo(undo_resid)); + + return cluster_undo_resid_master(undo_resid) == cluster_node_id; +} + +/* + * cluster_undo_path_uses_shared_root -- pure physical-root decision (D2-2). + * + * The single source of truth that both undo path builders consult: + * cluster_undo_path_resolve (runtime segment I/O) and the redo write + * surface in cluster_undo_xlog.c. Keeping the decision here (not + * duplicated in each builder) is what prevents own-instance undo + * split-brain -- runtime reading the shared copy while redo stamps the + * local copy (Hardening v1.0.1 裁决 A). + */ +bool +cluster_undo_path_uses_shared_root(ClusterUndoPathIntent intent, bool peer_mode, bool coherence_on) +{ + /* + * P1-3 hard contract: a dead-origin materialized copy (recovery rebuilt + * it in the local DataDir) NEVER migrates to the shared root, in any + * mode. Redirecting it would send dead-origin resolve / CR reads to an + * empty shared path (spec-5.22b §3.6, R1). + */ + if (intent == CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL) + return false; + + Assert(intent == CLUSTER_UNDO_PATH_RUNTIME_SHARED); + + /* + * Own live runtime undo migrates to the shared cluster_fs root only when + * the whole physical migration is armed: a declared multi-node + * deployment (peer_mode) AND cluster.undo_gcs_coherence on. Either off + * => local DataDir, so D2 lands inert at the default (裁决 A). + */ + return peer_mode && coherence_on; +} + +/* + * cluster_undo_grant_armed -- pure coherence gate (D2-3). + * + * See cluster_undo_gcs.h for the contract. Mirrors the physical-migration + * gate above (peer_mode && coherence_on): the grant/PI data plane and the + * physical migration arm together, so a peer never grants against undo bytes + * that were never migrated to the shared root. + */ +bool +cluster_undo_grant_armed(bool coherence_on, bool peer_mode) +{ + return coherence_on && peer_mode; +} + +/* + * cluster_undo_grant_admissible -- pure reader S-grant admissibility (D2-3). + * + * See cluster_undo_gcs.h for the two-dimension fail-closed contract. The two + * admit conditions are ANDed; any single failure returns false so the caller + * keeps the pre-existing 53R97 fail-closed boundary (Rule 8.A -- this only + * widens "admit when provable", never "admit when unprovable"). + */ +bool +cluster_undo_grant_admissible(const ClusterResId *undo_resid, uint32 expected_generation, + ClusterLiveAuthority auth, uint64 local_epoch, XLogRecPtr anchor_lsn) +{ + if (undo_resid == NULL) + return false; + + /* + * (1) Segment-generation anti-ABA (D1 §3.3 dim 1). A recycled segment + * reuses (undo_segment, block_no) for different content, so a generation + * mismatch means the reference is stale -- fail closed, never a match. + */ + if (!cluster_undo_resid_generation_matches(undo_resid, expected_generation)) + return false; + + /* + * (2) Owner-incarnation epoch + durable coverage (D-i2/D-i3). Reuse the + * pure live-authority window gate: authority sampled under a different + * membership epoch cannot be trusted, a reply with no live authority + * (invalid hwm) is never guessed, and the durable high-water must cover the + * version anchor (Invalid at the block level -> epoch + presence only). + */ + if (!cluster_vis_live_authority_covers_policy(anchor_lsn, auth, local_epoch)) + return false; + + return true; +} + +/* + * cluster_undo_grant_reader_pcm_mode / _transition -- reader lock contract + * (D2-3). The reader takes the undo block in PCM S mode via the read-first + * N->S transition (the legal read-first pair; cluster_pcm_lock owns and tests + * the transition-legality table). The writer/cleaner X path is D2-4. + */ +PcmState +cluster_undo_grant_reader_pcm_mode(void) +{ + return PCM_LOCK_MODE_S; +} + +PcmLockTransition +cluster_undo_grant_reader_pcm_transition(void) +{ + return PCM_TRANS_N_TO_S; +} diff --git a/src/backend/cluster/cluster_undo_gcs_grant.c b/src/backend/cluster/cluster_undo_gcs_grant.c new file mode 100644 index 0000000000..431e99ad7a --- /dev/null +++ b/src/backend/cluster/cluster_undo_gcs_grant.c @@ -0,0 +1,130 @@ +/*------------------------------------------------------------------------- + * + * cluster_undo_gcs_grant.c + * Shared-undo block GCS integration -- reader S-grant runtime primitive + * (spec-5.22b D2-3). + * + * This is the runtime wrapper that composes the pure decision core + * (cluster_undo_gcs.c: cluster_undo_grant_armed / _admissible) with the + * live wire: owner-as-master routing (D2-1), the reused 6.12i undo-TT + * fetch (owner ships its own image; the peer never opens the foreign undo + * file, invariant #8), and the fail-closed admission gate (Rule 8.A). + * + * It lives in its own object because it references the heavy backend + * surface (the GCS block fetch, the membership epoch, the coherence GUC) + * that the pure routing/decision object (cluster_undo_gcs.o) deliberately + * does NOT, so cluster_unit links that object standalone. This runtime + * wrapper is forward-covered by the D2-7 / D6 TAP legs (a peer really + * reading a foreign owner's undo block end-to-end), not by cluster_unit. + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * Portions Copyright (c) 2026, pgrac contributors + * + * Author: SqlRush + * + * IDENTIFICATION + * src/backend/cluster/cluster_undo_gcs_grant.c + * + * NOTES + * This is a pgrac-original file (no derivation from PostgreSQL). + * Spec: spec-5.22b-undo-block-gcs-integration.md (D2, §2.2/§3.1/§3.2) + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cluster/cluster_cr_server.h" /* cluster_gcs_block_undo_tt_fetch_and_wait */ +#include "cluster/cluster_epoch.h" /* cluster_epoch_get_current */ +#include "cluster/cluster_gcs_block.h" /* GCS_BLOCK_REPLY_GRANTED_FROM_HOLDER */ +#include "cluster/cluster_guc.h" /* cluster_undo_gcs_coherence */ +#include "cluster/cluster_mode.h" /* cluster_peer_mode_enabled, cluster_node_id */ +#include "cluster/cluster_undo_gcs.h" +#include "cluster/cluster_undo_resid.h" + +/* + * cluster_undo_block_acquire_shared -- reader S-grant primitive (D2-3). + * + * See cluster_undo_gcs.h for the full contract. A peer acquires a coherent + * S view of owner N's undo block through owner-as-master routing; any + * miss / DENIED / doubt returns false so the caller keeps its 53R97 + * fail-closed boundary (Rule 8.A -- MVCC/visibility never forward-links a + * false-visible edge). + */ +bool +cluster_undo_block_acquire_shared(const ClusterResId *undo_resid, uint32 expected_generation, + char *dst_block, ClusterUndoGrantResult *res) +{ + int32 owner; + uint32 seg; + uint32 block; + uint32 gen; + uint64 local_epoch; + ClusterLiveAuthority auth; + + if (undo_resid == NULL || dst_block == NULL || res == NULL) + return false; /* fail-closed */ + if (!cluster_undo_resid_is_undo(undo_resid)) + return false; + + memset(res, 0, sizeof(*res)); + + /* + * Coherence gate. At the default (coherence off, or a single-node / non + * peer-mode deployment) the path is not armed: the caller keeps its + * unchanged authority-less fetch path, so D2 lands inert (回归安全). + */ + if (!cluster_undo_grant_armed(cluster_undo_gcs_coherence, cluster_peer_mode_enabled())) + return false; + + cluster_undo_resid_decode(undo_resid, &owner, &seg, &block, &gen); + local_epoch = cluster_epoch_get_current(); + + if (cluster_undo_block_master_is_self(undo_resid)) { + /* + * master==self: the owner reading its OWN undo needs no network grant + * (local PCM suffices, §3.1). The owner-incarnation epoch self-check + * (L364) that must gate serving from this local fast path -- proving + * THIS node is still the legitimate owner incarnation at local_epoch, + * not a fenced zombie -- has no standalone primitive yet, and this API + * has no D2-3 consumer (the owner reads its own undo through the + * existing local undo paths). Rather than serve a local read that + * cannot yet be proven safe (Rule 8.A), the self fast path is + * fail-closed here and lands with its incarnation check + consumer in + * D6. The peer-reads-foreign S-grant below is D2-3's delivered heart. + */ + return false; /* honest fail-closed forward (D6); never a stale local serve */ + } + + /* + * master!=self (live owner): the owner is the authority AND the holder. + * Reuse the 6.12i owner-ships-image wire verbatim (zero new wire; Q3★A + * maps a success to GCS_BLOCK_REPLY_GRANTED_FROM_HOLDER at this layer). + * The owner reads its own undo and ships the current image + the authority + * co-sampled with it; this peer consumes the shipped image and NEVER opens + * the foreign undo file (invariant #8). A DENIED_RESOURCE_RECOVERING + * (owner dead / undo shard remastering) or any timeout / checksum / + * missing-trailer collapses the fetch to false -> fail-closed 53R97 + * (dead-owner serve is D4, not D2). + */ + if (!cluster_gcs_block_undo_tt_fetch_and_wait(owner, seg, block, dst_block, &auth)) + return false; + + /* + * Admit only a coherent view: segment-generation anti-ABA AND the owner's + * incarnation epoch equals ours AND the shipped durable high-water is + * present (Rule 8.A -- never admit on doubt). anchor_lsn is Invalid at + * the block level; the version-coverage (hwm >= page_lsn) gate is applied + * by the D3/D6 verdict consumer that owns the tuple's anchor. + */ + if (!cluster_undo_grant_admissible(undo_resid, expected_generation, auth, local_epoch, + InvalidXLogRecPtr)) + return false; + + res->origin_epoch = auth.origin_epoch; + res->live_hwm_lsn = auth.live_hwm_lsn; + res->tt_generation = auth.tt_generation; + res->status = (uint8)GCS_BLOCK_REPLY_GRANTED_FROM_HOLDER; + return true; +} diff --git a/src/backend/cluster/cluster_undo_record.c b/src/backend/cluster/cluster_undo_record.c index 5029caf77d..623d8cd70b 100644 --- a/src/backend/cluster/cluster_undo_record.c +++ b/src/backend/cluster/cluster_undo_record.c @@ -760,7 +760,8 @@ cluster_undo_try_mark_record_segment_committed(uint32 seg, uint8 owner_instance, if (UndoRecordShared == NULL || seg == 0) return; - if (!cluster_undo_smgr_read_block(seg, owner_instance, 0, blockbuf.data)) + if (!cluster_undo_smgr_read_block(cluster_undo_intent_for_owner(owner_instance), seg, + owner_instance, 0, blockbuf.data)) return; /* read fail -> retain (best-effort) */ if (!cluster_undo_segment_header_identity_ok(blockbuf.data, seg, owner_instance)) return; /* L212: identity, not template bytes -> retain */ @@ -801,7 +802,8 @@ cluster_undo_try_mark_record_segment_committed(uint32 seg, uint8 owner_instance, } if (dirty) - (void)cluster_undo_smgr_write_block(seg, owner_instance, 0, blockbuf.data, true); + (void)cluster_undo_smgr_write_block(cluster_undo_intent_for_owner(owner_instance), seg, + owner_instance, 0, blockbuf.data, true); } uint64 @@ -864,7 +866,8 @@ read_undo_block(uint32 segment_id, uint8 owner_instance, uint32 block_no, char * */ img = cluster_undo_buf_pin(segment_id, owner_instance, block_no, CLUSTER_UNDO_BUF_SHARED, &pin); if (img == NULL) - return cluster_undo_smgr_read_block(segment_id, owner_instance, block_no, buf); + return cluster_undo_smgr_read_block(cluster_undo_intent_for_owner(owner_instance), + segment_id, owner_instance, block_no, buf); memcpy(buf, img, BLCKSZ); cluster_undo_buf_unpin(&pin); return true; @@ -893,7 +896,8 @@ write_undo_block_ext(uint32 segment_id, uint8 owner_instance, uint32 block_no, c * do_fsync request goes straight to the direct smgr write. */ if (do_fsync) - return cluster_undo_smgr_write_block(segment_id, owner_instance, block_no, buf, true); + return cluster_undo_smgr_write_block(cluster_undo_intent_for_owner(owner_instance), + segment_id, owner_instance, block_no, buf, true); /* * Write-through the pool for DATA blocks: update the cached image and @@ -911,7 +915,8 @@ write_undo_block_ext(uint32 segment_id, uint8 owner_instance, uint32 block_no, c */ if (keep_clean) return false; - return cluster_undo_smgr_write_block(segment_id, owner_instance, block_no, buf, false); + return cluster_undo_smgr_write_block(cluster_undo_intent_for_owner(owner_instance), + segment_id, owner_instance, block_no, buf, false); } /* diff --git a/src/backend/cluster/cluster_undo_resid.c b/src/backend/cluster/cluster_undo_resid.c new file mode 100644 index 0000000000..ddb0af925c --- /dev/null +++ b/src/backend/cluster/cluster_undo_resid.c @@ -0,0 +1,134 @@ +/*------------------------------------------------------------------------- + * + * cluster_undo_resid.c + * Shared-undo block resource identity + owner-as-master routing -- + * pure layer (spec-5.22a D1). + * + * This file ships the backend-pure layer: the undo resid + * encoder/decoder, the class discriminator, the owner-as-master + * routing function, and the anti-ABA generation predicate. None of + * these touch elog / shmem / locks, so the cluster_unit test links the + * object standalone. The data plane that consumes this identity + * (grant / PI / block serving / recovery materialization / retention) + * lands with later deliverables. + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * Portions Copyright (c) 2026, pgrac contributors + * + * Author: SqlRush + * + * IDENTIFICATION + * src/backend/cluster/cluster_undo_resid.c + * + * NOTES + * This is a pgrac-original file (no derivation from PostgreSQL). + * Spec: spec-5.22a-undo-block-resource-identity.md (D1, §2.2 / §3.1) + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cluster/cluster_scn.h" /* SCN_NODE_ID_VALID */ +#include "cluster/cluster_undo_resid.h" + +/* + * cluster_undo_resid_encode -- build the undo-block resource id. + * + * field3 is the segment reuse generation (the segment header wrap_count), + * so a recycled segment lands a distinct resource (ABA defence), + * mirroring the HW relfilenode field. field4 is the owning instance: + * the undo authority lives at the owner, so the owner is part of the + * identity, not a routing afterthought. + */ +void +cluster_undo_resid_encode(int32 owner_node, uint32 undo_segment, uint32 block_no, uint32 generation, + ClusterResId *dst) +{ + Assert(dst != NULL); + if (dst == NULL) + return; + Assert(SCN_NODE_ID_VALID(owner_node)); + + dst->field1 = undo_segment; + dst->field2 = block_no; + dst->field3 = generation; + dst->field4 = (uint16)owner_node; + dst->type = CLUSTER_UNDO_RESID_TYPE; + dst->lockmethodid = DEFAULT_LOCKMETHOD; +} + +/* + * cluster_undo_resid_decode -- split an undo resid back into its fields. + * + * The caller must pass an undo-class resid (Assert enforced); the + * decoder is the wire-ABI boundary, so it never guesses at foreign + * classes. + */ +void +cluster_undo_resid_decode(const ClusterResId *rid, int32 *owner_node, uint32 *undo_segment, + uint32 *block_no, uint32 *generation) +{ + Assert(rid != NULL); + if (rid == NULL || owner_node == NULL || undo_segment == NULL || block_no == NULL + || generation == NULL) + return; + Assert(rid->type == CLUSTER_UNDO_RESID_TYPE); + + *owner_node = (int32)rid->field4; + *undo_segment = rid->field1; + *block_no = rid->field2; + *generation = rid->field3; +} + +/* + * cluster_undo_resid_is_undo -- class discriminator. + */ +bool +cluster_undo_resid_is_undo(const ClusterResId *rid) +{ + Assert(rid != NULL); + if (rid == NULL) + return false; + + return rid->type == CLUSTER_UNDO_RESID_TYPE; +} + +/* + * cluster_undo_resid_master -- owner-as-master routing. + * + * Returns the encoded owner_node directly: the undo authority lives at + * the owning instance, so the master is part of the identity and is + * NEVER derived from a shard hash. A hash-derived master would place + * the authority at a node that does not own the undo, which is exactly + * the misrouting the GRD-side guard fails closed on. + */ +int32 +cluster_undo_resid_master(const ClusterResId *rid) +{ + Assert(rid != NULL); + if (rid == NULL) + return -1; + Assert(rid->type == CLUSTER_UNDO_RESID_TYPE); + + return (int32)rid->field4; +} + +/* + * cluster_undo_resid_generation_matches -- anti-ABA check. + * + * false means the reference predates a whole-segment recycle (the + * segment header wrap_count moved on) and the caller MUST fail closed; + * it must never be treated as a match. + */ +bool +cluster_undo_resid_generation_matches(const ClusterResId *rid, uint32 expected_generation) +{ + Assert(rid != NULL); + if (rid == NULL) + return false; + Assert(rid->type == CLUSTER_UNDO_RESID_TYPE); + + return rid->field3 == expected_generation; +} diff --git a/src/backend/cluster/storage/cluster_shared_fs.c b/src/backend/cluster/storage/cluster_shared_fs.c index 354d047aca..951459e4dd 100644 --- a/src/backend/cluster/storage/cluster_shared_fs.c +++ b/src/backend/cluster/storage/cluster_shared_fs.c @@ -521,4 +521,61 @@ cluster_shared_fs_writeback(ClusterSharedFsHandle *handle, BlockNumber blocknum, cluster_shared_fs_active_ops->writeback(handle, blocknum, nblocks); } + +/* + * cluster_shared_fs_undo_path_resolve -- resolve an undo segment's path on + * the shared cluster_fs root (spec-5.22b D2-2 undo namespace). + * + * Undo is not a RelFileLocator, so it is addressed by resolving the SAME + * absolute path under cluster.shared_data_dir on every node -- exactly the + * pattern the shared-root sentinel, recovery anchor, and CF authority use. + * Owner-partitioned: the directory component is owner_instance-1 (= the + * owning node_id), a straight lift of the local $PGDATA/pg_undo layout so a + * segment's shared and local paths differ only in their root. + * + * Fail-closed (return -1) when the shared root is unset: this resolver is + * only reached on the RUNTIME_SHARED + coherence-on branch, so an unset root + * is a deployment error and must never silently fall back to a bogus path + * (mirrors build_anchor_path; the caller propagates -1 to a fail-closed + * read/write, and the GUC check-hook rejects coherence-on without a root). + */ +int +cluster_shared_fs_undo_path_resolve(uint8 owner_instance, uint32 segment_id, char *buf, + size_t buf_size) +{ + int ret; + + if (buf == NULL || buf_size == 0) + return -1; + Assert(owner_instance >= 1); + + if (cluster_shared_data_dir == NULL || cluster_shared_data_dir[0] == '\0') + return -1; + + ret = snprintf(buf, buf_size, "%s/pg_undo/instance_%u/seg_%u.dat", cluster_shared_data_dir, + (unsigned)(owner_instance - 1), (unsigned)segment_id); + if (ret < 0 || (size_t)ret >= buf_size) + return -1; + return 0; +} + +int +cluster_shared_fs_undo_instance_dir_resolve(uint8 owner_instance, char *buf, size_t buf_size) +{ + int ret; + + if (buf == NULL || buf_size == 0) + return -1; + Assert(owner_instance >= 1); + + if (cluster_shared_data_dir == NULL || cluster_shared_data_dir[0] == '\0') + return -1; + + ret = snprintf(buf, buf_size, "%s/pg_undo/instance_%u", cluster_shared_data_dir, + (unsigned)(owner_instance - 1)); + if (ret < 0 || (size_t)ret >= buf_size) + return -1; + return 0; +} + #endif /* USE_PGRAC_CLUSTER */ diff --git a/src/backend/cluster/storage/cluster_undo_alloc.c b/src/backend/cluster/storage/cluster_undo_alloc.c index 83c8694111..335a51e021 100644 --- a/src/backend/cluster/storage/cluster_undo_alloc.c +++ b/src/backend/cluster/storage/cluster_undo_alloc.c @@ -38,7 +38,11 @@ #include #include "access/xlog.h" -#include "cluster/cluster_scn.h" /* SCN_MAX_VALID_NODE_ID */ +#include "cluster/cluster_guc.h" /* cluster_undo_gcs_coherence */ +#include "cluster/cluster_mode.h" /* cluster_peer_mode_enabled */ +#include "common/file_perm.h" /* pg_mkdir_p / pg_dir_create_mode (shared subdir, D2-2) */ +#include "cluster/cluster_scn.h" /* SCN_MAX_VALID_NODE_ID */ +#include "cluster/cluster_undo_gcs.h" /* path intent + shared-root decision */ #include "cluster/cluster_undo_segment.h" #include "cluster/cluster_undo_segment_init.h" #include "cluster/cluster_undo_record_api.h" /* reuse counter note (3.13) */ @@ -48,7 +52,8 @@ #include "cluster/cluster_undo_segment_init.h" /* fresh header builder (D4 reuse) */ #include "access/xlog.h" /* XLogFlush (3.13 v0.3 (1)) */ #include "cluster/storage/cluster_undo_alloc.h" -#include "cluster/storage/cluster_undo_buf.h" /* invalidate_segment on reuse (3.18 D3.2) */ +#include "cluster/storage/cluster_shared_fs.h" /* undo namespace on shared root (D2-2) */ +#include "cluster/storage/cluster_undo_buf.h" /* invalidate_segment on reuse (3.18 D3.2) */ #include "cluster/storage/cluster_undo_xlog.h" #include "miscadmin.h" #include "storage/bufpage.h" @@ -79,7 +84,8 @@ extern int cluster_node_id; * the assert catches sentinel-0 misuse. */ int -cluster_undo_path_resolve(uint8 owner_instance, uint32 segment_id, char *buf, size_t buf_size) +cluster_undo_path_resolve(ClusterUndoPathIntent intent, uint8 owner_instance, uint32 segment_id, + char *buf, size_t buf_size) { int ret; @@ -87,7 +93,29 @@ cluster_undo_path_resolve(uint8 owner_instance, uint32 segment_id, char *buf, si return -1; Assert(owner_instance >= 1 && owner_instance <= UNDO_OWNER_INSTANCE_MAX); - /* directory uses cluster_node_id (= owner_instance - 1) */ + /* + * spec-5.22b D2-2 (threading strategy B): every caller derives intent via + * cluster_undo_intent_for_owner(owner), so in D2 RUNTIME_SHARED ⟺ own + * instance and MATERIALIZED_LOCAL ⟺ a foreign dead-origin owner. Assert + * that invariant here: a future spec (D4) that serves a foreign owner's + * live undo from shared storage passes RUNTIME_SHARED with owner!=self and + * MUST revisit this path -- the assert makes that divergence loud rather + * than a silent split. + */ + Assert((intent == CLUSTER_UNDO_PATH_RUNTIME_SHARED) + == (owner_instance == (uint8)(cluster_node_id + 1))); + + /* + * RUNTIME_SHARED own undo migrates to the shared cluster_fs root only + * under peer-mode + cluster.undo_gcs_coherence; MATERIALIZED_LOCAL and + * every non-coherent mode stay on the local DataDir (P1-3 / 裁决 A). An + * unset shared root on the shared branch fails closed (resolver -1). + */ + if (cluster_undo_path_uses_shared_root(intent, cluster_peer_mode_enabled(), + cluster_undo_gcs_coherence)) + return cluster_shared_fs_undo_path_resolve(owner_instance, segment_id, buf, buf_size); + + /* local DataDir: directory uses cluster_node_id (= owner_instance - 1) */ ret = snprintf(buf, buf_size, "%s/pg_undo/instance_%u/seg_%u.dat", DataDir, (unsigned)(owner_instance - 1), (unsigned)segment_id); if (ret < 0 || (size_t)ret >= buf_size) @@ -146,7 +174,29 @@ ensure_instance_subdir(uint8 owner_instance) Assert(owner_instance >= 1 && owner_instance <= UNDO_OWNER_INSTANCE_MAX); - /* directory uses cluster_node_id (= owner_instance - 1); see + /* + * spec-5.22b D2-2: own-instance allocation under coherence lands the + * segment on the shared cluster_fs root, whose pg_undo/instance_ tree + * is not seeded by initdb -- create the whole path (idempotent). Foreign + * owners never reach the allocator (single-writer), so only the shared + * branch differs from the pre-D2 local mkdir. + */ + if (cluster_undo_path_uses_shared_root(cluster_undo_intent_for_owner(owner_instance), + cluster_peer_mode_enabled(), + cluster_undo_gcs_coherence)) { + if (cluster_shared_fs_undo_instance_dir_resolve(owner_instance, path, sizeof(path)) != 0) + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("undo shared instance subdir unresolved: owner_instance=%u " + "(cluster.shared_data_dir unset)", + (unsigned)owner_instance))); + if (pg_mkdir_p(path, pg_dir_create_mode) != 0 && errno != EEXIST) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not create undo shared instance subdir \"%s\": %m", path))); + return; + } + + /* local DataDir: directory uses cluster_node_id (= owner_instance - 1); see * cluster_undo_path_resolve docstring. */ ret = snprintf(path, sizeof(path), "%s/pg_undo/instance_%u", DataDir, (unsigned)(owner_instance - 1)); @@ -270,7 +320,9 @@ cluster_undo_segment_allocate(uint32 segment_id, uint8 owner_instance) (unsigned)owner_instance, cluster_node_id, cluster_node_id + 1))); } - if (cluster_undo_path_resolve(owner_instance, segment_id, path, sizeof(path)) != 0) + if (cluster_undo_path_resolve(cluster_undo_intent_for_owner(owner_instance), owner_instance, + segment_id, path, sizeof(path)) + != 0) ereport(ERROR, (errcode(ERRCODE_NAME_TOO_LONG), errmsg("undo segment path too long: instance=%u seg=%u", (unsigned)owner_instance, (unsigned)segment_id))); @@ -477,7 +529,8 @@ static bool read_segment_header_via_smgr(uint32 segment_id, uint8 owner_instance, char *blockbuf, UndoSegmentHeaderData **out_hdr) { - if (!cluster_undo_smgr_read_block(segment_id, owner_instance, 0, blockbuf)) + if (!cluster_undo_smgr_read_block(cluster_undo_intent_for_owner(owner_instance), segment_id, + owner_instance, 0, blockbuf)) return false; *out_hdr = (UndoSegmentHeaderData *)blockbuf; return true; @@ -487,7 +540,8 @@ read_segment_header_via_smgr(uint32 segment_id, uint8 owner_instance, char *bloc static bool write_segment_header_via_smgr(uint32 segment_id, uint8 owner_instance, const char *blockbuf) { - return cluster_undo_smgr_write_block(segment_id, owner_instance, 0, blockbuf, true); + return cluster_undo_smgr_write_block(cluster_undo_intent_for_owner(owner_instance), segment_id, + owner_instance, 0, blockbuf, true); } @@ -870,7 +924,13 @@ cluster_undo_segment_extend_or_create(uint8 owner_instance, bool *out_at_hard_ca * within that lock so current_pool_size is non-stale. */ { - extern int cluster_undo_segments_max_per_instance; + /* + * cluster_undo_segments_max_per_instance is declared in + * cluster/cluster_guc.h (included above), so no local extern is + * needed here. PGRAC: D2-2 added that include for + * cluster_undo_gcs_coherence; dropping the redundant local extern + * avoids a shadowed-declaration static-analysis finding. + */ int guc_val = cluster_undo_segments_max_per_instance; uint32 current_pool_size = 0; uint32 probe_slot; @@ -889,7 +949,8 @@ cluster_undo_segment_extend_or_create(uint8 owner_instance, bool *out_at_hard_ca = (uint32)(owner_instance - 1) * CLUSTER_UNDO_SEGS_PER_INSTANCE + probe_slot + 1; path_ret - = cluster_undo_path_resolve(owner_instance, probe_segment_id, path, sizeof(path)); + = cluster_undo_path_resolve(cluster_undo_intent_for_owner(owner_instance), + owner_instance, probe_segment_id, path, sizeof(path)); if (path_ret != 0) break; @@ -927,7 +988,8 @@ cluster_undo_segment_extend_or_create(uint8 owner_instance, bool *out_at_hard_ca int fd; uint32 new_segment_id = base_segment_id + slot; - ret = cluster_undo_path_resolve(owner_instance, new_segment_id, path, sizeof(path)); + ret = cluster_undo_path_resolve(cluster_undo_intent_for_owner(owner_instance), + owner_instance, new_segment_id, path, sizeof(path)); if (ret != 0) continue; @@ -1015,7 +1077,8 @@ cluster_undo_segment_scan_max_existing(uint8 owner_instance) int fd; uint32 probe_id = base_segment_id + slot; - ret = cluster_undo_path_resolve(owner_instance, probe_id, path, sizeof(path)); + ret = cluster_undo_path_resolve(cluster_undo_intent_for_owner(owner_instance), + owner_instance, probe_id, path, sizeof(path)); if (ret != 0) break; @@ -1047,7 +1110,9 @@ cluster_undo_segment_file_exists(uint8 owner_instance, uint32 segment_id) if (owner_instance < 1 || owner_instance > UNDO_OWNER_INSTANCE_MAX) return false; - if (cluster_undo_path_resolve(owner_instance, segment_id, path, sizeof(path)) != 0) + if (cluster_undo_path_resolve(cluster_undo_intent_for_owner(owner_instance), owner_instance, + segment_id, path, sizeof(path)) + != 0) return false; return access(path, F_OK) == 0; } diff --git a/src/backend/cluster/storage/cluster_undo_buf.c b/src/backend/cluster/storage/cluster_undo_buf.c index df8f7a927f..bc977cc76d 100644 --- a/src/backend/cluster/storage/cluster_undo_buf.c +++ b/src/backend/cluster/storage/cluster_undo_buf.c @@ -488,7 +488,8 @@ flush_dirty_slot(int slotno) * XLogFlush above is attributed to its own WAL wait event. */ pgstat_report_wait_start(WAIT_EVENT_CLUSTER_UNDO_BUF_FLUSH); - if (!cluster_undo_smgr_write_block(seg, owner, blk, localbuf.data, /* do_fsync = */ true)) { + if (!cluster_undo_smgr_write_block(cluster_undo_intent_for_owner(owner), seg, owner, blk, + localbuf.data, /* do_fsync = */ true)) { pgstat_report_wait_end(); ereport(ERROR, (errcode(ERRCODE_CLUSTER_UNDO_RECORD_INVALID_UBA), errmsg("cluster undo buffer write-back flush failed " @@ -681,7 +682,8 @@ cluster_undo_buf_pin(uint32 segment_id, uint8 owner, uint32 block_no, ClusterUnd LWLockRelease(&UndoBufPool->map_lock); /* Disk read OUTSIDE the map_lock. */ - if (!cluster_undo_smgr_read_block(segment_id, owner, block_no, SLOT_DATA(slotno))) { + if (!cluster_undo_smgr_read_block(cluster_undo_intent_for_owner(owner), segment_id, + owner, block_no, SLOT_DATA(slotno))) { /* Fill failed — release reservation + report. */ LWLockRelease(&s->content_lock); LWLockAcquire(&UndoBufPool->map_lock, LW_EXCLUSIVE); @@ -732,8 +734,9 @@ cluster_undo_buf_mark_dirty(const ClusterUndoBufPin *pin, XLogRecPtr wal_lsn) * The block is NOT left buffered-dirty, so durability is identical to * today's per-block direct write. */ - if (!cluster_undo_smgr_write_block(s->segment_id, s->owner, s->block_no, - SLOT_DATA(pin->slot), /* do_fsync = */ false)) + if (!cluster_undo_smgr_write_block(cluster_undo_intent_for_owner(s->owner), s->segment_id, + s->owner, s->block_no, SLOT_DATA(pin->slot), + /* do_fsync = */ false)) ereport(ERROR, (errcode(ERRCODE_CLUSTER_UNDO_RECORD_INVALID_UBA), errmsg("cluster undo buffer write-through failed seg=%u owner=%u block=%u", diff --git a/src/backend/cluster/storage/cluster_undo_smgr.c b/src/backend/cluster/storage/cluster_undo_smgr.c index 676c976ce0..6dd82b008f 100644 --- a/src/backend/cluster/storage/cluster_undo_smgr.c +++ b/src/backend/cluster/storage/cluster_undo_smgr.c @@ -41,6 +41,7 @@ #include "storage/ipc.h" /* before_shmem_exit (fd cache cleanup) */ #include "utils/elog.h" +#include "cluster/cluster_undo_gcs.h" /* cluster_undo_intent_for_owner (D2-2) */ #include "cluster/cluster_undo_record_api.h" /* smgr syscall counter bumps */ #include "cluster/cluster_undo_smgr.h" #include "cluster/cluster_undo_segment.h" @@ -61,6 +62,13 @@ */ static uint32 cached_fd_segment = 0; /* 0 = empty */ static uint8 cached_fd_owner = 0; +/* + * spec-5.22b D2-2: the intent is part of the cache key. A given (segment, + * owner) resolves to a DIFFERENT physical path for RUNTIME_SHARED (shared + * root) vs MATERIALIZED_LOCAL (local DataDir), so a cached fd opened under + * one intent must never be reused for the other. + */ +static ClusterUndoPathIntent cached_fd_intent = CLUSTER_UNDO_PATH_RUNTIME_SHARED; static int cached_fd = -1; static bool cached_fd_exit_registered = false; @@ -90,17 +98,18 @@ fd_cache_on_exit(int code, Datum arg) * NOT close the returned fd (the cache owns it). */ static int -get_segment_fd(uint32 segment_id, uint8 owner_instance) +get_segment_fd(ClusterUndoPathIntent intent, uint32 segment_id, uint8 owner_instance) { char path[MAXPGPATH]; int fd; - if (cached_fd >= 0 && cached_fd_segment == segment_id && cached_fd_owner == owner_instance) + if (cached_fd >= 0 && cached_fd_segment == segment_id && cached_fd_owner == owner_instance + && cached_fd_intent == intent) return cached_fd; /* hit */ fd_cache_close(); /* miss: drop the stale fd first */ - if (cluster_undo_path_resolve(owner_instance, segment_id, path, sizeof(path)) != 0) + if (cluster_undo_path_resolve(intent, owner_instance, segment_id, path, sizeof(path)) != 0) return -1; fd = BasicOpenFile(path, O_RDWR | PG_BINARY); if (fd < 0) @@ -110,6 +119,7 @@ get_segment_fd(uint32 segment_id, uint8 owner_instance) cached_fd = fd; cached_fd_segment = segment_id; cached_fd_owner = owner_instance; + cached_fd_intent = intent; if (!cached_fd_exit_registered) { before_shmem_exit(fd_cache_on_exit, (Datum)0); cached_fd_exit_registered = true; @@ -129,7 +139,8 @@ cluster_undo_smgr_fd_cache_reset(void) bool -cluster_undo_smgr_read_block(uint32 segment_id, uint8 owner_instance, uint32 block_no, char *buf) +cluster_undo_smgr_read_block(ClusterUndoPathIntent intent, uint32 segment_id, uint8 owner_instance, + uint32 block_no, char *buf) { int fd; off_t offset; @@ -139,7 +150,7 @@ cluster_undo_smgr_read_block(uint32 segment_id, uint8 owner_instance, uint32 blo if (buf == NULL || block_no >= UNDO_BLOCKS_PER_SEGMENT) return false; - fd = get_segment_fd(segment_id, owner_instance); + fd = get_segment_fd(intent, segment_id, owner_instance); if (fd < 0) return false; @@ -152,8 +163,8 @@ cluster_undo_smgr_read_block(uint32 segment_id, uint8 owner_instance, uint32 blo bool -cluster_undo_smgr_write_block(uint32 segment_id, uint8 owner_instance, uint32 block_no, - const char *buf, bool do_fsync) +cluster_undo_smgr_write_block(ClusterUndoPathIntent intent, uint32 segment_id, uint8 owner_instance, + uint32 block_no, const char *buf, bool do_fsync) { int fd; off_t offset; @@ -163,7 +174,7 @@ cluster_undo_smgr_write_block(uint32 segment_id, uint8 owner_instance, uint32 bl if (buf == NULL || block_no >= UNDO_BLOCKS_PER_SEGMENT) return false; - fd = get_segment_fd(segment_id, owner_instance); + fd = get_segment_fd(intent, segment_id, owner_instance); if (fd < 0) return false; @@ -195,8 +206,8 @@ cluster_undo_smgr_write_block(uint32 segment_id, uint8 owner_instance, uint32 bl * must stay inside block 0 (BLCKSZ). */ bool -cluster_undo_smgr_read_header_bytes(uint32 segment_id, uint8 owner_instance, uint32 offset, - char *buf, uint32 len) +cluster_undo_smgr_read_header_bytes(ClusterUndoPathIntent intent, uint32 segment_id, + uint8 owner_instance, uint32 offset, char *buf, uint32 len) { int fd; ssize_t nread; @@ -204,7 +215,7 @@ cluster_undo_smgr_read_header_bytes(uint32 segment_id, uint8 owner_instance, uin if (buf == NULL || len == 0 || (uint64)offset + (uint64)len > (uint64)BLCKSZ) return false; - fd = get_segment_fd(segment_id, owner_instance); + fd = get_segment_fd(intent, segment_id, owner_instance); if (fd < 0) return false; @@ -214,8 +225,9 @@ cluster_undo_smgr_read_header_bytes(uint32 segment_id, uint8 owner_instance, uin } bool -cluster_undo_smgr_write_header_bytes(uint32 segment_id, uint8 owner_instance, uint32 offset, - const char *buf, uint32 len) +cluster_undo_smgr_write_header_bytes(ClusterUndoPathIntent intent, uint32 segment_id, + uint8 owner_instance, uint32 offset, const char *buf, + uint32 len) { int fd; ssize_t nwritten; @@ -223,7 +235,7 @@ cluster_undo_smgr_write_header_bytes(uint32 segment_id, uint8 owner_instance, ui if (buf == NULL || len == 0 || (uint64)offset + (uint64)len > (uint64)BLCKSZ) return false; - fd = get_segment_fd(segment_id, owner_instance); + fd = get_segment_fd(intent, segment_id, owner_instance); if (fd < 0) return false; @@ -253,7 +265,8 @@ cluster_undo_smgr_create_segment_file(uint32 segment_id, uint8 owner_instance) int ret; int fd; - ret = cluster_undo_path_resolve(owner_instance, segment_id, path, sizeof(path)); + ret = cluster_undo_path_resolve(cluster_undo_intent_for_owner(owner_instance), + owner_instance, segment_id, path, sizeof(path)); if (ret != 0) return -2; @@ -272,7 +285,7 @@ cluster_undo_smgr_fsync_segment_file(uint32 segment_id, uint8 owner_instance) { int fd; - fd = get_segment_fd(segment_id, owner_instance); + fd = get_segment_fd(cluster_undo_intent_for_owner(owner_instance), segment_id, owner_instance); if (fd < 0) return false; diff --git a/src/backend/cluster/storage/cluster_undo_xlog.c b/src/backend/cluster/storage/cluster_undo_xlog.c index 6ceed05c7f..a18f0297d5 100644 --- a/src/backend/cluster/storage/cluster_undo_xlog.c +++ b/src/backend/cluster/storage/cluster_undo_xlog.c @@ -43,13 +43,18 @@ #include "access/xloginsert.h" #include "access/xlogreader.h" #include "access/xlogutils.h" +#include "cluster/cluster_guc.h" /* cluster_undo_gcs_coherence (D2-2) */ #include "cluster/cluster_hw.h" /* spec-5.7 D1 HW authority apply/encode */ +#include "cluster/cluster_mode.h" /* cluster_peer_mode_enabled (D2-2) */ #include "cluster/cluster_tt_status.h" /* spec-3.16 D5 recovery counters */ #include "cluster/cluster_tt_durable.h" /* spec-3.11: redo decision predicate */ +#include "cluster/cluster_undo_gcs.h" /* shared-root decision (D2-2 裁决 A) */ #include "cluster/cluster_undo_segment.h" /* UNDO_SEGMENT_SIZE_BYTES */ -#include "cluster/storage/cluster_undo_alloc.h" /* header identity check (3.13 reuse redo) */ +#include "cluster/storage/cluster_shared_fs.h" /* undo shared-root dir resolve (D2-2) */ +#include "cluster/storage/cluster_undo_alloc.h" /* path resolve + intent (3.13 reuse redo) */ #include "cluster/storage/cluster_undo_buf.h" /* spec-3.18 D2b write-back gate */ #include "cluster/storage/cluster_undo_xlog.h" +#include "common/file_perm.h" /* pg_mkdir_p / pg_dir_create_mode (shared subdir, D2-2) */ #include "miscadmin.h" #include "storage/bufpage.h" #include "storage/fd.h" @@ -57,35 +62,27 @@ /* - * Build a path: $PGDATA/pg_undo/instance_/seg_.dat + * Build the undo segment path for the REDO write surface. * - * Both encoder (cluster_undo_emit_segment_init) and decoder - * (cluster_undo_redo) use this layout. pg_undo subdir is - * established by initdb (D4). instance_ subdir is created - * on demand by cluster_undo_redo_segment_init (Stage 1.22 ships - * only instance_0 from initdb; redo path may need other instance - * subdirs on standbys / cross-instance crash recovery). + * spec-5.22b D2-2 (Hardening v1.0.1 裁决 A): the redo write surface must + * resolve to the SAME physical root as the runtime write surface, or an + * own-instance segment would be read from the shared root (runtime) while + * redo re-stamps the local copy -- own-instance undo split-brain (block-0 + * TT is WAL-redo-only, so a lost redo write on the shared copy is a + * false-invisible 8.A hazard). So this delegates to the ONE decision + * source, cluster_undo_path_resolve: own redo (rec->instance == self) under + * coherence resolves to the shared cluster_fs root; a foreign rec->instance + * (dead-origin materialization rebuilt locally) resolves to the local + * DataDir. Callers pass cluster_undo_intent_for_owner(rec->instance). * - * Hardening v1.0.4 P1-1: directory naming uses cluster_node_id - * (= owner_instance - 1) so that single-node default - * (cluster_node_id = 0, owner_instance = 1) lands at instance_0/ - * matching the initdb seed. See cluster_undo_alloc.c - * cluster_undo_path_resolve docstring for full rationale. - * - * Returns 0 on success, -1 on path-too-long. Caller supplies - * buf with capacity >= MAXPGPATH. + * Returns 0 on success, -1 on path-too-long / unset shared root (fail- + * closed). Caller supplies buf with capacity >= MAXPGPATH. */ static int -build_undo_segment_path(uint8 owner_instance, uint32 segment_id, char *buf, size_t buf_size) +build_undo_segment_path(ClusterUndoPathIntent intent, uint8 owner_instance, uint32 segment_id, + char *buf, size_t buf_size) { - int ret; - - Assert(owner_instance >= 1 && owner_instance <= UNDO_OWNER_INSTANCE_MAX); - ret = snprintf(buf, buf_size, "%s/pg_undo/instance_%u/seg_%u.dat", DataDir, - (unsigned)(owner_instance - 1), (unsigned)segment_id); - if (ret < 0 || (size_t)ret >= buf_size) - return -1; - return 0; + return cluster_undo_path_resolve(intent, owner_instance, segment_id, buf, buf_size); } @@ -108,7 +105,28 @@ ensure_undo_instance_subdir(uint8 owner_instance) Assert(owner_instance >= 1 && owner_instance <= UNDO_OWNER_INSTANCE_MAX); - /* directory uses cluster_node_id (= owner_instance - 1) per Hardening v1.0.4 P1-1 */ + /* + * spec-5.22b D2-2: own-instance redo under coherence materializes the + * segment on the shared cluster_fs root, whose pg_undo/instance_ tree + * is not seeded by initdb -- create the whole path (idempotent). A + * foreign owner (dead-origin materialization) stays on the local DataDir + * (裁决 A), so only own redo under coherence takes the shared branch. + */ + if (cluster_undo_path_uses_shared_root(cluster_undo_intent_for_owner(owner_instance), + cluster_peer_mode_enabled(), + cluster_undo_gcs_coherence)) { + if (cluster_shared_fs_undo_instance_dir_resolve(owner_instance, path, sizeof(path)) != 0) + ereport(PANIC, (errmsg("undo shared instance subdir unresolved: owner_instance=%u " + "(cluster.shared_data_dir unset)", + (unsigned)owner_instance))); + if (pg_mkdir_p(path, pg_dir_create_mode) != 0 && errno != EEXIST) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not create undo shared instance subdir \"%s\": %m", path))); + return; + } + + /* local DataDir: pg_undo seeded by initdb; single-level mkdir (P1-1 naming). */ ret = snprintf(path, sizeof(path), "%s/pg_undo/instance_%u", DataDir, (unsigned)(owner_instance - 1)); if (ret < 0 || (size_t)ret >= sizeof(path)) @@ -633,7 +651,9 @@ cluster_undo_redo_segment_init(XLogReaderState *record) hdr = (xl_cluster_undo_segment_init *)payload; page_image = payload + sizeof(*hdr); - if (build_undo_segment_path(hdr->instance, hdr->segment_id, path, sizeof(path)) != 0) + if (build_undo_segment_path(cluster_undo_intent_for_owner(hdr->instance), hdr->instance, + hdr->segment_id, path, sizeof(path)) + != 0) ereport(PANIC, (errmsg("undo segment path too long: instance=%u seg=%u", hdr->instance, hdr->segment_id))); @@ -760,7 +780,9 @@ cluster_tt_durable_redo_stamp_slot(uint8 instance, uint32 segment_id, uint16 slo ereport(PANIC, (errmsg("TT slot commit redo: slot_offset %u out of range (max %d)", slot_offset, TT_SLOTS_PER_SEGMENT - 1))); - if (build_undo_segment_path(instance, segment_id, path, sizeof(path)) != 0) + if (build_undo_segment_path(cluster_undo_intent_for_owner(instance), instance, segment_id, path, + sizeof(path)) + != 0) ereport(PANIC, (errmsg("undo segment path too long: instance=%u seg=%u", instance, segment_id))); @@ -889,7 +911,9 @@ cluster_undo_redo_tt_slot_abort(XLogReaderState *record) ereport(PANIC, (errmsg("XLOG_UNDO_TT_SLOT_ABORT slot_offset %u out of range (max %d)", rec->slot_offset, TT_SLOTS_PER_SEGMENT - 1))); - if (build_undo_segment_path(rec->instance, rec->segment_id, path, sizeof(path)) != 0) + if (build_undo_segment_path(cluster_undo_intent_for_owner(rec->instance), rec->instance, + rec->segment_id, path, sizeof(path)) + != 0) ereport(PANIC, (errmsg("undo segment path too long: instance=%u seg=%u", rec->instance, rec->segment_id))); @@ -998,7 +1022,9 @@ cluster_undo_redo_tt_slot_set_head(XLogReaderState *record) ereport(PANIC, (errmsg("XLOG_UNDO_TT_SLOT_SET_HEAD slot_offset %u out of range (max %d)", rec->slot_offset, TT_SLOTS_PER_SEGMENT - 1))); - if (build_undo_segment_path(rec->instance, rec->segment_id, path, sizeof(path)) != 0) + if (build_undo_segment_path(cluster_undo_intent_for_owner(rec->instance), rec->instance, + rec->segment_id, path, sizeof(path)) + != 0) ereport(PANIC, (errmsg("undo segment path too long: instance=%u seg=%u", rec->instance, rec->segment_id))); @@ -1094,7 +1120,9 @@ cluster_undo_redo_segment_recycle(XLogReaderState *record) XLogRecGetDataLen(record)))); rec = (xl_undo_segment_recycle *)XLogRecGetData(record); - if (build_undo_segment_path(rec->instance, rec->segment_id, path, sizeof(path)) != 0) + if (build_undo_segment_path(cluster_undo_intent_for_owner(rec->instance), rec->instance, + rec->segment_id, path, sizeof(path)) + != 0) ereport(PANIC, (errmsg("undo segment path too long: instance=%u seg=%u", rec->instance, rec->segment_id))); @@ -1202,7 +1230,9 @@ cluster_undo_redo_segment_reuse(XLogReaderState *record) rec = (xl_undo_segment_reuse *)XLogRecGetData(record); image = XLogRecGetData(record) + sizeof(*rec); - if (build_undo_segment_path(rec->instance, rec->segment_id, path, sizeof(path)) != 0) + if (build_undo_segment_path(cluster_undo_intent_for_owner(rec->instance), rec->instance, + rec->segment_id, path, sizeof(path)) + != 0) ereport(PANIC, (errmsg("undo segment path too long: instance=%u seg=%u", rec->instance, rec->segment_id))); @@ -1355,7 +1385,9 @@ cluster_undo_redo_block_write(XLogReaderState *record) if (rec->block_no == 0) ereport(PANIC, (errmsg("XLOG_UNDO_BLOCK_WRITE redo: block_no 0 is the segment header"))); - if (build_undo_segment_path(rec->instance, rec->segment_id, path, sizeof(path)) != 0) + if (build_undo_segment_path(cluster_undo_intent_for_owner(rec->instance), rec->instance, + rec->segment_id, path, sizeof(path)) + != 0) ereport(PANIC, (errmsg("undo segment path too long: instance=%u seg=%u", rec->instance, rec->segment_id))); @@ -1474,7 +1506,9 @@ cluster_undo_redo_block_write_multi(XLogReaderState *record) ereport(PANIC, (errmsg("XLOG_UNDO_BLOCK_WRITE_MULTI redo: block_no 0 is the segment header"))); - if (build_undo_segment_path(rec->instance, rec->segment_id, path, sizeof(path)) != 0) + if (build_undo_segment_path(cluster_undo_intent_for_owner(rec->instance), rec->instance, + rec->segment_id, path, sizeof(path)) + != 0) ereport(PANIC, (errmsg("undo segment path too long: instance=%u seg=%u", rec->instance, rec->segment_id))); diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt index be8af8e758..190686996b 100644 --- a/src/backend/utils/errcodes.txt +++ b/src/backend/utils/errcodes.txt @@ -745,6 +745,15 @@ Section: Class 53 - Insufficient Resources (pgrac extension) # retried; never write dependent bytes to storage. 53R9P E ERRCODE_CLUSTER_SMART_FUSION_DEP_LOST cluster_smart_fusion_dep_lost +# spec-5.22a D1: undo resids are owner-as-master resources (the resource +# master IS the owning instance); routing one through the GRD shard-hash +# master lookup would place the authority at a node that does not own the +# undo. No caller may hash-route an undo resid; the hash lookup fails +# closed here (route via cluster_undo_resid_master instead). Q is the next +# free slot in the 53R9 family (A..P allocated; X taken by the clean-page +# X-transfer band below). +53R9Q E ERRCODE_CLUSTER_UNDO_RESID_HASH_ROUTED cluster_undo_resid_hash_routed + # spec-5.2a D6: clean-page X-transfer enabler terminal fail-closed. A clean # (sequence) page X-transfer could not complete safely and there is no proven- # safe fallback: a 3-node third-party master clean transfer (out of the 2-node diff --git a/src/include/cluster/cluster_guc.h b/src/include/cluster/cluster_guc.h index e4565731eb..bd975372b7 100644 --- a/src/include/cluster/cluster_guc.h +++ b/src/include/cluster/cluster_guc.h @@ -199,6 +199,11 @@ extern bool cluster_past_image; * (undo-block CF fetch + live authority gate; default off = 53R97). */ extern bool cluster_crossnode_runtime_visibility; +/* spec-5.22b D2-2: shared-undo GCS coherence master switch (default off = + * undo stays on the local DataDir; on = own-instance runtime + redo undo + * migrate to the shared cluster_fs root under owner-as-master GCS). */ +extern bool cluster_undo_gcs_coherence; + /* spec-6.15 D1: xid space segmentation -- striped allocation (default * off = vanilla dense per-node xid allocation). */ extern bool cluster_xid_striping; diff --git a/src/include/cluster/cluster_undo_gcs.h b/src/include/cluster/cluster_undo_gcs.h new file mode 100644 index 0000000000..60f7ef1a5e --- /dev/null +++ b/src/include/cluster/cluster_undo_gcs.h @@ -0,0 +1,182 @@ +/*------------------------------------------------------------------------- + * + * cluster_undo_gcs.h + * Shared-undo block GCS integration -- owner-as-master routing + + * coherent grant / PI data plane (spec-5.22b D2). + * + * D1 (cluster_undo_resid.h) named an undo block as a first-class cluster + * resource whose master IS the owning instance. D2 wires that identity + * into the real data plane: owner-as-master routing (this file's D2-1 + * layer), reader S-grant / writer X-grant with PI invalidation, a + * remaster serve-gate, and the physical migration of undo segments onto + * shared storage. + * + * D2-1 (owner-as-master routing, this increment) declares the two + * routing predicates. An undo resource NEVER hash-routes: its master is + * the encoded owner_node (cluster_undo_resid_master), and the GRD/GCS + * hash-master lookups fail closed on the undo class (D1-5 guard, 53R9Q). + * master==self selects the local fast path (no network grant); the + * owner-incarnation epoch self-check that L364 requires before serving + * from that fast path is applied on the grant/acquire path (D2-3), where + * the co-sampled live-authority triple is available -- this pure routing + * predicate is the node-id half only, so cluster_unit links it + * standalone. + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * Portions Copyright (c) 2026, pgrac contributors + * + * Author: SqlRush + * + * IDENTIFICATION + * src/include/cluster/cluster_undo_gcs.h + * + * NOTES + * This is a pgrac-original file (no derivation from PostgreSQL). + * Spec: spec-5.22b-undo-block-gcs-integration.md (D2, §2.1) + * + *------------------------------------------------------------------------- + */ +#ifndef CLUSTER_UNDO_GCS_H +#define CLUSTER_UNDO_GCS_H + +#include "cluster/cluster_grd.h" /* ClusterResId */ +#include "cluster/cluster_pcm_lock.h" /* PcmState / PcmLockTransition */ +#include "cluster/cluster_runtime_visibility.h" /* ClusterLiveAuthority */ +#include "cluster/storage/cluster_undo_alloc.h" /* ClusterUndoPathIntent */ + +/* + * cluster_undo_block_lookup_master -- owner-as-master routing entry. + * + * Returns the encoded owner_node (via cluster_undo_resid_master); an undo + * resource is NEVER hash-routed. cluster_grd_lookup_master / + * cluster_gcs_lookup_master fail closed on the undo class (D1-5 guard, + * ERRCODE_CLUSTER_UNDO_RESID_HASH_ROUTED), so this is the single legal + * master-lookup entry for undo resources. + */ +extern int32 cluster_undo_block_lookup_master(const ClusterResId *undo_resid); + +/* + * cluster_undo_block_master_is_self -- local fast-path routing gate. + * + * true iff this instance is the owning master of the undo resource + * (owner_node == cluster_node_id), i.e. the owner reading/writing its own + * undo can take the local path without a network grant. This is the pure + * routing half; the owner-incarnation epoch self-check that guards actually + * serving from the local fast path (L364) lands on the grant/acquire path + * (D2-3), not here. + */ +extern bool cluster_undo_block_master_is_self(const ClusterResId *undo_resid); + +/* + * cluster_undo_path_uses_shared_root -- pure physical-root decision (D2-2). + * + * Returns true iff an undo segment with the given path intent resolves to + * the shared cluster_fs root (cluster.shared_data_dir) rather than the + * local DataDir. Pure: takes the mode inputs explicitly (no globals), so + * cluster_unit drives every mode combination standalone. + * + * RUNTIME_SHARED -> shared iff (peer_mode && coherence_on) + * MATERIALIZED_LOCAL -> always local (P1-3 hard contract, spec-5.22b §3.6) + * + * cluster_undo_path_resolve (cluster_undo_alloc.c) and the redo write + * surface (cluster_undo_xlog.c) read the live peer_mode / coherence globals + * and delegate the decision here so the two path builders never diverge + * (own-instance undo split-brain, Hardening v1.0.1 裁决 A). + */ +extern bool cluster_undo_path_uses_shared_root(ClusterUndoPathIntent intent, bool peer_mode, + bool coherence_on); + +/* + * cluster_undo_intent_for_owner (the per-call intent derivation every undo + * smgr / path call site uses) is a static inline in cluster_undo_alloc.h so + * the ~30 call sites take no link dependency on this routing object. + */ + +/* + * ClusterUndoGrantResult -- reader S-grant outcome (spec-5.22b §2.2, D2-3). + * + * The authority triple co-sampled with the granted block image (never + * max-merged: it must stay the authority the bytes were shipped under), plus + * the semantic reply status. Under owner-as-master the owner is both master + * and holder, so a successful grant maps to GCS_BLOCK_REPLY_GRANTED_FROM_HOLDER + * (Q3★A: 2-way holder ship; the 6.12i undo-TT-fetch wire is reused verbatim, + * no new reply enum). + */ +typedef struct ClusterUndoGrantResult { + uint64 origin_epoch; /* owner incarnation epoch co-sampled with bytes */ + XLogRecPtr live_hwm_lsn; /* owner durable AND TT-applied high-water */ + uint64 tt_generation; /* owner TT-slot generation (anti-alias; D3 uses) */ + uint8 status; /* GCS_BLOCK_REPLY_* semantic status (Q3★A) */ +} ClusterUndoGrantResult; + +/* + * cluster_undo_grant_armed -- pure coherence gate (D2-3, §3.5/Q8). + * + * The mastered reader S-grant path is armed iff cluster.undo_gcs_coherence is + * on AND the deployment is peer-mode. Off in either dimension => the caller + * keeps its pre-D2 authority-less fetch path (D2 lands inert at the default). + * Pure (mode inputs explicit, no globals) so cluster_unit drives it standalone. + */ +extern bool cluster_undo_grant_armed(bool coherence_on, bool peer_mode); + +/* + * cluster_undo_grant_admissible -- pure reader S-grant admissibility (D2-3, + * §3.2/§3.3). + * + * Decide whether an owner-shipped undo-block image + co-sampled authority may + * be admitted as a coherent S view of undo_resid. Two ANDed fail-closed + * dimensions (Rule 8.A -- any doubt returns false, never admits): + * (1) SEGMENT-generation anti-ABA: the ref's encoded generation (segment + * wrap_count) must still match the reader's expected generation (D1 + * cluster_undo_resid_generation_matches), else the segment was recycled + * and the ref is stale. + * (2) owner-incarnation epoch + durable coverage: reuses the pure D-i2 gate + * cluster_vis_live_authority_covers_policy (epoch match + hwm present + + * hwm >= anchor_lsn). anchor_lsn is InvalidXLogRecPtr for a block-level + * grant with no specific page version (the version-coverage gate is the + * D3/D6 verdict consumer's, which owns the tuple's anchor). + * + * The per-slot TT generation (auth.tt_generation) is a separate finer ABA + * dimension resolved at D3 (§3.3 dim 3), not here. + */ +extern bool cluster_undo_grant_admissible(const ClusterResId *undo_resid, + uint32 expected_generation, ClusterLiveAuthority auth, + uint64 local_epoch, XLogRecPtr anchor_lsn); + +/* + * cluster_undo_grant_reader_pcm_mode / _transition -- reader lock contract + * (D2-3, §2.2). The reader takes the undo block in PCM S mode via the + * read-first N->S transition; the writer/cleaner path (X via N->X) is D2-4. + * The transition-legality table itself is owned + tested by cluster_pcm_lock. + */ +extern PcmState cluster_undo_grant_reader_pcm_mode(void); +extern PcmLockTransition cluster_undo_grant_reader_pcm_transition(void); + +/* + * cluster_undo_block_acquire_shared -- reader S-grant primitive (D2-3, §2.2). + * + * A peer reads owner N's undo block (block0 TT header is D3's first consumer) + * through owner-as-master routing: + * - not armed (coherence off / non-peer) -> false: caller keeps its old + * authority-less path (fresh refs stay 53R97 fail-closed). + * - master==self -> the owner reads its own undo; the owner-incarnation + * self-check + local fast-path land in D6 (see cluster_undo_gcs_grant.c), + * so this increment fail-closes self rather than serve an unproven local + * read (Rule 8.A). + * - master!=self (live owner) -> the owner (authority + holder) ships the + * current image over the reused 6.12i wire; the requesting peer consumes + * the shipped image and NEVER opens the foreign undo file (invariant #8). + * Admitted only through cluster_undo_grant_admissible. + * + * Returns true with *res populated and *dst_block holding the coherent image; + * false (any miss / DENIED / doubt) -> the caller MUST keep 53R97 (Rule 8.A, + * never false-visible). Runtime object (heavy deps): not in the standalone + * cluster_unit link; forward-covered by the D2-7/D6 TAP legs. + */ +extern bool cluster_undo_block_acquire_shared(const ClusterResId *undo_resid, + uint32 expected_generation, char *dst_block, + ClusterUndoGrantResult *res); + +#endif /* CLUSTER_UNDO_GCS_H */ diff --git a/src/include/cluster/cluster_undo_resid.h b/src/include/cluster/cluster_undo_resid.h new file mode 100644 index 0000000000..cb5060dc10 --- /dev/null +++ b/src/include/cluster/cluster_undo_resid.h @@ -0,0 +1,149 @@ +/*------------------------------------------------------------------------- + * + * cluster_undo_resid.h + * Shared-undo block resource identity + owner-as-master routing + * contract -- spec-5.22a D1. + * + * Names an undo block (including the segment TT header block) as a + * first-class cluster resource: (owner_node, undo_segment, block_no, + * generation) encoded into the 16-byte ClusterResId wire format. Undo + * is the first owner-as-master resid class: the resource master IS the + * owning instance (cluster_undo_resid_master returns owner_node), never + * a GRD shard-hash master. Hash-routing an undo resid through + * cluster_grd_lookup_master / cluster_gcs_lookup_master is a fail-closed + * error: the undo authority lives at the owner and a hash-derived master + * would bypass it. + * + * generation carries the segment reuse generation (the segment header + * wrap_count): a recycled segment reuses (undo_segment, block_no) for + * different content, so a generation mismatch means a stale reference + * and the caller must fail closed. The owner membership epoch (owner + * incarnation) is deliberately NOT part of the tag: it is a grant-time + * ownership-validity attribute negotiated on the authority path, not a + * static identity field. + * + * This header declares the PURE layer only (no elog / shmem / lock; + * standalone-linkable for cluster_unit). The data plane that consumes + * this identity (grant / PI / block serving / recovery materialization / + * retention) lands with later deliverables. + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * Portions Copyright (c) 2026, pgrac contributors + * + * Author: SqlRush + * + * IDENTIFICATION + * src/include/cluster/cluster_undo_resid.h + * + * NOTES + * This is a pgrac-original file (no derivation from PostgreSQL). + * Spec: spec-5.22a-undo-block-resource-identity.md (D1, §2.2 / §3.1) + * + *------------------------------------------------------------------------- + */ +#ifndef CLUSTER_UNDO_RESID_H +#define CLUSTER_UNDO_RESID_H + +#include "cluster/cluster_cf_enqueue.h" /* CLUSTER_CF_RESID_TYPE (collision check) */ +#include "cluster/cluster_dl.h" /* CLUSTER_DL_RESID_TYPE (collision check) */ +#include "cluster/cluster_grd.h" /* ClusterResId */ +#include "cluster/cluster_hw.h" /* CLUSTER_HW_RESID_TYPE (collision check) */ +#include "cluster/cluster_ir.h" /* CLUSTER_IR_RESID_TYPE (collision check) */ +#include "cluster/cluster_ko.h" /* CLUSTER_KO_RESID_TYPE (collision check) */ +#include "cluster/cluster_oid_lease.h" /* CLUSTER_OID_RESID_TYPE (collision check) */ +#include "cluster/cluster_relmap_lock.h" /* CLUSTER_RELMAP_RESID_TYPE (collision check) */ +#include "cluster/cluster_sequence.h" /* CLUSTER_SQ_RESID_TYPE (collision check) */ +#include "cluster/cluster_ts.h" /* CLUSTER_TT_RESID_TYPE (collision check) */ +#include "storage/lock.h" /* LOCKTAG_LAST_TYPE, DEFAULT_LOCKMETHOD */ + +/* + * CLUSTER_UNDO_RESID_TYPE -- undo-block resource-id namespace marker. + * 0xF9 is the next free slot after the contiguous 0xF0-0xF8 run. Must be + * above every PG LockTagType and distinct from every existing resid class. + * + * NB: 0xF3 is double-booked today by CLUSTER_DL_RESID_TYPE (cluster_dl.h) + * and the backend-local CLUSTER_RAW_LAYOUT_RESID_TYPE + * (cluster_shared_fs_block_device.c), which this cross-assert net cannot + * name. 0xF9 collides with neither; cleaning up the 0xF3 double-booking + * is a registered follow-up outside this header. + */ +#define CLUSTER_UNDO_RESID_TYPE 0xF9 + +StaticAssertDecl(CLUSTER_UNDO_RESID_TYPE > LOCKTAG_LAST_TYPE, + "undo resid namespace must not collide with any PG LockTagType"); +StaticAssertDecl(CLUSTER_UNDO_RESID_TYPE != CLUSTER_SQ_RESID_TYPE, + "undo and SQ resid namespaces must be distinct"); +StaticAssertDecl(CLUSTER_UNDO_RESID_TYPE != CLUSTER_CF_RESID_TYPE, + "undo and CF resid namespaces must be distinct"); +StaticAssertDecl(CLUSTER_UNDO_RESID_TYPE != CLUSTER_HW_RESID_TYPE, + "undo and HW resid namespaces must be distinct"); +StaticAssertDecl(CLUSTER_UNDO_RESID_TYPE != CLUSTER_DL_RESID_TYPE, + "undo and DL resid namespaces must be distinct"); +StaticAssertDecl(CLUSTER_UNDO_RESID_TYPE != CLUSTER_TT_RESID_TYPE, + "undo and TT (tablespace-DDL) resid namespaces must be distinct"); +StaticAssertDecl(CLUSTER_UNDO_RESID_TYPE != CLUSTER_IR_RESID_TYPE, + "undo and IR resid namespaces must be distinct"); +StaticAssertDecl(CLUSTER_UNDO_RESID_TYPE != CLUSTER_KO_RESID_TYPE, + "undo and KO resid namespaces must be distinct"); +StaticAssertDecl(CLUSTER_UNDO_RESID_TYPE != CLUSTER_OID_RESID_TYPE, + "undo and OID-lease resid namespaces must be distinct"); +StaticAssertDecl(CLUSTER_UNDO_RESID_TYPE != CLUSTER_RELMAP_RESID_TYPE, + "undo and RELMAP resid namespaces must be distinct"); + +/* + * ClusterResId field mapping for the undo class (16 bytes, NOT memcpy -- + * cluster_undo_resid_encode/decode are the wire-ABI boundary): + * + * field1 = undo_segment (per-instance undo segment number) + * field2 = block_no (block number within the segment; block 0 + * is the segment TT header block -- DATA and + * TT blocks share this one class) + * field3 = generation (segment reuse generation == the segment + * header wrap_count; anti-ABA guard against + * whole-segment recycling) + * field4 = owner_node (owning instance node id; uint16 on the + * wire, valid range [0, SCN_MAX_VALID_NODE_ID]) + * type = CLUSTER_UNDO_RESID_TYPE + * lockmethodid = DEFAULT_LOCKMETHOD + * + * The owner membership epoch (owner incarnation) is NOT in the tag; it is + * negotiated at grant/serve time on the authority path. + */ + +/* + * cluster_undo_resid_encode -- build the undo-block resource id. + */ +extern void cluster_undo_resid_encode(int32 owner_node, uint32 undo_segment, uint32 block_no, + uint32 generation, ClusterResId *dst); + +/* + * cluster_undo_resid_decode -- split an undo resid back into its fields. + * Must only be called on a resid whose type is CLUSTER_UNDO_RESID_TYPE. + */ +extern void cluster_undo_resid_decode(const ClusterResId *rid, int32 *owner_node, + uint32 *undo_segment, uint32 *block_no, uint32 *generation); + +/* + * cluster_undo_resid_is_undo -- class discriminator (type == 0xF9). + */ +extern bool cluster_undo_resid_is_undo(const ClusterResId *rid); + +/* + * cluster_undo_resid_master -- owner-as-master routing: returns the + * encoded owner_node directly, NEVER a hash-derived master. Undo + * resources route exclusively through this function; the GRD/GCS + * hash-master lookups reject the undo class (fail closed). + */ +extern int32 cluster_undo_resid_master(const ClusterResId *rid); + +/* + * cluster_undo_resid_generation_matches -- anti-ABA check: false means the + * reference is stale (the segment was recycled) and the caller MUST fail + * closed; it must never be treated as a match. + */ +extern bool cluster_undo_resid_generation_matches(const ClusterResId *rid, + uint32 expected_generation); + +#endif /* CLUSTER_UNDO_RESID_H */ diff --git a/src/include/cluster/cluster_undo_smgr.h b/src/include/cluster/cluster_undo_smgr.h index 20dda91f1f..01311e64da 100644 --- a/src/include/cluster/cluster_undo_smgr.h +++ b/src/include/cluster/cluster_undo_smgr.h @@ -46,19 +46,30 @@ #include "postgres.h" #include "storage/block.h" +#include "cluster/storage/cluster_undo_alloc.h" /* ClusterUndoPathIntent (D2-2) */ + + +/* + * spec-5.22b D2-2: the block / header I/O entries take an explicit + * ClusterUndoPathIntent so the shared-vs-local physical root is chosen per + * call (RUNTIME_SHARED own undo -> shared cluster_fs root under coherence; + * MATERIALIZED_LOCAL foreign dead-origin copy -> local DataDir). Callers + * pass cluster_undo_intent_for_owner(owner_instance). + */ /* * cluster_undo_smgr_read_block -- read one 8KB block from undo segment file. * * Args: + * intent -- shared-vs-local root selector (D2-2) * segment_id, owner_instance -- per-instance segment identity * block_no -- 0..UNDO_BLOCKS_PER_SEGMENT-1 * buf -- caller-provided 8KB buffer (BLCKSZ) * * Returns: true on success, false on I/O error or short read. */ -extern bool cluster_undo_smgr_read_block(uint32 segment_id, uint8 owner_instance, uint32 block_no, - char *buf); +extern bool cluster_undo_smgr_read_block(ClusterUndoPathIntent intent, uint32 segment_id, + uint8 owner_instance, uint32 block_no, char *buf); /* @@ -74,8 +85,9 @@ extern bool cluster_undo_smgr_read_block(uint32 segment_id, uint8 owner_instance * * NOT critical-section safe. */ -extern bool cluster_undo_smgr_write_block(uint32 segment_id, uint8 owner_instance, uint32 block_no, - const char *buf, bool do_fsync); +extern bool cluster_undo_smgr_write_block(ClusterUndoPathIntent intent, uint32 segment_id, + uint8 owner_instance, uint32 block_no, const char *buf, + bool do_fsync); /* @@ -89,10 +101,12 @@ extern bool cluster_undo_smgr_write_block(uint32 segment_id, uint8 owner_instanc * Returns true on success, false on bad args / I/O error / short transfer. * NOT critical-section safe. */ -extern bool cluster_undo_smgr_read_header_bytes(uint32 segment_id, uint8 owner_instance, - uint32 offset, char *buf, uint32 len); -extern bool cluster_undo_smgr_write_header_bytes(uint32 segment_id, uint8 owner_instance, - uint32 offset, const char *buf, uint32 len); +extern bool cluster_undo_smgr_read_header_bytes(ClusterUndoPathIntent intent, uint32 segment_id, + uint8 owner_instance, uint32 offset, char *buf, + uint32 len); +extern bool cluster_undo_smgr_write_header_bytes(ClusterUndoPathIntent intent, uint32 segment_id, + uint8 owner_instance, uint32 offset, + const char *buf, uint32 len); /* diff --git a/src/include/cluster/storage/cluster_shared_fs.h b/src/include/cluster/storage/cluster_shared_fs.h index 73e4f355f7..74f9fe7679 100644 --- a/src/include/cluster/storage/cluster_shared_fs.h +++ b/src/include/cluster/storage/cluster_shared_fs.h @@ -302,6 +302,38 @@ extern void cluster_shared_fs_writeback(ClusterSharedFsHandle *handle, BlockNumb BlockNumber nblocks); +/* ---------- + * Undo namespace on the shared root (spec-5.22b D2-2) + * + * Undo segments are NOT RelFileLocator relations, so the RelFileLocator + * vtable above cannot address them (see the shared-root sentinel note in + * cluster_shared_fs_sharedfs.c: non-RelFileLocator shared files are reached + * by resolving the SAME absolute path under cluster.shared_data_dir on every + * node, exactly as the recovery anchor and CF authority do). This resolver + * is that namespace for undo: it builds an owner-partitioned undo segment + * path under the shared root. cluster_undo_path_resolve routes here only for + * the CLUSTER_UNDO_PATH_RUNTIME_SHARED intent under peer-mode + + * cluster.undo_gcs_coherence. + * + * Returns 0 on success; -1 when the shared root is unset (fail-closed, so a + * misconfigured deployment never resolves a bogus path) or on buffer + * overflow. Caller supplies buf with capacity >= MAXPGPATH. + * ---------- + */ +extern int cluster_shared_fs_undo_path_resolve(uint8 owner_instance, uint32 segment_id, char *buf, + size_t buf_size); + +/* + * cluster_shared_fs_undo_instance_dir_resolve -- resolve the owner's undo + * instance DIRECTORY on the shared root (/pg_undo/ + * instance_). Unlike the local DataDir (where initdb pre-creates + * pg_undo), the shared root has no pre-seeded undo tree, so the caller + * pg_mkdir_p's this path before first use. Returns 0 / -1 (unset root). + */ +extern int cluster_shared_fs_undo_instance_dir_resolve(uint8 owner_instance, char *buf, + size_t buf_size); + + /* * Internal: built-in vtable instances exposed for cluster_shared_fs_init's * registration path and for cluster_unit linkage assertions. Backend diff --git a/src/include/cluster/storage/cluster_undo_alloc.h b/src/include/cluster/storage/cluster_undo_alloc.h index 38f316c2d8..2f65ac843a 100644 --- a/src/include/cluster/storage/cluster_undo_alloc.h +++ b/src/include/cluster/storage/cluster_undo_alloc.h @@ -34,6 +34,7 @@ #define CLUSTER_UNDO_ALLOC_H #include "c.h" +#include "cluster/cluster_scn.h" /* SCN (cluster_undo_segment_try_mark_recyclable) */ #include "storage/block.h" @@ -77,6 +78,54 @@ #define CLUSTER_UNDO_SEGS_PER_INSTANCE ((uint32)256) +/* + * ClusterUndoPathIntent -- spec-5.22b D2-2 (P1-3 hard contract). + * + * Declares, at every undo path-resolve call site, WHICH physical root a + * segment resolves to. The intent is stated EXPLICITLY by the caller and + * is never inferred from owner==self, because D4 will introduce a + * "foreign + runtime-shared serve" case (a survivor reading a dead owner's + * durable block from shared storage) where owner!=self no longer implies + * "local materialized copy". + * + * CLUSTER_UNDO_PATH_RUNTIME_SHARED + * The owner's own live runtime undo. Migrates to the shared + * cluster_fs root ONLY under peer-mode AND cluster.undo_gcs_coherence + * (both off => local DataDir, inert). + * + * CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL + * A dead-origin materialized copy that recovery rebuilt in the local + * DataDir (cluster_tt_durable.c by-xid resolve of a foreign origin, + * merged recovery, remote-xact outcome store). ALWAYS resolves to + * the local DataDir, in any mode -- D2 must never redirect these + * reads, else dead-origin recovery / CR regress (spec-5.22b §3.6, R1). + */ +typedef enum ClusterUndoPathIntent { + CLUSTER_UNDO_PATH_RUNTIME_SHARED, /* own live undo; shared under peer-mode+coherence */ + CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL /* dead-origin materialized copy; always local */ +} ClusterUndoPathIntent; + +/* cluster_node_id owns the +1 sentinel offset: owner_instance == node_id + 1. */ +extern int cluster_node_id; /* cluster_guc.c */ + +/* + * cluster_undo_intent_for_owner -- per-call intent derivation (D2-2, B). + * + * The single derivation every undo smgr / path call site consults: this + * node's own undo (owner_instance == cluster_node_id + 1) is + * RUNTIME_SHARED; a foreign owner -- in D2 always a dead-origin materialized + * copy that recovery rebuilt in the local DataDir -- is MATERIALIZED_LOCAL. + * Inline (mirrors cluster_mode.h's node-id gates) so the ~30 call sites take + * no link dependency on the GCS routing object. + */ +static inline ClusterUndoPathIntent +cluster_undo_intent_for_owner(uint8 owner_instance) +{ + return (owner_instance == (uint8)(cluster_node_id + 1)) ? CLUSTER_UNDO_PATH_RUNTIME_SHARED + : CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL; +} + + /* * cluster_undo_path_resolve * Build the segment file path: @@ -86,10 +135,19 @@ * in [0, 255] (uint8 max) * in [0, 4294967295] (uint32 max) * - * Returns 0 on success, -1 on buffer overflow. Caller supplies a - * buf with capacity >= MAXPGPATH. + * spec-5.22b D2-2: takes an explicit ClusterUndoPathIntent. A + * RUNTIME_SHARED own-instance segment resolves under the shared cluster_fs + * root when peer-mode + cluster.undo_gcs_coherence are on; MATERIALIZED_LOCAL + * (and every non-coherent mode) resolves under the local DataDir. Callers + * pass cluster_undo_intent_for_owner(owner) unless they have a stronger + * reason to name a literal intent. + * + * Returns 0 on success, -1 on buffer overflow (or an unset shared root on + * the shared branch -- fail-closed). Caller supplies a buf with capacity + * >= MAXPGPATH. */ -extern int cluster_undo_path_resolve(uint8 instance, uint32 segment_id, char *buf, size_t buf_size); +extern int cluster_undo_path_resolve(ClusterUndoPathIntent intent, uint8 instance, + uint32 segment_id, char *buf, size_t buf_size); /* diff --git a/src/test/cluster_tap/t/006_errcodes.pl b/src/test/cluster_tap/t/006_errcodes.pl index 075953a411..c233781d3e 100644 --- a/src/test/cluster_tap/t/006_errcodes.pl +++ b/src/test/cluster_tap/t/006_errcodes.pl @@ -106,6 +106,8 @@ sub raise_unknown "cluster_reconfig_in_progress -> 53R60"); is(raise_and_get_sqlstate('cluster_backup_incomplete'), '53RAD', "cluster_backup_incomplete -> 53RAD"); +is(raise_and_get_sqlstate('cluster_undo_resid_hash_routed'), '53R9Q', + "cluster_undo_resid_hash_routed -> 53R9Q"); is(raise_and_get_sqlstate('cluster_adg_apply_lag_excessive'), '57R06', "cluster_adg_apply_lag_excessive -> 57R06"); is(raise_and_get_sqlstate('cluster_adg_standby_unresolvable'), '57R07', diff --git a/src/test/cluster_unit/Makefile b/src/test/cluster_unit/Makefile index 11c235416b..05e6d1e4ec 100644 --- a/src/test/cluster_unit/Makefile +++ b/src/test/cluster_unit/Makefile @@ -86,7 +86,9 @@ TESTS = test_cluster_basic test_cluster_version test_cluster_backend_types \ test_cluster_membership \ test_cluster_node_remove \ test_cluster_hang_acceptance \ - test_cluster_xid_stripe + test_cluster_xid_stripe \ + test_cluster_undo_resid \ + test_cluster_undo_gcs # Path to the cluster_version object (no PG deps, safe to link standalone). CLUSTER_VERSION_O = $(top_builddir)/src/backend/cluster/cluster_version.o @@ -178,7 +180,7 @@ test_cluster_backup: test_cluster_backup.c unit_test.h $(CLUSTER_VERSION_O) \ # separate rules because they also link additional cluster_*.o # objects (the test files stub the PG backend symbols those # objects reference). -SIMPLE_TESTS = $(filter-out test_cluster_guc test_cluster_shmem test_cluster_signal test_cluster_views test_cluster_gviews test_cluster_ic test_cluster_conf test_cluster_ic_mock test_cluster_inject test_cluster_pgstat test_cluster_debug test_cluster_shared_fs test_cluster_shared_fs_sharedfs test_cluster_shared_fs_block_device test_cluster_smgr test_cluster_startup_phase test_cluster_lmon test_cluster_lck test_cluster_diag test_cluster_stats test_cluster_cssd test_cluster_qvotec test_cluster_voting_disk_io test_cluster_quorum_decision test_cluster_scn test_cluster_adg test_cluster_epoch test_cluster_fence test_cluster_reconfig test_cluster_ges test_cluster_grd test_cluster_grd_starvation test_cluster_lmd test_cluster_lmd_graph test_cluster_lmd_wait_state test_cluster_cancel_token test_cluster_lmd_probe_collector test_cluster_lock_acquire test_cluster_advisory test_cluster_terminal_authority test_cluster_retention test_cluster_visibility_variants test_cluster_tt_2pc test_cluster_stage3_acceptance test_cluster_undo_buf test_cluster_block_apply test_cluster_thread_apply test_cluster_thread_replay test_cluster_thread_driver test_cluster_thread_orchestrator test_cluster_write_fence test_cluster_stage4_acceptance test_cluster_stage5_integrated_acceptance test_cluster_stage5_beta_acceptance test_cluster_ges_mode test_cluster_sequence test_cluster_shared_catalog test_cluster_hw test_cluster_dl test_cluster_extend_gate test_cluster_ir test_cluster_ts test_cluster_ko test_cluster_hw_snapshot test_cluster_cf_authority test_cluster_cf_storage test_cluster_cf_enqueue test_cluster_cf_phase2 test_cluster_cf_stats test_cluster_hang test_cluster_hang_resolve test_cluster_cr_server_policy test_cluster_touched_peers test_cluster_clean_leave test_cluster_membership test_cluster_node_remove test_cluster_resolver_cache test_cluster_backup test_cluster_hang_acceptance test_cluster_gcs_reqid test_cluster_runtime_visibility test_cluster_xid_stripe test_cluster_bufmgr_pcm_hook test_cluster_cr test_cluster_cr_admit test_cluster_cr_admit_stat test_cluster_cr_cache test_cluster_cr_coordinator test_cluster_cr_key test_cluster_cr_lifecycle test_cluster_cr_pool test_cluster_cr_tuple test_cluster_cr_tuple_stat test_cluster_gcs_block test_cluster_gcs_block_2way test_cluster_gcs_block_3way test_cluster_gcs_block_lost_write test_cluster_gcs_block_retransmit test_cluster_gcs_dispatch test_cluster_ges_handoff test_cluster_heap_lock_tuple test_cluster_hw_lease test_cluster_ic_envelope test_cluster_ic_router test_cluster_itl_cleanout test_cluster_itl_cleanout_perf test_cluster_itl_reader_real_triple test_cluster_itl_touch test_cluster_itl_wal test_cluster_multixact test_cluster_pcm_lock test_cluster_perf_gates test_cluster_recovery_merge test_cluster_recovery_plan test_cluster_recovery_worker test_cluster_reverse_key test_cluster_sinval test_cluster_sinval_ack test_cluster_snapshot_source test_cluster_stage2_acceptance test_cluster_stage5_5_cr_acceptance test_cluster_subtrans test_cluster_tt_durable test_cluster_tt_slot_allocator test_cluster_tt_status test_cluster_tt_status_hint test_cluster_uba test_cluster_undo_format test_cluster_undo_lifecycle test_cluster_undo_record test_cluster_visibility_decide_scn test_cluster_visibility_fork test_cluster_visibility_inject test_cluster_wal_state test_cluster_wal_thread test_cluster_xnode_lever test_cluster_xnode_profile test_cluster_pi_shadow test_cluster_oid_lease test_cluster_recovery_anchor test_cluster_relmap_authority,$(TESTS)) +SIMPLE_TESTS = $(filter-out test_cluster_guc test_cluster_shmem test_cluster_signal test_cluster_views test_cluster_gviews test_cluster_ic test_cluster_conf test_cluster_ic_mock test_cluster_inject test_cluster_pgstat test_cluster_debug test_cluster_shared_fs test_cluster_shared_fs_sharedfs test_cluster_shared_fs_block_device test_cluster_smgr test_cluster_startup_phase test_cluster_lmon test_cluster_lck test_cluster_diag test_cluster_stats test_cluster_cssd test_cluster_qvotec test_cluster_voting_disk_io test_cluster_quorum_decision test_cluster_scn test_cluster_adg test_cluster_epoch test_cluster_fence test_cluster_reconfig test_cluster_ges test_cluster_grd test_cluster_grd_starvation test_cluster_lmd test_cluster_lmd_graph test_cluster_lmd_wait_state test_cluster_cancel_token test_cluster_lmd_probe_collector test_cluster_lock_acquire test_cluster_advisory test_cluster_terminal_authority test_cluster_retention test_cluster_visibility_variants test_cluster_tt_2pc test_cluster_stage3_acceptance test_cluster_undo_buf test_cluster_block_apply test_cluster_thread_apply test_cluster_thread_replay test_cluster_thread_driver test_cluster_thread_orchestrator test_cluster_write_fence test_cluster_stage4_acceptance test_cluster_stage5_integrated_acceptance test_cluster_stage5_beta_acceptance test_cluster_ges_mode test_cluster_sequence test_cluster_shared_catalog test_cluster_hw test_cluster_dl test_cluster_extend_gate test_cluster_ir test_cluster_ts test_cluster_ko test_cluster_hw_snapshot test_cluster_cf_authority test_cluster_cf_storage test_cluster_cf_enqueue test_cluster_cf_phase2 test_cluster_cf_stats test_cluster_hang test_cluster_hang_resolve test_cluster_cr_server_policy test_cluster_touched_peers test_cluster_clean_leave test_cluster_membership test_cluster_node_remove test_cluster_resolver_cache test_cluster_backup test_cluster_hang_acceptance test_cluster_gcs_reqid test_cluster_runtime_visibility test_cluster_xid_stripe test_cluster_bufmgr_pcm_hook test_cluster_cr test_cluster_cr_admit test_cluster_cr_admit_stat test_cluster_cr_cache test_cluster_cr_coordinator test_cluster_cr_key test_cluster_cr_lifecycle test_cluster_cr_pool test_cluster_cr_tuple test_cluster_cr_tuple_stat test_cluster_gcs_block test_cluster_gcs_block_2way test_cluster_gcs_block_3way test_cluster_gcs_block_lost_write test_cluster_gcs_block_retransmit test_cluster_gcs_dispatch test_cluster_ges_handoff test_cluster_heap_lock_tuple test_cluster_hw_lease test_cluster_ic_envelope test_cluster_ic_router test_cluster_itl_cleanout test_cluster_itl_cleanout_perf test_cluster_itl_reader_real_triple test_cluster_itl_touch test_cluster_itl_wal test_cluster_multixact test_cluster_pcm_lock test_cluster_perf_gates test_cluster_recovery_merge test_cluster_recovery_plan test_cluster_recovery_worker test_cluster_reverse_key test_cluster_sinval test_cluster_sinval_ack test_cluster_snapshot_source test_cluster_stage2_acceptance test_cluster_stage5_5_cr_acceptance test_cluster_subtrans test_cluster_tt_durable test_cluster_tt_slot_allocator test_cluster_tt_status test_cluster_tt_status_hint test_cluster_uba test_cluster_undo_format test_cluster_undo_lifecycle test_cluster_undo_record test_cluster_visibility_decide_scn test_cluster_visibility_fork test_cluster_visibility_inject test_cluster_wal_state test_cluster_wal_thread test_cluster_xnode_lever test_cluster_xnode_profile test_cluster_pi_shadow test_cluster_oid_lease test_cluster_recovery_anchor test_cluster_relmap_authority test_cluster_undo_resid test_cluster_undo_gcs,$(TESTS)) # spec-2.4 D16: test_cluster_epoch links cluster_epoch.o standalone. # cluster_epoch.c references ShmemInitStruct + cluster_shmem_register_region @@ -1828,3 +1830,34 @@ test_cluster_xid_stripe: test_cluster_xid_stripe.c unit_test.h \ $(CC) $(CFLAGS) $(CPPFLAGS) $< \ $(CLUSTER_XID_STRIPE_O) \ $(top_builddir)/src/port/libpgport_srv.a -o $@ + +# spec-5.22a D1: test_cluster_undo_resid — shared-undo block resource +# identity pure layer (undo resid class byte + encode/decode + +# owner-as-master routing + anti-ABA generation predicate). Links +# cluster_hw.o for the is_undo cross-class check (real HW encoder). The +# pure layer has no PG-backend dependencies, so nothing is stubbed beyond +# the Assert ExceptionalCondition hook. +CLUSTER_UNDO_RESID_O = $(top_builddir)/src/backend/cluster/cluster_undo_resid.o +test_cluster_undo_resid: test_cluster_undo_resid.c unit_test.h \ + $(CLUSTER_VERSION_O) $(CLUSTER_UNDO_RESID_O) $(CLUSTER_HW_O) + $(CC) $(CFLAGS) $(CPPFLAGS) $< \ + $(CLUSTER_VERSION_O) $(CLUSTER_UNDO_RESID_O) $(CLUSTER_HW_O) \ + $(top_builddir)/src/port/libpgport_srv.a -o $@ + +# spec-5.22b D2-1/D2-3: test_cluster_undo_gcs — owner-as-master routing layer +# (D2-1: cluster_undo_block_lookup_master / _master_is_self) + the pure reader +# S-grant decision core (D2-3: cluster_undo_grant_armed / _admissible / +# _reader_pcm_*). Links the D1 undo resid pure object, the D2 routing object, +# and the pure live-authority policy object (cluster_undo_grant_admissible +# reuses cluster_vis_live_authority_covers_policy); the test owns the +# cluster_node_id stub so the routing object links without the GUC layer. The +# runtime acquire_shared wrapper (heavy fetch/epoch/smgr deps) lives in +# cluster_undo_gcs_grant.o, NOT here — it is forward-covered by the D2-7/D6 TAP. +CLUSTER_UNDO_GCS_O = $(top_builddir)/src/backend/cluster/cluster_undo_gcs.o +test_cluster_undo_gcs: test_cluster_undo_gcs.c unit_test.h \ + $(CLUSTER_VERSION_O) $(CLUSTER_UNDO_RESID_O) $(CLUSTER_UNDO_GCS_O) \ + $(CLUSTER_RUNTIME_VIS_POLICY_O) + $(CC) $(CFLAGS) $(CPPFLAGS) $< \ + $(CLUSTER_VERSION_O) $(CLUSTER_UNDO_RESID_O) $(CLUSTER_UNDO_GCS_O) \ + $(CLUSTER_RUNTIME_VIS_POLICY_O) \ + $(top_builddir)/src/port/libpgport_srv.a -o $@ diff --git a/src/test/cluster_unit/test_cluster_grd.c b/src/test/cluster_unit/test_cluster_grd.c index 2a0b568524..73f280fbd5 100644 --- a/src/test/cluster_unit/test_cluster_grd.c +++ b/src/test/cluster_unit/test_cluster_grd.c @@ -56,6 +56,7 @@ #include "access/transam.h" /* spec-5.8 D1c — InvalidTransactionId */ #include "cluster/cluster_grd.h" #include "cluster/cluster_lmd.h" /* spec-5.8 D1b — WFG vertex + submit/cancel edge */ +#include "cluster/cluster_undo_resid.h" /* spec-5.22a D1-5 — undo-class hash-route guard */ #include "cluster/cluster_reconfig.h" /* spec-4.6 D1 — ReconfigEvent stub type */ #include "cluster/cluster_thread_recovery.h" /* spec-4.11 D3 (L238) — gate_unfreeze proto */ #include "port/atomics.h" @@ -85,34 +86,55 @@ bool IsUnderPostmaster = false; +/* spec-5.22a D1-5 verifies a fail-closed guard is reached. Setjmp-based + * trampoline (mirrors test_cluster_fence.c): when armed, an Assert trip + * (assert builds) or an ereport(ERROR) (production builds) jumps back to + * the test instead of aborting the binary. For elevel < ERROR the stubs + * keep the historical silent-no-op behaviour. */ +static sigjmp_buf ut_trap_jump; +static bool ut_trap_jump_armed = false; +static int ut_ereport_last_errcode = 0; +static int ut_current_elevel = 0; + void ExceptionalCondition(const char *conditionName pg_attribute_unused(), const char *fileName pg_attribute_unused(), int lineNumber pg_attribute_unused()) { + if (ut_trap_jump_armed) + siglongjmp(ut_trap_jump, 2); abort(); } bool -errstart(int e pg_attribute_unused(), const char *d pg_attribute_unused()) +errstart(int elevel, const char *d pg_attribute_unused()) { - return false; + ut_current_elevel = elevel; + /* PG: ERROR = 21 (elog.h). >= ERROR runs the ereport body so the + * errcode stub can capture the SQLSTATE before errfinish jumps. */ + return elevel >= 21; } bool -errstart_cold(int e pg_attribute_unused(), const char *d pg_attribute_unused()) +errstart_cold(int elevel, const char *d) { - return false; + return errstart(elevel, d); } void errfinish(const char *f pg_attribute_unused(), int l pg_attribute_unused(), const char *fn pg_attribute_unused()) -{} +{ + if (ut_current_elevel >= 21 && ut_trap_jump_armed) + siglongjmp(ut_trap_jump, 1); + /* Otherwise (LOG/NOTICE/no jump armed): silent return. */ +} int -errcode(int s pg_attribute_unused()) +errcode(int s) { + if (ut_trap_jump_armed) + ut_ereport_last_errcode = s; return 0; } @@ -1983,6 +2005,47 @@ UT_TEST(test_grd_lookup_master_gen_q3c_verbatim) mock_lms_shard_master_generation = 0; } +/* spec-5.22a D1-5: the GRD shard-hash master lookup fail-closes on an + * undo-class resid. Undo is owner-as-master (the master IS the encoded + * owner); a hash-derived master would bypass the owner authority, so no + * caller may hash-route it. Assert builds trip the guard's Assert first + * (trampoline rc 2); production builds reach the ereport(ERROR) (rc 1) + * whose SQLSTATE must be 53R9Q. */ +UT_TEST(test_grd_lookup_master_rejects_undo_resid) +{ + int32 nodes2[] = { 0, 1 }; + ClusterResId undo; + int rc; + + cluster_grd_shmem_init(); + set_mock_declared(2, nodes2); + cluster_grd_master_map_init(); + + /* hand-built undo-class resid: only the type byte matters to the + * guard (the undo encoder object is deliberately not linked here) */ + memset(&undo, 0, sizeof(undo)); + undo.field1 = 7; /* undo_segment */ + undo.field2 = 129; /* block_no */ + undo.field3 = 3; /* generation */ + undo.field4 = 1; /* owner_node */ + undo.type = CLUSTER_UNDO_RESID_TYPE; + undo.lockmethodid = DEFAULT_LOCKMETHOD; + + ut_ereport_last_errcode = 0; + rc = sigsetjmp(ut_trap_jump, 0); + if (rc == 0) { + ut_trap_jump_armed = true; + (void)cluster_grd_lookup_master(&undo); + /* reaching here means the undo resid was hash-routed */ + ut_trap_jump_armed = false; + UT_ASSERT(false); + } + ut_trap_jump_armed = false; + UT_ASSERT(rc == 1 || rc == 2); + if (rc == 1) /* ereport path (production builds) */ + UT_ASSERT_EQ(ut_ereport_last_errcode, ERRCODE_CLUSTER_UNDO_RESID_HASH_ROUTED); +} + UT_TEST(test_grd_shard_phase_accessors) { int32 nodes2[] = { 0, 1 }; @@ -4055,7 +4118,7 @@ main(int argc pg_attribute_unused(), char *argv[] pg_attribute_unused()) * D1e:+2 (U4a-b); 5.9 Hardening:+1 (convert ABA); * spec-5.16:+12 (join-remaster U1-U5/U10-U16); * +1 (U17 cross-episode fence Hardening). */ - UT_PLAN(80); + UT_PLAN(81); UT_RUN(test_grd_clusterresid_size_16); UT_RUN(test_grd_resid_encode_decode_roundtrip); @@ -4091,6 +4154,7 @@ main(int argc pg_attribute_unused(), char *argv[] pg_attribute_unused()) UT_RUN(test_grd_remaster_multi_death_and_sparse); UT_RUN(test_grd_remaster_no_survivor_fail_closed); UT_RUN(test_grd_lookup_master_gen_q3c_verbatim); + UT_RUN(test_grd_lookup_master_rejects_undo_resid); UT_RUN(test_grd_shard_phase_accessors); UT_RUN(test_grd_d2_redeclare_scan_completion_gate); diff --git a/src/test/cluster_unit/test_cluster_shared_fs.c b/src/test/cluster_unit/test_cluster_shared_fs.c index 71f7f05aa1..0d8724b75a 100644 --- a/src/test/cluster_unit/test_cluster_shared_fs.c +++ b/src/test/cluster_unit/test_cluster_shared_fs.c @@ -731,6 +731,32 @@ UT_TEST(test_sharedfs_sentinel_symbols_linkable) UT_ASSERT_NOT_NULL((void *)has_fn); } +/* ============================================================ + * spec-5.22b D2-2: undo namespace path resolve on the shared root. + * Non-RelFileLocator; mirrors build_anchor_path (fail-closed when the + * shared root is unset), owner-partitioned instance_. + * ============================================================ */ +UT_TEST(test_undo_shared_path_resolve) +{ + char buf[MAXPGPATH]; + + /* fail-closed when the shared root is unset: never resolve a bogus path */ + cluster_shared_data_dir = NULL; + UT_ASSERT_EQ(cluster_shared_fs_undo_path_resolve(1, 1, buf, sizeof(buf)), -1); + + /* owner-partitioned instance_ under the shared root; the directory uses + * owner_instance-1 (= node_id), matching the local $PGDATA/pg_undo layout */ + cluster_shared_data_dir = "/u01/pgrac/shared"; + UT_ASSERT_EQ(cluster_shared_fs_undo_path_resolve(1, 1, buf, sizeof(buf)), 0); + UT_ASSERT_STR_EQ(buf, "/u01/pgrac/shared/pg_undo/instance_0/seg_1.dat"); + + cluster_shared_data_dir = "/mnt/shared"; + UT_ASSERT_EQ(cluster_shared_fs_undo_path_resolve(3, 257, buf, sizeof(buf)), 0); + UT_ASSERT_STR_EQ(buf, "/mnt/shared/pg_undo/instance_2/seg_257.dat"); + + cluster_shared_data_dir = NULL; /* restore stub default */ +} + /* ============================================================ * Test runner @@ -739,7 +765,7 @@ UT_TEST(test_sharedfs_sentinel_symbols_linkable) int main(void) { - UT_PLAN(15); + UT_PLAN(16); UT_RUN(test_shared_fs_backend_max_constant); UT_RUN(test_shared_fs_backend_id_enum_frozen); UT_RUN(test_shared_fs_vtable_struct_nonempty); @@ -755,6 +781,7 @@ main(void) UT_RUN(test_sharedfs_vtable_callbacks_nonnull); UT_RUN(test_sharedfs_vtable_identity); UT_RUN(test_sharedfs_sentinel_symbols_linkable); + UT_RUN(test_undo_shared_path_resolve); UT_DONE(); return ut_failed_count == 0 ? 0 : 1; } diff --git a/src/test/cluster_unit/test_cluster_tt_durable.c b/src/test/cluster_unit/test_cluster_tt_durable.c index bab49dde88..3b286c6a9d 100644 --- a/src/test/cluster_unit/test_cluster_tt_durable.c +++ b/src/test/cluster_unit/test_cluster_tt_durable.c @@ -193,7 +193,8 @@ cluster_undo_cleaner_scan_wait_end(void) {} bool -cluster_undo_smgr_read_header_bytes(uint32 segment_id pg_attribute_unused(), +cluster_undo_smgr_read_header_bytes(ClusterUndoPathIntent intent pg_attribute_unused(), + uint32 segment_id pg_attribute_unused(), uint8 owner_instance pg_attribute_unused(), uint32 offset, char *buf, uint32 len) { @@ -212,7 +213,8 @@ static int g_write_hdr_calls = 0; /* spec-3.13 D2-B scan-only invariant probe * static TTSlot g_last_written_slot; /* spec-3.15: capture write payload */ bool -cluster_undo_smgr_write_header_bytes(uint32 segment_id pg_attribute_unused(), +cluster_undo_smgr_write_header_bytes(ClusterUndoPathIntent intent pg_attribute_unused(), + uint32 segment_id pg_attribute_unused(), uint8 owner_instance pg_attribute_unused(), uint32 offset pg_attribute_unused(), const char *buf, uint32 len) @@ -224,8 +226,8 @@ cluster_undo_smgr_write_header_bytes(uint32 segment_id pg_attribute_unused(), } bool -cluster_undo_smgr_read_block(uint32 segment_id, uint8 owner_instance pg_attribute_unused(), - uint32 block_no, char *buf) +cluster_undo_smgr_read_block(ClusterUndoPathIntent intent pg_attribute_unused(), uint32 segment_id, + uint8 owner_instance pg_attribute_unused(), uint32 block_no, char *buf) { if (buf == NULL || block_no != 0 || segment_id != g_canned_block_segment) return false; /* other segments "don't exist" -> by-xid skips them */ diff --git a/src/test/cluster_unit/test_cluster_undo_buf.c b/src/test/cluster_unit/test_cluster_undo_buf.c index c959f0b3cf..63d09579e5 100644 --- a/src/test/cluster_unit/test_cluster_undo_buf.c +++ b/src/test/cluster_unit/test_cluster_undo_buf.c @@ -63,6 +63,14 @@ UT_DEFINE_GLOBALS(); +/* + * Self-node stub: cluster_undo_buf.o now derives the undo path intent via the + * cluster_undo_intent_for_owner inline (spec-5.22b D2-2), which reads + * cluster_node_id. The buffer-pool unit exercises own-instance segments, so + * node 0 keeps every smgr call on the RUNTIME_SHARED (own) branch. + */ +int cluster_node_id = 0; + void ExceptionalCondition(const char *conditionName pg_attribute_unused(), const char *fileName pg_attribute_unused(), @@ -217,7 +225,8 @@ static uint32 smgr_last_write_block = 0; static char smgr_last_write_byte0 = 0; bool -cluster_undo_smgr_read_block(uint32 segment_id pg_attribute_unused(), +cluster_undo_smgr_read_block(ClusterUndoPathIntent intent pg_attribute_unused(), + uint32 segment_id pg_attribute_unused(), uint8 owner_instance pg_attribute_unused(), uint32 block_no, char *buf) { smgr_read_calls++; @@ -227,7 +236,8 @@ cluster_undo_smgr_read_block(uint32 segment_id pg_attribute_unused(), } bool -cluster_undo_smgr_write_block(uint32 segment_id pg_attribute_unused(), +cluster_undo_smgr_write_block(ClusterUndoPathIntent intent pg_attribute_unused(), + uint32 segment_id pg_attribute_unused(), uint8 owner_instance pg_attribute_unused(), uint32 block_no, const char *buf, bool do_fsync) { diff --git a/src/test/cluster_unit/test_cluster_undo_gcs.c b/src/test/cluster_unit/test_cluster_undo_gcs.c new file mode 100644 index 0000000000..47e1d9d726 --- /dev/null +++ b/src/test/cluster_unit/test_cluster_undo_gcs.c @@ -0,0 +1,288 @@ +/*------------------------------------------------------------------------- + * + * test_cluster_undo_gcs.c + * Unit tests for the shared-undo block GCS integration data plane + * (spec-5.22b D2). + * + * D2-1 (this increment) pins owner-as-master routing: the master of an + * undo resource IS the encoded owner_node (never a shard hash), and the + * master==self predicate selects the local fast path. The grant / PI / + * physical-migration legs (D2-2 .. D2-5) add their own truth-table cases + * to this file as they land. + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * Portions Copyright (c) 2026, pgrac contributors + * + * Author: SqlRush + * + * IDENTIFICATION + * src/test/cluster_unit/test_cluster_undo_gcs.c + * + * NOTES + * This is a pgrac-original file. + * Spec: spec-5.22b-undo-block-gcs-integration.md (D2, §4.1) + * + * cluster_undo_gcs.o references the cluster_node_id global (self node); + * the test supplies its own definition so the routing object links + * standalone without the full GUC layer. + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cluster/cluster_scn.h" /* SCN_MAX_VALID_NODE_ID */ +#include "cluster/cluster_undo_gcs.h" +#include "cluster/cluster_undo_resid.h" +#include "cluster/storage/cluster_undo_alloc.h" /* ClusterUndoPathIntent */ + +#undef printf +#undef fprintf +#undef snprintf + +#include "unit_test.h" + +UT_DEFINE_GLOBALS(); + +/* + * Stub self-node global. cluster_undo_gcs.o resolves cluster_node_id from + * cluster_guc.c in a real backend; here the test owns it so master_is_self + * can be driven against a known self node. + */ +int cluster_node_id = 0; + +void +ExceptionalCondition(const char *conditionName, const char *fileName, int lineNumber) +{ + printf("# Assert failed: %s at %s:%d\n", conditionName, fileName, lineNumber); + abort(); +} + +/* ====================================================================== + * U1 -- owner-as-master routing: lookup_master returns the encoded + * owner_node, never a hash-derived node (spec-5.22b §2.1, Q1) + * ====================================================================== */ +UT_TEST(test_undo_gcs_lookup_master_is_owner) +{ + ClusterResId r; + + cluster_undo_resid_encode(2, 7, 129, 3, &r); + UT_ASSERT_EQ(cluster_undo_block_lookup_master(&r), 2); + + /* the master must not vary with the non-owner identity dimensions + * (a shard-hash master would move with segment/block/generation) */ + cluster_undo_resid_encode(2, 9999, 424242, 17, &r); + UT_ASSERT_EQ(cluster_undo_block_lookup_master(&r), 2); + + cluster_undo_resid_encode(0, 1, 1, 0, &r); + UT_ASSERT_EQ(cluster_undo_block_lookup_master(&r), 0); + + cluster_undo_resid_encode(SCN_MAX_VALID_NODE_ID, 1, 1, 1, &r); + UT_ASSERT_EQ(cluster_undo_block_lookup_master(&r), SCN_MAX_VALID_NODE_ID); +} + +/* ====================================================================== + * U2 -- master_is_self: owner_node == cluster_node_id selects the local + * fast path (true); a foreign owner routes remote (false) (spec-5.22b §2.1) + * ====================================================================== */ +UT_TEST(test_undo_gcs_master_is_self) +{ + ClusterResId r; + + cluster_node_id = 2; + cluster_undo_resid_encode(2, 7, 129, 3, &r); /* owner == self */ + UT_ASSERT(cluster_undo_block_master_is_self(&r)); + + cluster_undo_resid_encode(3, 7, 129, 3, &r); /* owner 3 != self 2 */ + UT_ASSERT(!cluster_undo_block_master_is_self(&r)); + + cluster_node_id = 0; + cluster_undo_resid_encode(0, 1, 1, 0, &r); /* owner == self */ + UT_ASSERT(cluster_undo_block_master_is_self(&r)); + + cluster_undo_resid_encode(1, 1, 1, 0, &r); /* owner 1 != self 0 */ + UT_ASSERT(!cluster_undo_block_master_is_self(&r)); + + cluster_node_id = SCN_MAX_VALID_NODE_ID; + cluster_undo_resid_encode(SCN_MAX_VALID_NODE_ID, 4, 4, 4, &r); + UT_ASSERT(cluster_undo_block_master_is_self(&r)); +} + +/* ====================================================================== + * U3 -- physical-path root decision (D2-2): a RUNTIME_SHARED (own-instance + * live) undo segment migrates to the shared cluster_fs root ONLY under + * peer-mode AND cluster.undo_gcs_coherence; every other mode combination + * stays on the local DataDir (inert, zero behaviour change) (spec-5.22b + * §2.3/§3.5, Hardening v1.0.1 裁决 A, Q8/Q9) + * ====================================================================== */ +UT_TEST(test_undo_gcs_path_runtime_shared_mode_branch) +{ + /* migrate: own runtime undo, peer-mode, coherence on */ + UT_ASSERT(cluster_undo_path_uses_shared_root(CLUSTER_UNDO_PATH_RUNTIME_SHARED, true /*peer*/, + true /*coherence*/)); + + /* coherence off => inert, stay local (回归安全, 回 6.12i 无主权路径) */ + UT_ASSERT(!cluster_undo_path_uses_shared_root(CLUSTER_UNDO_PATH_RUNTIME_SHARED, true /*peer*/, + false /*coherence*/)); + + /* single-node / non-peer-mode => local regardless of coherence */ + UT_ASSERT(!cluster_undo_path_uses_shared_root(CLUSTER_UNDO_PATH_RUNTIME_SHARED, false /*peer*/, + true /*coherence*/)); + UT_ASSERT(!cluster_undo_path_uses_shared_root(CLUSTER_UNDO_PATH_RUNTIME_SHARED, false /*peer*/, + false /*coherence*/)); +} + +/* ====================================================================== + * U10 -- path-intent layering (P1-3 hard contract): a MATERIALIZED_LOCAL + * (dead-origin materialized copy that recovery rebuilt in the local + * DataDir) NEVER migrates to the shared root, in ANY mode. D2 must not + * redirect dead-origin materialized reads, else dead-origin recovery / CR + * regress (spec-5.22b §3.6, R1, Q9) + * ====================================================================== */ +UT_TEST(test_undo_gcs_path_materialized_never_migrates) +{ + UT_ASSERT(!cluster_undo_path_uses_shared_root(CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL, + true /*peer*/, true /*coherence*/)); + UT_ASSERT(!cluster_undo_path_uses_shared_root(CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL, + true /*peer*/, false /*coherence*/)); + UT_ASSERT(!cluster_undo_path_uses_shared_root(CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL, + false /*peer*/, true /*coherence*/)); + UT_ASSERT(!cluster_undo_path_uses_shared_root(CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL, + false /*peer*/, false /*coherence*/)); +} + +/* ====================================================================== + * U11 -- per-call intent derivation (spec-5.22b D2-2, threading strategy B): + * an undo segment whose owner IS this node maps to RUNTIME_SHARED (own live + * undo); any foreign owner maps to MATERIALIZED_LOCAL (dead-origin + * materialized copy). This is the single derivation every smgr call site + * uses, so a misclassification cannot hide at an individual site. + * owner_instance == cluster_node_id + 1 is "self" (the +1 sentinel offset). + * ====================================================================== */ +UT_TEST(test_undo_gcs_intent_for_owner) +{ + cluster_node_id = 0; /* self owner_instance = 1 */ + UT_ASSERT_EQ(cluster_undo_intent_for_owner(1), CLUSTER_UNDO_PATH_RUNTIME_SHARED); + UT_ASSERT_EQ(cluster_undo_intent_for_owner(2), CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL); + UT_ASSERT_EQ(cluster_undo_intent_for_owner(3), CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL); + + cluster_node_id = 2; /* self owner_instance = 3 */ + UT_ASSERT_EQ(cluster_undo_intent_for_owner(3), CLUSTER_UNDO_PATH_RUNTIME_SHARED); + UT_ASSERT_EQ(cluster_undo_intent_for_owner(1), CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL); + UT_ASSERT_EQ(cluster_undo_intent_for_owner(4), CLUSTER_UNDO_PATH_MATERIALIZED_LOCAL); + + cluster_node_id = 0; /* restore stub default */ +} + +/* ====================================================================== + * U9 -- reader S-grant coherence gate (spec-5.22b §3.5, Q8): the mastered + * S-grant path is ARMED only when cluster.undo_gcs_coherence is on AND the + * deployment is peer-mode. Off in either dimension => not armed => the caller + * keeps its pre-D2 authority-less fetch path (回归安全, zero behaviour change at + * the default). Pure: branch-only integer logic, no globals. + * ====================================================================== */ +UT_TEST(test_undo_gcs_grant_armed_gate) +{ + UT_ASSERT(cluster_undo_grant_armed(true /*coherence*/, true /*peer*/)); + UT_ASSERT(!cluster_undo_grant_armed(false /*coherence*/, true /*peer*/)); + UT_ASSERT(!cluster_undo_grant_armed(true /*coherence*/, false /*peer*/)); + UT_ASSERT(!cluster_undo_grant_armed(false /*coherence*/, false /*peer*/)); +} + +/* ====================================================================== + * U4 -- reader S-grant lock contract (spec-5.22b §2.2/§4.1): the reader + * acquires the undo block in PCM S mode via the read-first N->S transition. + * The N->S transition-legality TABLE is owned + exhaustively tested by + * cluster_pcm_lock (test_cluster_pcm_lock, spec-2.30); D2-3 only pins that the + * reader selects that legal read-first pair, never a write/upgrade transition. + * Pure: compile-time constants. + * ====================================================================== */ +UT_TEST(test_undo_gcs_grant_reader_pcm_contract) +{ + UT_ASSERT_EQ(cluster_undo_grant_reader_pcm_mode(), PCM_LOCK_MODE_S); + UT_ASSERT_EQ(cluster_undo_grant_reader_pcm_transition(), PCM_TRANS_N_TO_S); + + /* a reader must never select a write-side mode / transition */ + UT_ASSERT(cluster_undo_grant_reader_pcm_mode() != PCM_LOCK_MODE_X); + UT_ASSERT(cluster_undo_grant_reader_pcm_transition() != PCM_TRANS_N_TO_X); +} + +/* ====================================================================== + * U7 -- reader S-grant admissibility: SEGMENT-generation anti-ABA (spec-5.22b + * §3.3 dim 1, D1). A reference whose encoded generation (segment wrap_count) + * no longer matches the reader's expected generation is STALE (the segment was + * recycled) -- admit MUST fail closed (Rule 8.A), never treat as a match. The + * per-slot TT generation (auth.tt_generation) is a SEPARATE finer dimension + * resolved at D3, not here. + * ====================================================================== */ +UT_TEST(test_undo_gcs_grant_admissible_generation) +{ + ClusterResId r; + ClusterLiveAuthority auth; + + /* authority that is otherwise admissible (epoch match, hwm present) */ + memset(&auth, 0, sizeof(auth)); + auth.origin_epoch = 42; + auth.live_hwm_lsn = 0x1000; + auth.tt_generation = 9; + + cluster_undo_resid_encode(3, 7, 129, 5 /*generation*/, &r); + + /* generation matches => admissible (epoch 42 == local 42, hwm >= anchor 0) */ + UT_ASSERT(cluster_undo_grant_admissible(&r, 5 /*expected_gen*/, auth, 42 /*local_epoch*/, + InvalidXLogRecPtr)); + + /* generation mismatch (recycled segment) => fail closed */ + UT_ASSERT( + !cluster_undo_grant_admissible(&r, 6 /*expected_gen != 5*/, auth, 42, InvalidXLogRecPtr)); +} + +/* ====================================================================== + * U6 -- reader S-grant admissibility: owner-incarnation epoch + durable + * coverage (spec-5.22b §3.2, D-i2/D-i3). Authority sampled under a different + * membership epoch than the reader's current epoch cannot be trusted (a + * reconfig may have remastered/fenced the owner between sample and use) => fail + * closed. A missing live authority (hwm invalid) also fails closed -- never + * guess. Reuses the pure cluster_vis_live_authority_covers_policy gate. + * ====================================================================== */ +UT_TEST(test_undo_gcs_grant_admissible_epoch) +{ + ClusterResId r; + ClusterLiveAuthority auth; + + cluster_undo_resid_encode(3, 7, 129, 5, &r); + + memset(&auth, 0, sizeof(auth)); + auth.origin_epoch = 100; + auth.live_hwm_lsn = 0x2000; + auth.tt_generation = 1; + + /* epoch match => admissible */ + UT_ASSERT(cluster_undo_grant_admissible(&r, 5, auth, 100 /*local_epoch*/, InvalidXLogRecPtr)); + + /* epoch mismatch (reconfig between sample and use) => fail closed */ + UT_ASSERT( + !cluster_undo_grant_admissible(&r, 5, auth, 101 /*local_epoch != 100*/, InvalidXLogRecPtr)); + + /* missing live authority (hwm invalid) => fail closed, never guess */ + auth.live_hwm_lsn = InvalidXLogRecPtr; + UT_ASSERT(!cluster_undo_grant_admissible(&r, 5, auth, 100, InvalidXLogRecPtr)); +} + +int +main(void) +{ + UT_PLAN(9); + UT_RUN(test_undo_gcs_lookup_master_is_owner); + UT_RUN(test_undo_gcs_master_is_self); + UT_RUN(test_undo_gcs_path_runtime_shared_mode_branch); + UT_RUN(test_undo_gcs_path_materialized_never_migrates); + UT_RUN(test_undo_gcs_intent_for_owner); + UT_RUN(test_undo_gcs_grant_armed_gate); + UT_RUN(test_undo_gcs_grant_reader_pcm_contract); + UT_RUN(test_undo_gcs_grant_admissible_generation); + UT_RUN(test_undo_gcs_grant_admissible_epoch); + UT_DONE(); + return ut_failed_count == 0 ? 0 : 1; +} diff --git a/src/test/cluster_unit/test_cluster_undo_resid.c b/src/test/cluster_unit/test_cluster_undo_resid.c new file mode 100644 index 0000000000..9f85cf3328 --- /dev/null +++ b/src/test/cluster_unit/test_cluster_undo_resid.c @@ -0,0 +1,245 @@ +/*------------------------------------------------------------------------- + * + * test_cluster_undo_resid.c + * Unit tests for the shared-undo block resource identity pure layer + * (spec-5.22a D1). + * + * U2 pins the undo resid class byte (0xF9) above every PG LockTagType + * and distinct from every header-visible resid class (SQ 0xF0 / CF 0xF1 + * / HW 0xF2 / DL 0xF3 / TT 0xF4 / IR 0xF5 / KO 0xF6 / OID 0xF7 / + * RELMAP 0xF8), plus the backend-local raw-layout 0xF3 value that the + * header cross-assert net cannot see. The real owner-as-master data + * plane (grant / PI / shipping) is D2+; this file pins the identity + * contract only. + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * Portions Copyright (c) 2026, pgrac contributors + * + * Author: SqlRush + * + * IDENTIFICATION + * src/test/cluster_unit/test_cluster_undo_resid.c + * + * NOTES + * This is a pgrac-original file. + * Spec: spec-5.22a-undo-block-resource-identity.md (D1, §4.1) + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cluster/cluster_scn.h" /* SCN_MAX_VALID_NODE_ID */ +#include "cluster/cluster_undo_resid.h" +#include "storage/lock.h" + +#undef printf +#undef fprintf +#undef snprintf + +#include "unit_test.h" + +UT_DEFINE_GLOBALS(); + +void +ExceptionalCondition(const char *conditionName, const char *fileName, int lineNumber) +{ + printf("# Assert failed: %s at %s:%d\n", conditionName, fileName, lineNumber); + abort(); +} + +/* ====================================================================== + * U2 -- class byte: 0xF9, above PG lock types, no collision with any + * existing resid class (spec-5.22a §2.1) + * ====================================================================== */ +UT_TEST(test_undo_resid_class_byte) +{ + /* 0xF9 is the next free slot after the contiguous 0xF0-0xF8 run */ + UT_ASSERT_EQ(CLUSTER_UNDO_RESID_TYPE, 0xF9); + UT_ASSERT(CLUSTER_UNDO_RESID_TYPE > LOCKTAG_LAST_TYPE); + + /* distinct from every header-visible resid class */ + UT_ASSERT_NE(CLUSTER_UNDO_RESID_TYPE, CLUSTER_SQ_RESID_TYPE); + UT_ASSERT_NE(CLUSTER_UNDO_RESID_TYPE, CLUSTER_CF_RESID_TYPE); + UT_ASSERT_NE(CLUSTER_UNDO_RESID_TYPE, CLUSTER_HW_RESID_TYPE); + UT_ASSERT_NE(CLUSTER_UNDO_RESID_TYPE, CLUSTER_DL_RESID_TYPE); + UT_ASSERT_NE(CLUSTER_UNDO_RESID_TYPE, CLUSTER_TT_RESID_TYPE); + UT_ASSERT_NE(CLUSTER_UNDO_RESID_TYPE, CLUSTER_IR_RESID_TYPE); + UT_ASSERT_NE(CLUSTER_UNDO_RESID_TYPE, CLUSTER_KO_RESID_TYPE); + UT_ASSERT_NE(CLUSTER_UNDO_RESID_TYPE, CLUSTER_OID_RESID_TYPE); + UT_ASSERT_NE(CLUSTER_UNDO_RESID_TYPE, CLUSTER_RELMAP_RESID_TYPE); + + /* + * Namespace audit beyond the header net: CLUSTER_RAW_LAYOUT_RESID_TYPE + * (0xF3) is a backend-local define in cluster_shared_fs_block_device.c, + * not header-visible, so the header StaticAssert net cannot name it. + * Pin the raw value here so the undo class also stays clear of it. + * NB: 0xF3 itself is double-booked today (DL + raw layout); cleanup is + * a registered follow-up outside this spec. + */ + UT_ASSERT_NE(CLUSTER_UNDO_RESID_TYPE, 0xF3); +} + +/* ====================================================================== + * U1 -- encode/decode round-trip: all four identity dimensions survive + * ====================================================================== */ +UT_TEST(test_undo_resid_encode_decode_roundtrip) +{ + ClusterResId r; + int32 owner_node; + uint32 undo_segment; + uint32 block_no; + uint32 generation; + + memset(&r, 0xEE, sizeof(r)); + cluster_undo_resid_encode(2, 7, 129, 3, &r); + + UT_ASSERT_EQ(r.field1, 7); /* undo_segment */ + UT_ASSERT_EQ(r.field2, 129); /* block_no */ + UT_ASSERT_EQ(r.field3, 3); /* generation */ + UT_ASSERT_EQ(r.field4, 2); /* owner_node */ + UT_ASSERT_EQ(r.type, CLUSTER_UNDO_RESID_TYPE); + UT_ASSERT_EQ(r.lockmethodid, DEFAULT_LOCKMETHOD); + + owner_node = -1; + undo_segment = 0xDEADBEEF; + block_no = 0xDEADBEEF; + generation = 0xDEADBEEF; + cluster_undo_resid_decode(&r, &owner_node, &undo_segment, &block_no, &generation); + UT_ASSERT_EQ(owner_node, 2); + UT_ASSERT_EQ(undo_segment, 7); + UT_ASSERT_EQ(block_no, 129); + UT_ASSERT_EQ(generation, 3); +} + +/* block 0 (the segment TT header block) is a valid block_no: DATA and TT + * blocks share the one undo class */ +UT_TEST(test_undo_resid_tt_header_block_zero) +{ + ClusterResId r; + int32 owner_node; + uint32 undo_segment; + uint32 block_no; + uint32 generation; + + cluster_undo_resid_encode(1, 42, 0, 9, &r); + cluster_undo_resid_decode(&r, &owner_node, &undo_segment, &block_no, &generation); + UT_ASSERT_EQ(block_no, 0); + UT_ASSERT_EQ(undo_segment, 42); +} + +/* ====================================================================== + * U3 -- class discriminator: true for undo, false for other classes + * ====================================================================== */ +UT_TEST(test_undo_resid_is_undo) +{ + ClusterResId undo_r; + ClusterResId hw_r; + ClusterResId cf_r; + RelFileLocator rloc; + + cluster_undo_resid_encode(0, 1, 2, 0, &undo_r); + UT_ASSERT(cluster_undo_resid_is_undo(&undo_r)); + + /* a real HW resid (encoder linked) is not an undo resid */ + rloc.spcOid = 1663; + rloc.dbOid = 5; + rloc.relNumber = 16384; + cluster_hw_resid_encode(rloc, MAIN_FORKNUM, &hw_r); + UT_ASSERT(!cluster_undo_resid_is_undo(&hw_r)); + + /* CF resid constructed by hand (the CF encoder lives in a backend + * object; only the type byte matters to the discriminator) */ + memset(&cf_r, 0, sizeof(cf_r)); + cf_r.type = CLUSTER_CF_RESID_TYPE; + cf_r.lockmethodid = DEFAULT_LOCKMETHOD; + UT_ASSERT(!cluster_undo_resid_is_undo(&cf_r)); +} + +/* ====================================================================== + * U6 -- wire ABI: ClusterResId stays 16 bytes + * ====================================================================== */ +UT_TEST(test_undo_resid_wire_abi_16_bytes) +{ + UT_ASSERT_EQ(sizeof(ClusterResId), 16); +} + +/* ====================================================================== + * U7 -- owner_node boundary encoding (0 / 15 / SCN_MAX_VALID_NODE_ID) + * ====================================================================== */ +UT_TEST(test_undo_resid_owner_bounds) +{ + static const int32 owners[] = { 0, 15, SCN_MAX_VALID_NODE_ID }; + int i; + + for (i = 0; i < (int)lengthof(owners); i++) { + ClusterResId r; + int32 owner_node = -1; + uint32 undo_segment; + uint32 block_no; + uint32 generation; + + cluster_undo_resid_encode(owners[i], 3, 5, 1, &r); + cluster_undo_resid_decode(&r, &owner_node, &undo_segment, &block_no, &generation); + UT_ASSERT_EQ(owner_node, owners[i]); + } +} + +/* ====================================================================== + * U4 -- owner-as-master routing: the master IS the encoded owner_node, + * never a hash-derived node (spec-5.22a §3.1) + * ====================================================================== */ +UT_TEST(test_undo_resid_master_is_owner) +{ + ClusterResId r; + + cluster_undo_resid_encode(3, 11, 200, 5, &r); + UT_ASSERT_EQ(cluster_undo_resid_master(&r), 3); + + /* the master must not vary with the non-owner identity dimensions + * (a shard-hash master would) */ + cluster_undo_resid_encode(3, 9999, 123456, 42, &r); + UT_ASSERT_EQ(cluster_undo_resid_master(&r), 3); + + cluster_undo_resid_encode(0, 1, 1, 1, &r); + UT_ASSERT_EQ(cluster_undo_resid_master(&r), 0); + + cluster_undo_resid_encode(SCN_MAX_VALID_NODE_ID, 1, 1, 1, &r); + UT_ASSERT_EQ(cluster_undo_resid_master(&r), SCN_MAX_VALID_NODE_ID); +} + +/* ====================================================================== + * U5 -- anti-ABA generation predicate: mismatch means stale reference + * (caller must fail closed, never treat as a match) + * ====================================================================== */ +UT_TEST(test_undo_resid_generation_matches) +{ + ClusterResId r; + + cluster_undo_resid_encode(1, 7, 129, 3, &r); + UT_ASSERT(cluster_undo_resid_generation_matches(&r, 3)); + UT_ASSERT(!cluster_undo_resid_generation_matches(&r, 2)); + UT_ASSERT(!cluster_undo_resid_generation_matches(&r, 4)); + + /* generation 0 (never-reused segment) matches only 0 */ + cluster_undo_resid_encode(1, 7, 129, 0, &r); + UT_ASSERT(cluster_undo_resid_generation_matches(&r, 0)); + UT_ASSERT(!cluster_undo_resid_generation_matches(&r, 1)); +} + +int +main(void) +{ + UT_PLAN(8); + UT_RUN(test_undo_resid_class_byte); + UT_RUN(test_undo_resid_encode_decode_roundtrip); + UT_RUN(test_undo_resid_tt_header_block_zero); + UT_RUN(test_undo_resid_is_undo); + UT_RUN(test_undo_resid_wire_abi_16_bytes); + UT_RUN(test_undo_resid_owner_bounds); + UT_RUN(test_undo_resid_master_is_owner); + UT_RUN(test_undo_resid_generation_matches); + UT_DONE(); + return ut_failed_count == 0 ? 0 : 1; +}