diff --git a/src/backend/cluster/Makefile b/src/backend/cluster/Makefile index 5ea41237f7..c62e189809 100644 --- a/src/backend/cluster/Makefile +++ b/src/backend/cluster/Makefile @@ -183,6 +183,7 @@ OBJS = \ cluster_visibility_resolve.o \ cluster_visibility_verdict.o \ cluster_undo_record.o \ + cluster_undo_resid.o \ cluster_undo_retention.o \ cluster_undo_srf.o \ cluster_cr.o \ 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_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/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_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/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..063e5aa67a 100644 --- a/src/test/cluster_unit/Makefile +++ b/src/test/cluster_unit/Makefile @@ -86,7 +86,8 @@ 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 # 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 +179,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,$(TESTS)) # spec-2.4 D16: test_cluster_epoch links cluster_epoch.o standalone. # cluster_epoch.c references ShmemInitStruct + cluster_shmem_register_region @@ -1828,3 +1829,16 @@ 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 $@ 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_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; +}