From 5bc6c475e1477351237d27a26ed6e236c2d46f11 Mon Sep 17 00:00:00 2001 From: SqlRush Date: Mon, 10 Aug 2026 09:01:39 +0800 Subject: [PATCH 1/8] feat(cluster): add shared semantic admission gate Bind semantic admission to one coherent shared tuple and checked process-owned debt. Spec: spec-8.4-oracle-synchronous-consistent-read.md --- .../cluster/cluster_semantic_activation.c | 365 +++++++++++++++-- .../cluster/cluster_semantic_activation.h | 33 +- src/test/cluster_unit/Makefile | 2 + .../cluster_r4_activation_test_stubs.h | 79 ++++ .../test_cluster_r4_activation_fsm.c | 366 +++++++++++++++++- .../test_cluster_r4_activation_record.c | 32 +- .../cluster_unit/test_cluster_r4_lock_order.c | 2 + 7 files changed, 808 insertions(+), 71 deletions(-) create mode 100644 src/test/cluster_unit/cluster_r4_activation_test_stubs.h diff --git a/src/backend/cluster/cluster_semantic_activation.c b/src/backend/cluster/cluster_semantic_activation.c index d99e06ad04d..b0bce2ed6c3 100644 --- a/src/backend/cluster/cluster_semantic_activation.c +++ b/src/backend/cluster/cluster_semantic_activation.c @@ -16,16 +16,21 @@ */ #include "postgres.h" +#include "miscadmin.h" +#include "cluster/cluster_epoch.h" #include "cluster/cluster_semantic_activation.h" #include "cluster/storage/cluster_undo_block0.h" #include "port/atomics.h" #include "port/pg_crc32c.h" +#include "storage/ipc.h" #include "storage/shmem.h" #define CLUSTER_SEMANTIC_RECORD_MAGIC UINT32_C(0x50475341) #define CLUSTER_SEMANTIC_RECORD_VERSION 1 #define CLUSTER_SEMANTIC_RECORD_HEADER_LEN 104 #define CLUSTER_SEMANTIC_RECORD_CRC_OFFSET 96 +#define CLUSTER_SEMANTIC_ADMISSION_SNAPSHOT_TRIES 3 +#define CLUSTER_SEMANTIC_ADMISSION_COUNTER_TRIES 16 typedef struct ClusterSemanticRecordSample { bool readable; @@ -39,14 +44,37 @@ typedef struct ClusterSemanticActivationShmem { uint64 record_cas_expected_generation; uint64 record_cas_expected_source_feature_bitmap; uint8 record_cas_desired_bytes[CLUSTER_SEMANTIC_ACTIVATION_RECORD_BYTES]; + pg_atomic_uint64 admission_seq; + pg_atomic_uint64 active_bits; + pg_atomic_uint64 record_generation; + pg_atomic_uint64 formation_epoch; + pg_atomic_uint32 transition_closed; + pg_atomic_uint32 inflight[2][64]; } ClusterSemanticActivationShmem; +StaticAssertDecl(offsetof(ClusterSemanticActivationShmem, admission_seq) == 552, + "semantic admission sequence must follow the unchanged CAS mailbox"); +StaticAssertDecl(offsetof(ClusterSemanticActivationShmem, active_bits) == 560, + "semantic admission active bitmap offset must remain stable"); +StaticAssertDecl(offsetof(ClusterSemanticActivationShmem, record_generation) == 568, + "semantic admission record generation offset must remain stable"); +StaticAssertDecl(offsetof(ClusterSemanticActivationShmem, formation_epoch) == 576, + "semantic admission formation offset must remain stable"); +StaticAssertDecl(offsetof(ClusterSemanticActivationShmem, transition_closed) == 584, + "semantic admission closed flag offset must remain stable"); +StaticAssertDecl(offsetof(ClusterSemanticActivationShmem, inflight) == 588, + "semantic admission inflight offset must remain stable"); +StaticAssertDecl(sizeof(ClusterSemanticActivationShmem) == 1104, + "semantic activation shared state must retain its natural layout"); + static ClusterSemanticActivationShmem *SemanticActivationShmem = NULL; +static uint32 semantic_activation_local_inflight[2][64]; +static int semantic_activation_exit_hook_pid; static bool semantic_activation_record_cas_mailbox_submit( uint64 expected_generation, uint64 expected_source_feature_bitmap, - const uint8 desired_bytes[CLUSTER_SEMANTIC_ACTIVATION_RECORD_BYTES], - uint64 *out_request_seq) pg_attribute_unused(); + const uint8 desired_bytes[CLUSTER_SEMANTIC_ACTIVATION_RECORD_BYTES], uint64 *out_request_seq) + pg_attribute_unused(); static bool semantic_activation_record_cas_mailbox_poll_completion( uint64 request_seq, ClusterSemanticActivationResult *out_result) pg_attribute_unused(); @@ -129,14 +157,13 @@ typedef enum SemanticActivationEffect { SEMANTIC_ACTIVATION_EFFECT_DATA_WIRE = UINT32_C(64) } SemanticActivationEffect; -/* - * B2 is deliberately deployed with the source semantics open and the target - * semantics dormant. The later shared-memory/control-plane integration owns - * changing this snapshot; the dependency-light core only consumes it. - */ -static uint64 semantic_activation_active_bits = 0; -static uint64 semantic_activation_record_generation = 0; -static bool semantic_activation_transition_closed = false; +typedef struct SemanticActivationAdmissionSnapshot { + uint64 seq; + uint64 active_bits; + uint64 record_generation; + uint64 formation_epoch; + bool transition_closed; +} SemanticActivationAdmissionSnapshot; static uint16 semantic_activation_read_u16_le(const uint8 *bytes) @@ -477,47 +504,270 @@ static const ClusterSemanticActivationDescriptor r4_descriptor = { .open_target_admission = r4_stage_fail_closed, }; +static bool +semantic_activation_feature_index(uint64 feature_bit, int *feature_index) +{ + if (feature_bit != CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1 || feature_index == NULL) + return false; + *feature_index = 0; + return true; +} + +static bool +semantic_activation_snapshot(SemanticActivationAdmissionSnapshot *snapshot) +{ + int attempt; + + if (SemanticActivationShmem == NULL || snapshot == NULL) + return false; + + for (attempt = 0; attempt < CLUSTER_SEMANTIC_ADMISSION_SNAPSHOT_TRIES; attempt++) { + uint64 seq_before; + uint64 seq_after; + + seq_before = pg_atomic_read_u64(&SemanticActivationShmem->admission_seq); + if ((seq_before & UINT64_C(1)) != 0) + continue; + pg_read_barrier(); + snapshot->active_bits = pg_atomic_read_u64(&SemanticActivationShmem->active_bits); + snapshot->record_generation + = pg_atomic_read_u64(&SemanticActivationShmem->record_generation); + snapshot->formation_epoch = pg_atomic_read_u64(&SemanticActivationShmem->formation_epoch); + snapshot->transition_closed + = pg_atomic_read_u32(&SemanticActivationShmem->transition_closed) != 0; + pg_read_barrier(); + seq_after = pg_atomic_read_u64(&SemanticActivationShmem->admission_seq); + if (seq_before == seq_after && (seq_after & UINT64_C(1)) == 0) { + snapshot->seq = seq_after; + return true; + } + } + return false; +} + +static bool +semantic_activation_counter_increment(pg_atomic_uint32 *counter) +{ + int attempt; + uint32 observed; + + observed = pg_atomic_read_u32(counter); + for (attempt = 0; attempt < CLUSTER_SEMANTIC_ADMISSION_COUNTER_TRIES; attempt++) { + uint32 expected = observed; + + if (observed == UINT32_MAX) + return false; + if (pg_atomic_compare_exchange_u32(counter, &expected, observed + 1)) + return true; + observed = expected; + } + return false; +} + +static void +semantic_activation_counter_subtract(pg_atomic_uint32 *counter, uint32 amount) +{ + uint32 observed = pg_atomic_read_u32(counter); + + for (;;) { + uint32 expected = observed; + + if (amount == 0) + return; + if (observed < amount) + ereport(PANIC, (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("semantic activation admission debt underflow"), + errhint("Restart the failed process and retain the shared-memory image " + "for diagnosis."))); + if (pg_atomic_compare_exchange_u32(counter, &expected, observed - amount)) + return; + observed = expected; + } +} + +static void +semantic_activation_exit_cleanup(int code, Datum arg) +{ + int side; + int feature_index; + int registered_pid = DatumGetInt32(arg); + + (void)code; + if (registered_pid != MyProcPid || semantic_activation_exit_hook_pid != MyProcPid + || SemanticActivationShmem == NULL) + return; + + HOLD_INTERRUPTS(); + for (side = 0; side < 2; side++) { + for (feature_index = 0; feature_index < 64; feature_index++) { + uint32 local = semantic_activation_local_inflight[side][feature_index]; + uint32 shared; + + if (local == 0) + continue; + shared = pg_atomic_read_u32(&SemanticActivationShmem->inflight[side][feature_index]); + if (shared < local) + ereport( + PANIC, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("semantic activation exit debt is inconsistent"), + errhint("Retain the shared-memory image and restart the failed process."))); + } + } + for (side = 0; side < 2; side++) { + for (feature_index = 0; feature_index < 64; feature_index++) { + uint32 local = semantic_activation_local_inflight[side][feature_index]; + + if (local == 0) + continue; + semantic_activation_counter_subtract( + &SemanticActivationShmem->inflight[side][feature_index], local); + semantic_activation_local_inflight[side][feature_index] = 0; + } + } + semantic_activation_exit_hook_pid = 0; + RESUME_INTERRUPTS(); +} + +static bool +semantic_activation_ensure_exit_hook(void) +{ + if (MyProcPid <= 0) + return false; + if (semantic_activation_exit_hook_pid == MyProcPid) + return true; + + memset(semantic_activation_local_inflight, 0, sizeof(semantic_activation_local_inflight)); + on_shmem_exit(semantic_activation_exit_cleanup, Int32GetDatum(MyProcPid)); + semantic_activation_exit_hook_pid = MyProcPid; + return true; +} + +static void +semantic_activation_release_debt(ClusterSemanticAdmissionSide side, int feature_index) +{ + uint32 *local = &semantic_activation_local_inflight[side][feature_index]; + + if (*local == 0) + ereport(PANIC, (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("semantic activation local debt is missing"), + errhint("Retain the process and shared-memory state for diagnosis."))); + semantic_activation_counter_subtract(&SemanticActivationShmem->inflight[side][feature_index], + 1); + (*local)--; +} + ClusterSemanticAdmissionResult cluster_semantic_activation_enter(uint64 feature_bit, ClusterSemanticAdmissionSide side, ClusterSemanticAdmissionToken *token) { + SemanticActivationAdmissionSnapshot before; + SemanticActivationAdmissionSnapshot after; ClusterSemanticAdmissionResult result; + uint64 epoch_before; + uint64 epoch_after; + int feature_index; + bool incremented = false; if (token != NULL) memset(token, 0, sizeof(*token)); - if (token == NULL) + if (token == NULL || SemanticActivationShmem == NULL + || (side != CLUSTER_SEMANTIC_SOURCE_SIDE && side != CLUSTER_SEMANTIC_TARGET_SIDE) + || !semantic_activation_feature_index(feature_bit, &feature_index) + || !semantic_activation_ensure_exit_hook()) return CLUSTER_SEMANTIC_ADMISSION_CLOSED; + epoch_before = cluster_epoch_get_current(); + if (!semantic_activation_snapshot(&before)) + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; + if (before.formation_epoch != epoch_before) + return CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED; result = semantic_activation_admission_policy( - feature_bit, semantic_activation_active_bits, semantic_activation_transition_closed, side, - semantic_activation_record_generation, semantic_activation_record_generation); - if (result == CLUSTER_SEMANTIC_ADMISSION_OK) { - token->feature_bit = feature_bit; - token->record_generation = semantic_activation_record_generation; - token->side = (uint8)side; - token->entered = true; + feature_bit, before.active_bits, before.transition_closed, side, before.record_generation, + before.record_generation); + if (result != CLUSTER_SEMANTIC_ADMISSION_OK) + return result; + + HOLD_INTERRUPTS(); + if (semantic_activation_local_inflight[side][feature_index] != UINT32_MAX + && semantic_activation_counter_increment( + &SemanticActivationShmem->inflight[side][feature_index])) { + semantic_activation_local_inflight[side][feature_index]++; + incremented = true; } - return result; + pg_write_barrier(); + RESUME_INTERRUPTS(); + if (!incremented) + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; + + epoch_after = cluster_epoch_get_current(); + if (!semantic_activation_snapshot(&after)) + result = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + else if (before.seq != after.seq || before.record_generation != after.record_generation + || before.formation_epoch != after.formation_epoch || epoch_before != epoch_after + || after.formation_epoch != epoch_after) + result = CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED; + else + result = semantic_activation_admission_policy( + feature_bit, after.active_bits, after.transition_closed, side, before.record_generation, + after.record_generation); + if (result != CLUSTER_SEMANTIC_ADMISSION_OK) { + HOLD_INTERRUPTS(); + semantic_activation_release_debt(side, feature_index); + RESUME_INTERRUPTS(); + return result; + } + + token->feature_bit = feature_bit; + token->record_generation = before.record_generation; + token->formation_epoch = before.formation_epoch; + token->side = (uint8)side; + token->entered = true; + return CLUSTER_SEMANTIC_ADMISSION_OK; } bool cluster_semantic_activation_recheck(const ClusterSemanticAdmissionToken *token) { + SemanticActivationAdmissionSnapshot snapshot; + uint64 current_epoch; + int feature_index; + if (token == NULL || !token->entered) return false; + if (!semantic_activation_feature_index(token->feature_bit, &feature_index) + || token->side > CLUSTER_SEMANTIC_TARGET_SIDE || SemanticActivationShmem == NULL) + return false; + (void)feature_index; + current_epoch = cluster_epoch_get_current(); + if (!semantic_activation_snapshot(&snapshot) || snapshot.formation_epoch != current_epoch + || token->formation_epoch != current_epoch) + return false; return semantic_activation_admission_policy( - token->feature_bit, semantic_activation_active_bits, - semantic_activation_transition_closed, (ClusterSemanticAdmissionSide)token->side, - token->record_generation, semantic_activation_record_generation) + token->feature_bit, snapshot.active_bits, snapshot.transition_closed, + (ClusterSemanticAdmissionSide)token->side, token->record_generation, + snapshot.record_generation) == CLUSTER_SEMANTIC_ADMISSION_OK; } void cluster_semantic_activation_leave(ClusterSemanticAdmissionToken *token) { - if (token != NULL) - token->entered = false; + int feature_index; + + if (token == NULL || !token->entered) + return; + if (SemanticActivationShmem == NULL || token->side > CLUSTER_SEMANTIC_TARGET_SIDE + || !semantic_activation_feature_index(token->feature_bit, &feature_index)) + ereport(PANIC, + (errcode(ERRCODE_INTERNAL_ERROR), errmsg("semantic activation token is invalid"), + errhint("Retain the process and shared-memory state for diagnosis."))); + + HOLD_INTERRUPTS(); + semantic_activation_release_debt((ClusterSemanticAdmissionSide)token->side, feature_index); + memset(token, 0, sizeof(*token)); + RESUME_INTERRUPTS(); } Size @@ -530,6 +780,8 @@ void cluster_semantic_activation_shmem_init(void) { bool found; + int side; + int feature_index; SemanticActivationShmem = (ClusterSemanticActivationShmem *)ShmemInitStruct( "pgrac cluster semantic activation", cluster_semantic_activation_shmem_size(), &found); @@ -544,6 +796,15 @@ cluster_semantic_activation_shmem_init(void) SemanticActivationShmem->record_cas_expected_source_feature_bitmap = 0; memset(SemanticActivationShmem->record_cas_desired_bytes, 0, sizeof(SemanticActivationShmem->record_cas_desired_bytes)); + pg_atomic_init_u64(&SemanticActivationShmem->admission_seq, 0); + pg_atomic_init_u64(&SemanticActivationShmem->active_bits, 0); + pg_atomic_init_u64(&SemanticActivationShmem->record_generation, 0); + pg_atomic_init_u64(&SemanticActivationShmem->formation_epoch, 0); + pg_atomic_init_u32(&SemanticActivationShmem->transition_closed, 1); + for (side = 0; side < 2; side++) { + for (feature_index = 0; feature_index < 64; feature_index++) + pg_atomic_init_u32(&SemanticActivationShmem->inflight[side][feature_index], 0); + } } static bool @@ -600,8 +861,8 @@ cluster_semantic_activation_qvotec_poll_record_cas(ClusterSemanticActivationCasR } bool -cluster_semantic_activation_qvotec_complete_record_cas( - uint64 request_seq, ClusterSemanticActivationResult result) +cluster_semantic_activation_qvotec_complete_record_cas(uint64 request_seq, + ClusterSemanticActivationResult result) { uint64 current_request_seq; uint64 completion_seq; @@ -609,8 +870,7 @@ cluster_semantic_activation_qvotec_complete_record_cas( if (SemanticActivationShmem == NULL) return false; - current_request_seq - = pg_atomic_read_u64(&SemanticActivationShmem->record_cas_request_seq); + current_request_seq = pg_atomic_read_u64(&SemanticActivationShmem->record_cas_request_seq); completion_seq = pg_atomic_read_u64(&SemanticActivationShmem->record_cas_completion_seq); if (current_request_seq != request_seq || completion_seq == UINT64_MAX || completion_seq + 1 != request_seq) @@ -623,8 +883,8 @@ cluster_semantic_activation_qvotec_complete_record_cas( } static bool -semantic_activation_record_cas_mailbox_poll_completion( - uint64 request_seq, ClusterSemanticActivationResult *out_result) +semantic_activation_record_cas_mailbox_poll_completion(uint64 request_seq, + ClusterSemanticActivationResult *out_result) { if (SemanticActivationShmem == NULL || out_result == NULL || pg_atomic_read_u64(&SemanticActivationShmem->record_cas_completion_seq) != request_seq) @@ -745,7 +1005,50 @@ cluster_semantic_activation_record_decode(const uint8 bytes[512], void cluster_semantic_activation_lmon_tick(void) -{} +{ + uint64 seq; + uint64 active_bits; + uint64 generation; + uint64 formation_epoch; + + if (SemanticActivationShmem == NULL) + return; + seq = pg_atomic_read_u64(&SemanticActivationShmem->admission_seq); + if ((seq & UINT64_C(1)) != 0) { + if (seq == UINT64_MAX) + ereport(PANIC, (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("semantic activation admission sequence exhausted"), + errhint("Retain the shared-memory image and restart the cluster."))); + pg_atomic_write_u64(&SemanticActivationShmem->active_bits, 0); + pg_atomic_write_u64(&SemanticActivationShmem->record_generation, 0); + pg_atomic_write_u64(&SemanticActivationShmem->formation_epoch, cluster_epoch_get_current()); + pg_atomic_write_u32(&SemanticActivationShmem->transition_closed, 1); + pg_write_barrier(); + pg_atomic_write_u64(&SemanticActivationShmem->admission_seq, seq + 1); + return; + } + + active_bits = pg_atomic_read_u64(&SemanticActivationShmem->active_bits); + generation = pg_atomic_read_u64(&SemanticActivationShmem->record_generation); + formation_epoch = cluster_epoch_get_current(); + if (active_bits != 0 || generation != 0 + || (pg_atomic_read_u32(&SemanticActivationShmem->transition_closed) == 0 + && pg_atomic_read_u64(&SemanticActivationShmem->formation_epoch) == formation_epoch)) + return; + if (seq > UINT64_MAX - 2) + ereport(PANIC, (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("semantic activation admission sequence exhausted"), + errhint("Retain the shared-memory image and restart the cluster."))); + + pg_atomic_write_u64(&SemanticActivationShmem->admission_seq, seq + 1); + pg_write_barrier(); + pg_atomic_write_u64(&SemanticActivationShmem->active_bits, 0); + pg_atomic_write_u64(&SemanticActivationShmem->record_generation, 0); + pg_atomic_write_u64(&SemanticActivationShmem->formation_epoch, formation_epoch); + pg_atomic_write_u32(&SemanticActivationShmem->transition_closed, 0); + pg_write_barrier(); + pg_atomic_write_u64(&SemanticActivationShmem->admission_seq, seq + 2); +} ClusterSemanticActivationResult cluster_semantic_activation_submit(ClusterSemanticActivationAction action, diff --git a/src/include/cluster/cluster_semantic_activation.h b/src/include/cluster/cluster_semantic_activation.h index 86e6fa6ab6a..caa9f610837 100644 --- a/src/include/cluster/cluster_semantic_activation.h +++ b/src/include/cluster/cluster_semantic_activation.h @@ -35,10 +35,24 @@ typedef enum ClusterSemanticAdmissionResult { typedef struct ClusterSemanticAdmissionToken { uint64 feature_bit; uint64 record_generation; + uint64 formation_epoch; uint8 side; bool entered; } ClusterSemanticAdmissionToken; +StaticAssertDecl(offsetof(ClusterSemanticAdmissionToken, feature_bit) == 0, + "semantic admission feature offset must remain stable"); +StaticAssertDecl(offsetof(ClusterSemanticAdmissionToken, record_generation) == 8, + "semantic admission generation offset must remain stable"); +StaticAssertDecl(offsetof(ClusterSemanticAdmissionToken, formation_epoch) == 16, + "semantic admission formation offset must remain stable"); +StaticAssertDecl(offsetof(ClusterSemanticAdmissionToken, side) == 24, + "semantic admission side offset must remain stable"); +StaticAssertDecl(offsetof(ClusterSemanticAdmissionToken, entered) == 25, + "semantic admission entered offset must remain stable"); +StaticAssertDecl(sizeof(ClusterSemanticAdmissionToken) == 32, + "semantic admission token must remain 32 bytes"); + typedef enum ClusterSemanticActivationPhase { CLUSTER_SEMANTIC_PHASE_NONE = 0, CLUSTER_SEMANTIC_PHASE_PREPARE = 1, @@ -130,16 +144,15 @@ cluster_semantic_activation_register(const ClusterSemanticActivationDescriptor * extern bool cluster_semantic_activation_record_encode(const ClusterSemanticActivationRecord *record, uint8 bytes[512]); extern bool cluster_semantic_activation_record_decode(const uint8 bytes[512], - ClusterSemanticActivationRecord *record, - ClusterSemanticActivationRefusal *refusal); -extern ClusterSemanticActivationResult -cluster_semantic_activation_record_cas_write(uint64 expected_generation, - uint64 expected_source_feature_bitmap, - const uint8 bytes[512]); -extern bool cluster_semantic_activation_qvotec_poll_record_cas( - ClusterSemanticActivationCasRequest *out); -extern bool cluster_semantic_activation_qvotec_complete_record_cas( - uint64 request_seq, ClusterSemanticActivationResult result); + ClusterSemanticActivationRecord *record, + ClusterSemanticActivationRefusal *refusal); +extern ClusterSemanticActivationResult cluster_semantic_activation_record_cas_write( + uint64 expected_generation, uint64 expected_source_feature_bitmap, const uint8 bytes[512]); +extern bool +cluster_semantic_activation_qvotec_poll_record_cas(ClusterSemanticActivationCasRequest *out); +extern bool +cluster_semantic_activation_qvotec_complete_record_cas(uint64 request_seq, + ClusterSemanticActivationResult result); extern void cluster_semantic_activation_lmon_tick(void); extern ClusterSemanticActivationResult cluster_semantic_activation_submit(ClusterSemanticActivationAction action, diff --git a/src/test/cluster_unit/Makefile b/src/test/cluster_unit/Makefile index 54d128bb393..c78517a5e6f 100644 --- a/src/test/cluster_unit/Makefile +++ b/src/test/cluster_unit/Makefile @@ -1952,6 +1952,7 @@ CLUSTER_SEMANTIC_ACTIVATION_C = $(top_srcdir)/src/backend/cluster/cluster_semant CLUSTER_SEMANTIC_ACTIVATION_H = $(top_srcdir)/src/include/cluster/cluster_semantic_activation.h test_cluster_r4_activation_record: test_cluster_r4_activation_record.c unit_test.h \ + cluster_r4_activation_test_stubs.h \ $(CLUSTER_SEMANTIC_ACTIVATION_C) $(CLUSTER_SEMANTIC_ACTIVATION_H) \ $(CLUSTER_VERSION_O) $(CLUSTER_UNDO_BLOCK0_O) $(CC) $(CFLAGS) $(CPPFLAGS) $< \ @@ -1968,6 +1969,7 @@ test_cluster_r4_activation_fsm: test_cluster_r4_activation_fsm.c unit_test.h \ $(top_builddir)/src/port/libpgport_srv.a -o $@ test_cluster_r4_lock_order: test_cluster_r4_lock_order.c unit_test.h \ + cluster_r4_activation_test_stubs.h \ $(CLUSTER_SEMANTIC_ACTIVATION_C) $(CLUSTER_SEMANTIC_ACTIVATION_H) \ $(CLUSTER_VERSION_O) $(CLUSTER_UNDO_BLOCK0_O) $(CC) $(CFLAGS) $(CPPFLAGS) $< \ diff --git a/src/test/cluster_unit/cluster_r4_activation_test_stubs.h b/src/test/cluster_unit/cluster_r4_activation_test_stubs.h new file mode 100644 index 00000000000..a9fa9b60f57 --- /dev/null +++ b/src/test/cluster_unit/cluster_r4_activation_test_stubs.h @@ -0,0 +1,79 @@ +/*------------------------------------------------------------------------- + * + * cluster_r4_activation_test_stubs.h + * Process-lifecycle stubs for dependency-light semantic activation tests. + * + * 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/cluster_r4_activation_test_stubs.h + * + *------------------------------------------------------------------------- + */ +#ifndef CLUSTER_R4_ACTIVATION_TEST_STUBS_H +#define CLUSTER_R4_ACTIVATION_TEST_STUBS_H + +#include "storage/ipc.h" + +int MyProcPid = 101; +volatile sig_atomic_t InterruptPending = false; +volatile uint32 InterruptHoldoffCount = 0; +volatile uint32 QueryCancelHoldoffCount = 0; +volatile uint32 CritSectionCount = 0; + +void ProcessInterrupts(void); + +void +ProcessInterrupts(void) +{} + +uint64 +cluster_epoch_get_current(void) +{ + return 0; +} + +void +on_shmem_exit(pg_on_exit_callback function pg_attribute_unused(), Datum arg pg_attribute_unused()) +{} + +bool +errstart(int elevel pg_attribute_unused(), const char *domain pg_attribute_unused()) +{ + return false; +} + +bool +errstart_cold(int elevel pg_attribute_unused(), const char *domain pg_attribute_unused()) +{ + return false; +} + +void +errfinish(const char *filename pg_attribute_unused(), int lineno pg_attribute_unused(), + const char *funcname pg_attribute_unused()) +{} + +int +errcode(int sqlerrcode pg_attribute_unused()) +{ + return 0; +} + +int +errmsg(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +int +errhint(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +#endif /* CLUSTER_R4_ACTIVATION_TEST_STUBS_H */ diff --git a/src/test/cluster_unit/test_cluster_r4_activation_fsm.c b/src/test/cluster_unit/test_cluster_r4_activation_fsm.c index 34b9f11ac27..5de0071cc60 100644 --- a/src/test/cluster_unit/test_cluster_r4_activation_fsm.c +++ b/src/test/cluster_unit/test_cluster_r4_activation_fsm.c @@ -7,14 +7,100 @@ */ #include "postgres.h" +#include "cluster/cluster_epoch.h" #include "cluster/cluster_semantic_activation.h" +#include "port/atomics.h" +#include "storage/ipc.h" #include "storage/shmem.h" +#define TEST_SEMANTIC_SHMEM_BYTES 1104 +#define TEST_GATE_SEQ_OFFSET 552 +#define TEST_GATE_ACTIVE_BITS_OFFSET 560 +#define TEST_GATE_RECORD_GENERATION_OFFSET 568 +#define TEST_GATE_FORMATION_EPOCH_OFFSET 576 +#define TEST_GATE_CLOSED_OFFSET 584 +#define TEST_GATE_INFLIGHT_OFFSET 588 + +typedef union TestSemanticShmemStorage { + pg_atomic_uint64 align; + uint8 bytes[TEST_SEMANTIC_SHMEM_BYTES]; +} TestSemanticShmemStorage; + +static TestSemanticShmemStorage test_semantic_shmem; +static bool test_shmem_found; +static Size test_shmem_requested_size; +static pg_on_exit_callback test_exit_callback; +static Datum test_exit_callback_arg; +static int test_exit_registration_count; +static uint64 test_current_epoch = 7; + +int MyProcPid = 101; +volatile sig_atomic_t InterruptPending = false; +volatile uint32 InterruptHoldoffCount = 0; +volatile uint32 QueryCancelHoldoffCount = 0; +volatile uint32 CritSectionCount = 0; + +void ProcessInterrupts(void); + void * -ShmemInitStruct(const char *name pg_attribute_unused(), Size size pg_attribute_unused(), - bool *foundPtr pg_attribute_unused()) +ShmemInitStruct(const char *name pg_attribute_unused(), Size size, bool *foundPtr) +{ + test_shmem_requested_size = size; + *foundPtr = test_shmem_found; + return test_semantic_shmem.bytes; +} + +uint64 +cluster_epoch_get_current(void) +{ + return test_current_epoch; +} + +void +on_shmem_exit(pg_on_exit_callback function, Datum arg) +{ + test_exit_callback = function; + test_exit_callback_arg = arg; + test_exit_registration_count++; +} + +void +ProcessInterrupts(void) +{} + +bool +errstart(int elevel pg_attribute_unused(), const char *domain pg_attribute_unused()) +{ + return false; +} + +bool +errstart_cold(int elevel pg_attribute_unused(), const char *domain pg_attribute_unused()) +{ + return false; +} + +void +errfinish(const char *filename pg_attribute_unused(), int lineno pg_attribute_unused(), + const char *funcname pg_attribute_unused()) +{} + +int +errcode(int sqlerrcode pg_attribute_unused()) +{ + return 0; +} + +int +errmsg(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +int +errhint(const char *fmt pg_attribute_unused(), ...) { - return NULL; + return 0; } /* Exercise the real product-local policy helpers without exporting a test API. */ @@ -36,6 +122,62 @@ ExceptionalCondition(const char *condition_name pg_attribute_unused(), abort(); } +static pg_atomic_uint64 * +test_gate_u64(Size offset) +{ + return (pg_atomic_uint64 *)(test_semantic_shmem.bytes + offset); +} + +static pg_atomic_uint32 * +test_gate_u32(Size offset) +{ + return (pg_atomic_uint32 *)(test_semantic_shmem.bytes + offset); +} + +static pg_atomic_uint32 * +test_gate_inflight(ClusterSemanticAdmissionSide side, int feature_index) +{ + return test_gate_u32(TEST_GATE_INFLIGHT_OFFSET + + ((Size)side * 64 + (Size)feature_index) * sizeof(pg_atomic_uint32)); +} + +static void +test_gate_reset(void) +{ + memset(&test_semantic_shmem, 0, sizeof(test_semantic_shmem)); + test_shmem_found = false; + test_shmem_requested_size = 0; + test_exit_callback = NULL; + test_exit_callback_arg = (Datum)0; + test_exit_registration_count = 0; + test_current_epoch = 7; + MyProcPid = 101; + SemanticActivationShmem = NULL; + memset(semantic_activation_local_inflight, 0, sizeof(semantic_activation_local_inflight)); + semantic_activation_exit_hook_pid = 0; + cluster_semantic_activation_shmem_init(); +} + +static void +test_gate_publish(uint64 seq, uint64 active_bits, uint64 generation, uint64 formation_epoch, + bool closed) +{ + pg_atomic_write_u64(test_gate_u64(TEST_GATE_SEQ_OFFSET), seq); + pg_atomic_write_u64(test_gate_u64(TEST_GATE_ACTIVE_BITS_OFFSET), active_bits); + pg_atomic_write_u64(test_gate_u64(TEST_GATE_RECORD_GENERATION_OFFSET), generation); + pg_atomic_write_u64(test_gate_u64(TEST_GATE_FORMATION_EPOCH_OFFSET), formation_epoch); + pg_atomic_write_u32(test_gate_u32(TEST_GATE_CLOSED_OFFSET), closed ? 1 : 0); +} + +static uint64 +test_token_formation_epoch(const ClusterSemanticAdmissionToken *token) +{ + uint64 formation_epoch; + + memcpy(&formation_epoch, ((const uint8 *)token) + 16, sizeof(formation_epoch)); + return formation_epoch; +} + static SemanticActivationAckTuple valid_ack(void) { @@ -557,6 +699,8 @@ UT_TEST(test_95_dormant_target_enter_has_no_token) { ClusterSemanticAdmissionToken token; + test_gate_reset(); + test_gate_publish(2, 0, 0, test_current_epoch, false); memset(&token, 0xa5, sizeof(token)); UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, CLUSTER_SEMANTIC_TARGET_SIDE, &token), @@ -568,6 +712,8 @@ UT_TEST(test_96_source_token_recheck_and_leave_are_generation_scoped) { ClusterSemanticAdmissionToken token; + test_gate_reset(); + test_gate_publish(2, 0, 0, test_current_epoch, false); memset(&token, 0, sizeof(token)); UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, CLUSTER_SEMANTIC_SOURCE_SIDE, &token), @@ -587,16 +733,24 @@ UT_TEST(test_97_old_epoch_completion_is_inert_and_requires_revalidation) SemanticActivationAckTuple expected = valid_ack(); SemanticActivationAckTuple observed = expected; uint8 desired[CLUSTER_SEMANTIC_ACTIVATION_RECORD_BYTES]; - uint64 before_active_bits = semantic_activation_active_bits; - uint64 before_generation = semantic_activation_record_generation; - bool before_closed = semantic_activation_transition_closed; + uint64 before_active_bits; + uint64 before_generation; + bool before_closed; uint64 seq = 0; memset(&shmem, 0, sizeof(shmem)); pg_atomic_init_u64(&shmem.record_cas_request_seq, 0); pg_atomic_init_u64(&shmem.record_cas_completion_seq, 0); pg_atomic_init_u32(&shmem.record_cas_result, CLUSTER_SEMANTIC_ACTIVATION_BAD_STATE); + pg_atomic_init_u64(&shmem.admission_seq, 0); + pg_atomic_init_u64(&shmem.active_bits, CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1); + pg_atomic_init_u64(&shmem.record_generation, 7); + pg_atomic_init_u64(&shmem.formation_epoch, test_current_epoch); + pg_atomic_init_u32(&shmem.transition_closed, 1); SemanticActivationShmem = &shmem; + before_active_bits = pg_atomic_read_u64(&shmem.active_bits); + before_generation = pg_atomic_read_u64(&shmem.record_generation); + before_closed = pg_atomic_read_u32(&shmem.transition_closed) != 0; memset(&desired_record, 0, sizeof(desired_record)); desired_record.source_feature_bitmap = UINT64_C(0x11); @@ -607,8 +761,7 @@ UT_TEST(test_97_old_epoch_completion_is_inert_and_requires_revalidation) desired_record.coordinator_incarnation = expected.admitted_incarnation; desired_record.phase = CLUSTER_SEMANTIC_PHASE_COMMIT; UT_ASSERT(cluster_semantic_activation_record_encode(&desired_record, desired)); - UT_ASSERT(semantic_activation_record_cas_mailbox_submit(7, UINT64_C(0x11), desired, - &seq)); + UT_ASSERT(semantic_activation_record_cas_mailbox_submit(7, UINT64_C(0x11), desired, &seq)); UT_ASSERT(cluster_semantic_activation_qvotec_poll_record_cas(&request)); UT_ASSERT(cluster_semantic_activation_qvotec_complete_record_cas( request.request_seq, CLUSTER_SEMANTIC_ACTIVATION_OK)); @@ -616,18 +769,195 @@ UT_TEST(test_97_old_epoch_completion_is_inert_and_requires_revalidation) UT_ASSERT_EQ(result, CLUSTER_SEMANTIC_ACTIVATION_OK); cluster_semantic_activation_lmon_tick(); - UT_ASSERT_EQ(semantic_activation_active_bits, before_active_bits); - UT_ASSERT_EQ(semantic_activation_record_generation, before_generation); - UT_ASSERT_EQ(semantic_activation_transition_closed, before_closed); + UT_ASSERT_EQ(pg_atomic_read_u64(&shmem.active_bits), before_active_bits); + UT_ASSERT_EQ(pg_atomic_read_u64(&shmem.record_generation), before_generation); + UT_ASSERT_EQ(pg_atomic_read_u32(&shmem.transition_closed) != 0, before_closed); observed.transition_epoch = desired_record.transition_epoch; UT_ASSERT(!semantic_activation_ack_matches(&observed, &expected)); SemanticActivationShmem = NULL; } +UT_TEST(test_98_admission_token_has_frozen_natural_layout) +{ + ClusterSemanticAdmissionToken token; + + memset(&token, 0, sizeof(token)); + UT_ASSERT_EQ(sizeof(token), 32); + UT_ASSERT_EQ((Size)((char *)&token.feature_bit - (char *)&token), 0); + UT_ASSERT_EQ((Size)((char *)&token.record_generation - (char *)&token), 8); + UT_ASSERT_EQ((Size)((char *)&token.side - (char *)&token), 24); + UT_ASSERT_EQ((Size)((char *)&token.entered - (char *)&token), 25); +} + +UT_TEST(test_99_shared_gate_layout_and_bootstrap_are_fail_closed) +{ + test_gate_reset(); + UT_ASSERT_EQ(test_shmem_requested_size, TEST_SEMANTIC_SHMEM_BYTES); + UT_ASSERT_EQ(pg_atomic_read_u64(test_gate_u64(TEST_GATE_SEQ_OFFSET)), 0); + UT_ASSERT_EQ(pg_atomic_read_u64(test_gate_u64(TEST_GATE_ACTIVE_BITS_OFFSET)), 0); + UT_ASSERT_EQ(pg_atomic_read_u64(test_gate_u64(TEST_GATE_RECORD_GENERATION_OFFSET)), 0); + UT_ASSERT_EQ(pg_atomic_read_u64(test_gate_u64(TEST_GATE_FORMATION_EPOCH_OFFSET)), 0); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_u32(TEST_GATE_CLOSED_OFFSET)), 1); +} + +UT_TEST(test_100_source_enter_owns_shared_debt_and_epoch_token) +{ + ClusterSemanticAdmissionToken token; + + test_gate_reset(); + test_gate_publish(2, 0, 11, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &token), + CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT(token.entered); + UT_ASSERT_EQ(test_token_formation_epoch(&token), test_current_epoch); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 1); + UT_ASSERT_EQ(test_exit_registration_count, 1); +} + +UT_TEST(test_101_active_source_refuses_before_debt) +{ + ClusterSemanticAdmissionToken token; + + test_gate_reset(); + test_gate_publish(2, CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, 12, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &token), + CLUSTER_SEMANTIC_ADMISSION_SOURCE_DORMANT); + UT_ASSERT(!token.entered); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 0); +} + +UT_TEST(test_102_inactive_target_refuses_before_debt) +{ + ClusterSemanticAdmissionToken token; + + test_gate_reset(); + test_gate_publish(2, 0, 13, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_TARGET_SIDE, &token), + CLUSTER_SEMANTIC_ADMISSION_TARGET_DISABLED); + UT_ASSERT(!token.entered); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_TARGET_SIDE, 0)), 0); +} + +UT_TEST(test_103_epoch_drift_invalidates_recheck_without_losing_debt) +{ + ClusterSemanticAdmissionToken token; + + test_gate_reset(); + test_gate_publish(2, 0, 14, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &token), + CLUSTER_SEMANTIC_ADMISSION_OK); + test_current_epoch++; + UT_ASSERT(!cluster_semantic_activation_recheck(&token)); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 1); + cluster_semantic_activation_leave(&token); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 0); +} + +UT_TEST(test_104_close_invalidates_recheck_and_leave_balances_once) +{ + ClusterSemanticAdmissionToken token; + + test_gate_reset(); + test_gate_publish(2, 0, 15, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &token), + CLUSTER_SEMANTIC_ADMISSION_OK); + test_gate_publish(4, 0, 15, test_current_epoch, true); + UT_ASSERT(!cluster_semantic_activation_recheck(&token)); + cluster_semantic_activation_leave(&token); + cluster_semantic_activation_leave(&token); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 0); +} + +UT_TEST(test_105_pid_change_discards_inherited_local_ledger_only) +{ + ClusterSemanticAdmissionToken parent_token; + ClusterSemanticAdmissionToken child_token; + + test_gate_reset(); + test_gate_publish(2, 0, 16, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &parent_token), + CLUSTER_SEMANTIC_ADMISSION_OK); + MyProcPid = 202; + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &child_token), + CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT_EQ(test_exit_registration_count, 2); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 2); + cluster_semantic_activation_leave(&child_token); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 1); +} + +UT_TEST(test_106_exit_hook_drains_both_side_ledgers) +{ + ClusterSemanticAdmissionToken source_token; + ClusterSemanticAdmissionToken target_token; + + test_gate_reset(); + test_gate_publish(2, 0, 17, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &source_token), + CLUSTER_SEMANTIC_ADMISSION_OK); + test_gate_publish(4, CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, 18, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_TARGET_SIDE, &target_token), + CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT_NOT_NULL(test_exit_callback); + if (test_exit_callback != NULL) + test_exit_callback(0, test_exit_callback_arg); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 0); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_TARGET_SIDE, 0)), 0); +} + +UT_TEST(test_107_odd_snapshot_is_bounded_closed_without_debt) +{ + ClusterSemanticAdmissionToken token; + + test_gate_reset(); + test_gate_publish(3, 0, 19, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &token), + CLUSTER_SEMANTIC_ADMISSION_CLOSED); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 0); +} + +UT_TEST(test_108_nonregistered_feature_is_closed_without_debt) +{ + ClusterSemanticAdmissionToken token; + + test_gate_reset(); + test_gate_publish(2, 0, 20, test_current_epoch, false); + UT_ASSERT_EQ( + cluster_semantic_activation_enter(UINT64_C(1) << 7, CLUSTER_SEMANTIC_SOURCE_SIDE, &token), + CLUSTER_SEMANTIC_ADMISSION_CLOSED); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 7)), 0); +} + +UT_TEST(test_109_lmon_legacy_zero_publish_opens_source_atomically) +{ + ClusterSemanticAdmissionToken token; + + test_gate_reset(); + cluster_semantic_activation_lmon_tick(); + UT_ASSERT_EQ(pg_atomic_read_u64(test_gate_u64(TEST_GATE_SEQ_OFFSET)), 2); + UT_ASSERT_EQ(pg_atomic_read_u64(test_gate_u64(TEST_GATE_FORMATION_EPOCH_OFFSET)), + test_current_epoch); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_u32(TEST_GATE_CLOSED_OFFSET)), 0); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &token), + CLUSTER_SEMANTIC_ADMISSION_OK); + cluster_semantic_activation_leave(&token); +} + int main(void) { - UT_PLAN(97); + UT_PLAN(109); UT_RUN(test_01_feature_bit_is_one); UT_RUN(test_02_required_hello_caps_are_frozen); UT_RUN(test_03_action_values_are_frozen); @@ -725,6 +1055,18 @@ main(void) UT_RUN(test_95_dormant_target_enter_has_no_token); UT_RUN(test_96_source_token_recheck_and_leave_are_generation_scoped); UT_RUN(test_97_old_epoch_completion_is_inert_and_requires_revalidation); + UT_RUN(test_98_admission_token_has_frozen_natural_layout); + UT_RUN(test_99_shared_gate_layout_and_bootstrap_are_fail_closed); + UT_RUN(test_100_source_enter_owns_shared_debt_and_epoch_token); + UT_RUN(test_101_active_source_refuses_before_debt); + UT_RUN(test_102_inactive_target_refuses_before_debt); + UT_RUN(test_103_epoch_drift_invalidates_recheck_without_losing_debt); + UT_RUN(test_104_close_invalidates_recheck_and_leave_balances_once); + UT_RUN(test_105_pid_change_discards_inherited_local_ledger_only); + UT_RUN(test_106_exit_hook_drains_both_side_ledgers); + UT_RUN(test_107_odd_snapshot_is_bounded_closed_without_debt); + UT_RUN(test_108_nonregistered_feature_is_closed_without_debt); + UT_RUN(test_109_lmon_legacy_zero_publish_opens_source_atomically); UT_DONE(); return ut_failed_count == 0 ? 0 : 1; } diff --git a/src/test/cluster_unit/test_cluster_r4_activation_record.c b/src/test/cluster_unit/test_cluster_r4_activation_record.c index 003ab1a7eb1..4b550fdfa4b 100644 --- a/src/test/cluster_unit/test_cluster_r4_activation_record.c +++ b/src/test/cluster_unit/test_cluster_r4_activation_record.c @@ -15,6 +15,8 @@ #include "port/pg_crc32c.h" #include "storage/shmem.h" +#include "cluster_r4_activation_test_stubs.h" + void * ShmemInitStruct(const char *name pg_attribute_unused(), Size size pg_attribute_unused(), bool *foundPtr pg_attribute_unused()) @@ -663,8 +665,7 @@ UT_TEST(test_49_record_cas_mailbox_exact_sequence_lifecycle) { ClusterSemanticActivationShmem shmem; ClusterSemanticActivationCasRequest request; - ClusterSemanticActivationRecord desired_record - = valid_record(CLUSTER_SEMANTIC_PHASE_COMMIT, 8); + ClusterSemanticActivationRecord desired_record = valid_record(CLUSTER_SEMANTIC_PHASE_COMMIT, 8); ClusterSemanticActivationResult result = CLUSTER_SEMANTIC_ACTIVATION_BAD_STATE; uint8 desired[CLUSTER_SEMANTIC_ACTIVATION_RECORD_BYTES]; uint64 seq = UINT64_MAX; @@ -675,19 +676,16 @@ UT_TEST(test_49_record_cas_mailbox_exact_sequence_lifecycle) pg_atomic_init_u32(&shmem.record_cas_result, CLUSTER_SEMANTIC_ACTIVATION_BAD_STATE); SemanticActivationShmem = NULL; UT_ASSERT(encode(desired_record, desired)); - UT_ASSERT(!semantic_activation_record_cas_mailbox_submit(7, UINT64_C(0x11), desired, - &seq)); + UT_ASSERT(!semantic_activation_record_cas_mailbox_submit(7, UINT64_C(0x11), desired, &seq)); SemanticActivationShmem = &shmem; UT_ASSERT(!semantic_activation_record_cas_mailbox_submit(7, UINT64_C(0x11), NULL, &seq)); UT_ASSERT(!semantic_activation_record_cas_mailbox_submit(7, UINT64_C(0x11), desired, NULL)); - UT_ASSERT(semantic_activation_record_cas_mailbox_submit(7, UINT64_C(0x11), desired, - &seq)); + UT_ASSERT(semantic_activation_record_cas_mailbox_submit(7, UINT64_C(0x11), desired, &seq)); UT_ASSERT_EQ(seq, 1); UT_ASSERT_EQ(pg_atomic_read_u64(&shmem.record_cas_request_seq), 1); UT_ASSERT_EQ(pg_atomic_read_u64(&shmem.record_cas_completion_seq), 0); - UT_ASSERT(!semantic_activation_record_cas_mailbox_submit(8, UINT64_C(0x22), desired, - &seq)); + UT_ASSERT(!semantic_activation_record_cas_mailbox_submit(8, UINT64_C(0x22), desired, &seq)); memset(&request, 0, sizeof(request)); UT_ASSERT(cluster_semantic_activation_qvotec_poll_record_cas(&request)); @@ -695,26 +693,24 @@ UT_TEST(test_49_record_cas_mailbox_exact_sequence_lifecycle) UT_ASSERT_EQ(request.expected_generation, 7); UT_ASSERT_EQ(request.expected_source_feature_bitmap, UINT64_C(0x11)); UT_ASSERT_EQ(memcmp(request.desired_bytes, desired, sizeof(desired)), 0); - UT_ASSERT(!cluster_semantic_activation_qvotec_complete_record_cas( - 0, CLUSTER_SEMANTIC_ACTIVATION_OK)); - UT_ASSERT(!cluster_semantic_activation_qvotec_complete_record_cas( - 2, CLUSTER_SEMANTIC_ACTIVATION_OK)); + UT_ASSERT( + !cluster_semantic_activation_qvotec_complete_record_cas(0, CLUSTER_SEMANTIC_ACTIVATION_OK)); + UT_ASSERT( + !cluster_semantic_activation_qvotec_complete_record_cas(2, CLUSTER_SEMANTIC_ACTIVATION_OK)); UT_ASSERT_EQ(pg_atomic_read_u64(&shmem.record_cas_completion_seq), 0); UT_ASSERT(cluster_semantic_activation_qvotec_complete_record_cas( 1, CLUSTER_SEMANTIC_ACTIVATION_RECORD_CONFLICT)); - UT_ASSERT(!cluster_semantic_activation_qvotec_complete_record_cas( - 1, CLUSTER_SEMANTIC_ACTIVATION_OK)); + UT_ASSERT( + !cluster_semantic_activation_qvotec_complete_record_cas(1, CLUSTER_SEMANTIC_ACTIVATION_OK)); UT_ASSERT(semantic_activation_record_cas_mailbox_poll_completion(1, &result)); UT_ASSERT_EQ(result, CLUSTER_SEMANTIC_ACTIVATION_RECORD_CONFLICT); UT_ASSERT(!semantic_activation_record_cas_mailbox_poll_completion(2, &result)); - UT_ASSERT(semantic_activation_record_cas_mailbox_submit(8, UINT64_C(0x22), desired, - &seq)); + UT_ASSERT(semantic_activation_record_cas_mailbox_submit(8, UINT64_C(0x22), desired, &seq)); UT_ASSERT_EQ(seq, 2); pg_atomic_write_u64(&shmem.record_cas_request_seq, UINT64_MAX); pg_atomic_write_u64(&shmem.record_cas_completion_seq, UINT64_MAX); - UT_ASSERT(!semantic_activation_record_cas_mailbox_submit(8, UINT64_C(0x22), desired, - &seq)); + UT_ASSERT(!semantic_activation_record_cas_mailbox_submit(8, UINT64_C(0x22), desired, &seq)); UT_ASSERT_EQ(pg_atomic_read_u64(&shmem.record_cas_request_seq), UINT64_MAX); UT_ASSERT_EQ(pg_atomic_read_u64(&shmem.record_cas_completion_seq), UINT64_MAX); SemanticActivationShmem = NULL; diff --git a/src/test/cluster_unit/test_cluster_r4_lock_order.c b/src/test/cluster_unit/test_cluster_r4_lock_order.c index dadf1584a90..072e5cbd0d8 100644 --- a/src/test/cluster_unit/test_cluster_r4_lock_order.c +++ b/src/test/cluster_unit/test_cluster_r4_lock_order.c @@ -10,6 +10,8 @@ #include "cluster/cluster_semantic_activation.h" #include "storage/shmem.h" +#include "cluster_r4_activation_test_stubs.h" + void * ShmemInitStruct(const char *name pg_attribute_unused(), Size size pg_attribute_unused(), bool *foundPtr pg_attribute_unused()) From 300ff8c55d465951d558f6995c0e94417aaeb7c5 Mon Sep 17 00:00:00 2001 From: SqlRush Date: Mon, 10 Aug 2026 09:10:44 +0800 Subject: [PATCH 2/8] feat(cluster): gate legacy CR source dispatch Spec: spec-8.4-oracle-synchronous-consistent-read.md --- src/backend/cluster/cluster_cr.c | 16 ++++++--- src/backend/cluster/cluster_gcs_block.c | 47 +++++++++++++++++++++++-- src/include/cluster/cluster_cr_server.h | 25 +++++++++---- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/backend/cluster/cluster_cr.c b/src/backend/cluster/cluster_cr.c index 74429675aba..949c6ed1767 100644 --- a/src/backend/cluster/cluster_cr.c +++ b/src/backend/cluster/cluster_cr.c @@ -1540,19 +1540,27 @@ cr_construct_from_copy(char *dst_page, SCN read_scn, RelFileLocator cur_locator, NodeId head_origin = uba_origin_node_id(chains[0].undo_segment_head); if (cluster_cr_coordinator_classify_origin(head_origin) == CR_COORD_ORIGIN_RUNTIME_REMOTE) { - bool partial = false; + ClusterR4SourceCrRequest source_request; + ClusterR4SourceCrResult source_result; BufferTag tag; InitBufferTag(&tag, &cur_locator, cur_fork, cur_block); - if (!cluster_gcs_block_cr_fetch_and_wait(tag, read_scn, (int32)head_origin, dst_page, - &partial)) { + memset(&source_request, 0, sizeof(source_request)); + source_request.tag = tag; + source_request.read_scn = read_scn; + source_request.origin_node = (int32)head_origin; + source_request.dst_page = dst_page; + if (cluster_r4_source_cr_dispatch(CLUSTER_R4_SOURCE_CR_FETCH, &source_request, + &source_result) + != CLUSTER_SEMANTIC_ADMISSION_OK + || !source_result.fetched) { if (CRShared != NULL) pg_atomic_fetch_add_u64(&CRShared->cr_remote_failed_count, 1); /* Unchanged spec-5.57 refusal: 53R9G + coordinator counters. */ cr_coordinator_refuse_runtime_remote((int)head_origin); } - if (!partial) { + if (!source_result.partial) { /* FULL: dst holds the origin-finished CR page (it already ran * prune + walk + durable resolve there). */ if (CRShared != NULL) { diff --git a/src/backend/cluster/cluster_gcs_block.c b/src/backend/cluster/cluster_gcs_block.c index 305d482a10b..992ca5365bd 100644 --- a/src/backend/cluster/cluster_gcs_block.c +++ b/src/backend/cluster/cluster_gcs_block.c @@ -4040,9 +4040,9 @@ cluster_gcs_local_master_read_image_and_wait(BufferDesc *buf, const PcmAuthority * is NEVER installed as current and never flushed; it exists * only in the caller's CR destination. */ -bool -cluster_gcs_block_cr_fetch_and_wait(BufferTag tag, SCN read_scn, int32 origin_node, char *dst_page, - bool *out_partial) +static bool +cluster_gcs_block_cr_fetch_and_wait_raw(BufferTag tag, SCN read_scn, int32 origin_node, + char *dst_page, bool *out_partial) { ClusterGcsBlockOutstandingSlot *slot; uint64 request_id = 0; @@ -4152,6 +4152,47 @@ cluster_gcs_block_cr_fetch_and_wait(BufferTag tag, SCN read_scn, int32 origin_no return fetched; /* false -> caller keeps the unchanged 53R9G refusal */ } +ClusterSemanticAdmissionResult +cluster_r4_source_cr_dispatch(ClusterR4SourceCrOp op, const ClusterR4SourceCrRequest *request, + ClusterR4SourceCrResult *result) +{ + ClusterSemanticAdmissionToken token; + ClusterSemanticAdmissionResult admission; + ClusterR4SourceCrResult local_result = { 0 }; + + if (result != NULL) + memset(result, 0, sizeof(*result)); + admission = cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &token); + if (admission != CLUSTER_SEMANTIC_ADMISSION_OK) + return admission; + + PG_TRY(); + { + if (result == NULL || request == NULL || op != CLUSTER_R4_SOURCE_CR_FETCH + || request->dst_page == NULL) + admission = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + else + local_result.fetched = cluster_gcs_block_cr_fetch_and_wait_raw( + request->tag, request->read_scn, request->origin_node, request->dst_page, + &local_result.partial); + if (admission == CLUSTER_SEMANTIC_ADMISSION_OK + && !cluster_semantic_activation_recheck(&token)) + admission = CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED; + } + PG_CATCH(); + { + cluster_semantic_activation_leave(&token); + PG_RE_THROW(); + } + PG_END_TRY(); + + cluster_semantic_activation_leave(&token); + if (admission == CLUSTER_SEMANTIC_ADMISSION_OK) + *result = local_result; + return admission; +} + /* * PGRAC: spec-6.12i D-i1 — requester-side undo-TT fetch. diff --git a/src/include/cluster/cluster_cr_server.h b/src/include/cluster/cluster_cr_server.h index 4ed8869c514..89e78c2c4e0 100644 --- a/src/include/cluster/cluster_cr_server.h +++ b/src/include/cluster/cluster_cr_server.h @@ -60,6 +60,7 @@ #include "cluster/cluster_gcs_block.h" #include "cluster/cluster_runtime_visibility.h" /* ClusterLiveAuthority (spec-6.12i) */ +#include "cluster/cluster_semantic_activation.h" #include "cluster/cluster_undo_verdict.h" /* ClusterUndoVerdictResult (spec-5.22d D4-6) */ /* Split verdict for the server-side construction (see banner). */ @@ -267,13 +268,23 @@ extern void cluster_lms_cr_ship_ready(void); extern void cluster_gcs_block_forward_serve_inline(const GcsBlockForwardPayload *fwd, ClusterLmsCrSlotKind kind); -/* Requester side (backend): fetch a CR page for (locator, fork, block) at - * read_scn from origin_node. On success copies the shipped page into - * dst_page and returns true; *out_partial says whether the local - * construction must continue on it. false = fail-closed (caller keeps the - * unchanged 53R9G refusal). */ -extern bool cluster_gcs_block_cr_fetch_and_wait(BufferTag tag, SCN read_scn, int32 origin_node, - char *dst_page, bool *out_partial); +typedef enum ClusterR4SourceCrOp { CLUSTER_R4_SOURCE_CR_FETCH = 0 } ClusterR4SourceCrOp; + +typedef struct ClusterR4SourceCrRequest { + BufferTag tag; + SCN read_scn; + int32 origin_node; + char *dst_page; +} ClusterR4SourceCrRequest; + +typedef struct ClusterR4SourceCrResult { + bool fetched; + bool partial; +} ClusterR4SourceCrResult; + +extern ClusterSemanticAdmissionResult +cluster_r4_source_cr_dispatch(ClusterR4SourceCrOp op, const ClusterR4SourceCrRequest *request, + ClusterR4SourceCrResult *result); /* Requester side (backend, spec-6.12i D-i1): fetch origin_node's TT-bearing * undo header block (segment_id, block_no) over the same wire, together with From 291494810cbd9539f62b24e759f799c79a42935b Mon Sep 17 00:00:00 2001 From: SqlRush Date: Mon, 10 Aug 2026 09:18:33 +0800 Subject: [PATCH 3/8] feat(cluster): gate hint source dispatch Route legacy hint queue operations through the shared semantic admission boundary and keep raw bodies module-private. Spec: spec-8.4-oracle-synchronous-consistent-read.md --- src/backend/cluster/cluster_tt_status_hint.c | 179 ++++++--- src/include/cluster/cluster_tt_status_hint.h | 72 ++-- .../test_cluster_r4_d10_hint_source.c | 375 ++++++++++++++++++ 3 files changed, 534 insertions(+), 92 deletions(-) create mode 100644 src/test/cluster_unit/test_cluster_r4_d10_hint_source.c diff --git a/src/backend/cluster/cluster_tt_status_hint.c b/src/backend/cluster/cluster_tt_status_hint.c index 71de287b9b3..d7fe208caab 100644 --- a/src/backend/cluster/cluster_tt_status_hint.c +++ b/src/backend/cluster/cluster_tt_status_hint.c @@ -57,6 +57,7 @@ #include "cluster/cluster_ic_envelope.h" #include "cluster/cluster_ic_router.h" #include "cluster/cluster_lmon.h" +#include "cluster/cluster_semantic_activation.h" #include "cluster/cluster_shmem.h" #include "cluster/cluster_tx_enqueue.h" /* spec-5.2 D6: wake TX-enqueue waiters */ #include "cluster/cluster_tt_status.h" @@ -119,6 +120,16 @@ typedef struct ClusterMultiXactHintOutboundRing { static ClusterMultiXactHintOutboundRing *ClusterMultiXactHintOutbound = NULL; +static void cluster_tt_status_hint_emit_raw(const ClusterTTStatusKey *key, ClusterTTStatus status, + SCN commit_scn); +static void cluster_tt_status_hint_emit_subcommitted_raw(const ClusterTTStatusKey *child_key, + const ClusterTTStatusKey *parent_key); +static void cluster_tt_status_hint_emit_multixact_overlay_raw( + const ClusterMultiXactKey *key, uint16 member_count, const ClusterMultiXactMember *members); +static void cluster_tt_status_hint_drain_outbound_raw(void); +static void cluster_tt_status_hint_handle_envelope_raw(const ClusterICEnvelope *env, + const void *payload); + static Size v4_outbound_ring_size(int capacity) { @@ -217,12 +228,23 @@ cluster_tt_status_hint_shmem_register(void) /* IC msg type registration (HC185 producer mask) */ /* ------------------------------------------------------------ */ +static void +cluster_tt_status_hint_handle_envelope_adapter(const ClusterICEnvelope *env, const void *payload) +{ + ClusterTTStatusHintSourceRequest request; + + memset(&request, 0, sizeof(request)); + request.env = env; + request.payload = payload; + (void)cluster_tt_status_hint_source_dispatch(CLUSTER_TT_HINT_SOURCE_HANDLE_ENVELOPE, &request); +} + static const ClusterICMsgTypeInfo cluster_tt_status_hint_msg_info = { .msg_type = PGRAC_IC_MSG_TT_STATUS_HINT, .name = "cluster_tt_status_hint", .allowed_producer_mask = CLUSTER_IC_PRODUCER_TT_STATUS_HINT, .broadcast_ok = true, - .handler = cluster_tt_status_hint_handle_envelope, + .handler = cluster_tt_status_hint_handle_envelope_adapter, }; void @@ -235,8 +257,9 @@ cluster_tt_status_hint_register_msg_type(void) /* emit path (D4 calls this from xact commit/abort hook) */ /* ------------------------------------------------------------ */ -void -cluster_tt_status_hint_emit(const ClusterTTStatusKey *key, ClusterTTStatus status, SCN commit_scn) +static void +cluster_tt_status_hint_emit_raw(const ClusterTTStatusKey *key, ClusterTTStatus status, + SCN commit_scn) { uint32 tail; uint32 next_tail; @@ -297,7 +320,7 @@ cluster_tt_status_hint_emit(const ClusterTTStatusKey *key, ClusterTTStatus statu } /* - * cluster_tt_status_hint_emit_subcommitted (PGRAC spec-3.5 D3 NEW) + * cluster_tt_status_hint_emit_subcommitted_raw (PGRAC spec-3.5 D3 NEW) * * Enqueue a SUBCOMMITTED hint with parent_key chain pointer. Emit * path is identical to V2 except the slot carries has_parent_key=1 @@ -305,9 +328,9 @@ cluster_tt_status_hint_emit(const ClusterTTStatusKey *key, ClusterTTStatus statu * must have installed local overlay via * cluster_tt_status_install_subcommitted() first. */ -void -cluster_tt_status_hint_emit_subcommitted(const ClusterTTStatusKey *child_key, - const ClusterTTStatusKey *parent_key) +static void +cluster_tt_status_hint_emit_subcommitted_raw(const ClusterTTStatusKey *child_key, + const ClusterTTStatusKey *parent_key) { uint32 tail; uint32 next_tail; @@ -351,16 +374,17 @@ cluster_tt_status_hint_emit_subcommitted(const ClusterTTStatusKey *child_key, } /* - * cluster_tt_status_hint_emit_multixact_overlay (PGRAC spec-3.6 D4 NEW) + * cluster_tt_status_hint_emit_multixact_overlay_raw (PGRAC spec-3.6 D4 NEW) * * Enqueue a V4 sidecar emit (multixact composition overlay) for LMON * drain. Sender member_count > GUC cap → fail-closed (no partial * emit) + overflow counter. Uses dedicated V4 sidecar outbound queue * (does NOT pollute V2/V3 fixed ring). */ -void -cluster_tt_status_hint_emit_multixact_overlay(const ClusterMultiXactKey *key, uint16 member_count, - const ClusterMultiXactMember *members) +static void +cluster_tt_status_hint_emit_multixact_overlay_raw(const ClusterMultiXactKey *key, + uint16 member_count, + const ClusterMultiXactMember *members) { uint32 tail; uint32 next_tail; @@ -419,8 +443,8 @@ cluster_tt_status_hint_emit_multixact_overlay(const ClusterMultiXactKey *key, ui /* LMON drain (L172 family — LMON-only HC185) */ /* ------------------------------------------------------------ */ -void -cluster_tt_status_hint_drain_outbound(void) +static void +cluster_tt_status_hint_drain_outbound_raw(void) { if (ClusterTTHintOutbound == NULL || ClusterTTHintCounters == NULL) return; @@ -532,8 +556,8 @@ cluster_tt_status_hint_drain_outbound(void) /* receiver path */ /* ------------------------------------------------------------ */ -void -cluster_tt_status_hint_handle_envelope(const ClusterICEnvelope *env, const void *payload) +static void +cluster_tt_status_hint_handle_envelope_raw(const ClusterICEnvelope *env, const void *payload) { uint16 msg_version; uint16 status_raw; @@ -812,6 +836,92 @@ cluster_tt_status_hint_handle_envelope(const ClusterICEnvelope *env, const void pg_atomic_fetch_add_u64(&ClusterTTHintCounters->install_count, 1); } +/* + * cluster_tt_status_hint_source_request_valid -- Validate an admitted op. + * + * The caller has already acquired SOURCE admission. This ordering keeps + * all request reads behind the common semantic activation gate. + */ +static bool +cluster_tt_status_hint_source_request_valid(ClusterTTStatusHintSourceOp op, + const ClusterTTStatusHintSourceRequest *request) +{ + switch (op) { + case CLUSTER_TT_HINT_SOURCE_EMIT: + return request != NULL && request->key != NULL; + case CLUSTER_TT_HINT_SOURCE_EMIT_SUBCOMMITTED: + return request != NULL && request->key != NULL && request->parent_key != NULL; + case CLUSTER_TT_HINT_SOURCE_EMIT_MULTIXACT_OVERLAY: + return request != NULL && request->multi_key != NULL && request->members != NULL; + case CLUSTER_TT_HINT_SOURCE_HANDLE_ENVELOPE: + return request != NULL && request->env != NULL && request->payload != NULL; + case CLUSTER_TT_HINT_SOURCE_DRAIN_OUTBOUND: + return true; + } + return false; +} + +/* + * cluster_tt_status_hint_source_dispatch -- Admit one dormant hint op. + * + * Admission precedes every request read and every queue, counter or + * transport mutation. A caught backend error is copied, admission is + * released through the common funnel, and the original error is rethrown. + */ +ClusterSemanticAdmissionResult +cluster_tt_status_hint_source_dispatch(ClusterTTStatusHintSourceOp op, + const ClusterTTStatusHintSourceRequest *request) +{ + ClusterSemanticAdmissionToken admission; + ClusterSemanticAdmissionResult result; + ErrorData *volatile error_data = NULL; + + result = cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &admission); + if (result != CLUSTER_SEMANTIC_ADMISSION_OK) + return result; + + if (!cluster_tt_status_hint_source_request_valid(op, request)) + result = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + else { + PG_TRY(); + { + switch (op) { + case CLUSTER_TT_HINT_SOURCE_EMIT: + cluster_tt_status_hint_emit_raw(request->key, request->status, request->commit_scn); + break; + case CLUSTER_TT_HINT_SOURCE_EMIT_SUBCOMMITTED: + cluster_tt_status_hint_emit_subcommitted_raw(request->key, request->parent_key); + break; + case CLUSTER_TT_HINT_SOURCE_EMIT_MULTIXACT_OVERLAY: + cluster_tt_status_hint_emit_multixact_overlay_raw( + request->multi_key, request->member_count, request->members); + break; + case CLUSTER_TT_HINT_SOURCE_HANDLE_ENVELOPE: + cluster_tt_status_hint_handle_envelope_raw(request->env, request->payload); + break; + case CLUSTER_TT_HINT_SOURCE_DRAIN_OUTBOUND: + cluster_tt_status_hint_drain_outbound_raw(); + break; + } + } + PG_CATCH(); + { + error_data = CopyErrorData(); + FlushErrorState(); + } + PG_END_TRY(); + + if (error_data == NULL && !cluster_semantic_activation_recheck(&admission)) + result = CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED; + } + + cluster_semantic_activation_leave(&admission); + if (error_data != NULL) + ReThrowError((ErrorData *)error_data); + return result; +} + /* ------------------------------------------------------------ */ /* counter getters */ /* ------------------------------------------------------------ */ @@ -852,31 +962,13 @@ void cluster_tt_status_hint_shmem_register(void) {} -void -cluster_tt_status_hint_emit(const ClusterTTStatusKey *key, ClusterTTStatus status, SCN commit_scn) -{ - (void)key; - (void)status; - (void)commit_scn; -} - -void -cluster_tt_status_hint_emit_subcommitted(const ClusterTTStatusKey *child_key, - const ClusterTTStatusKey *parent_key) -{ - (void)child_key; - (void)parent_key; -} - -void -cluster_tt_status_hint_drain_outbound(void) -{} - -void -cluster_tt_status_hint_handle_envelope(const ClusterICEnvelope *env, const void *payload) +ClusterSemanticAdmissionResult +cluster_tt_status_hint_source_dispatch(ClusterTTStatusHintSourceOp op, + const ClusterTTStatusHintSourceRequest *request) { - (void)env; - (void)payload; + (void)op; + (void)request; + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; } #define CLUSTER_TT_HINT_GETTER_STUB(name) \ @@ -897,13 +989,4 @@ CLUSTER_TT_HINT_GETTER_STUB(v3_downgrade_count) /* PGRAC spec-3.6 D4 */ CLUSTER_TT_HINT_GETTER_STUB(v4_drop_unknown_count) -void -cluster_tt_status_hint_emit_multixact_overlay(const ClusterMultiXactKey *key, uint16 member_count, - const ClusterMultiXactMember *members) -{ - (void)key; - (void)member_count; - (void)members; -} - #endif /* USE_PGRAC_CLUSTER */ diff --git a/src/include/cluster/cluster_tt_status_hint.h b/src/include/cluster/cluster_tt_status_hint.h index dfca78a85a4..5a1ec481b17 100644 --- a/src/include/cluster/cluster_tt_status_hint.h +++ b/src/include/cluster/cluster_tt_status_hint.h @@ -54,6 +54,7 @@ #include "c.h" #include "access/transam.h" +#include "cluster/cluster_semantic_activation.h" #include "cluster/cluster_tt_status.h" /* ClusterTTStatus + ClusterTTStatusKey */ /* Forward decl from cluster_ic_envelope.h (avoid heavy include in this @@ -200,41 +201,6 @@ typedef ClusterTTStatusHintMsgV1 ClusterTTStatusHintMsg; */ #define CLUSTER_IC_PRODUCER_TT_STATUS_HINT ((uint32)(1u << B_LMON)) -/* - * Public API. - * - * cluster_tt_status_hint_emit: - * enqueue a hint for cross-node propagation. Caller is D4 - * xact.c commit/abort hook (spec-3.1 D5 install path) — caller - * passes the EXACT key it just install_local'd (HC184:no raw-xid - * rebuild). Fire-and-forget;does not block commit hot path; - * enqueue failure increments drop_invalid_count + WARNING. - * - * cluster_tt_status_hint_handle_envelope: - * tier1 receiver dispatcher. Validates per §3.2 (msg_version + - * checksum + epoch + anti-spoof + reserved-zero + status range), - * then install_local with msg.key directly. - * - * cluster_tt_status_hint_drain_outbound: - * LMON drain entry point. Iterates alive peers (3-gate) and - * fanout each hint via tier1 send. Only LMON calls this (HC185). - */ -extern void cluster_tt_status_hint_emit(const ClusterTTStatusKey *key, ClusterTTStatus status, - SCN commit_scn); - -/* - * cluster_tt_status_hint_emit_subcommitted (spec-3.5 D3 NEW): - * Emit a V3 SUBCOMMITTED hint with parent_key chain pointer. Used by - * spec-3.5 D7 xact.c CommitSubTransaction hook. Caller MUST have - * already installed local overlay via cluster_tt_status_install_subcommitted. - * V3-only peers receive correctly;V1/V2 peers DROP (forward-compat). - * Origin skips emit if no peer >= V3 (warn-only counter bump). - */ -extern void cluster_tt_status_hint_emit_subcommitted(const ClusterTTStatusKey *child_key, - const ClusterTTStatusKey *parent_key); -extern void cluster_tt_status_hint_handle_envelope(const struct ClusterICEnvelope *env, - const void *payload); -extern void cluster_tt_status_hint_drain_outbound(void); extern void cluster_tt_status_hint_register_msg_type(void); /* Counters (spec-3.3 D9: 7 counters; +drop_v1_compat). */ @@ -298,16 +264,34 @@ StaticAssertDecl(sizeof(ClusterMultiXactHintOutboundEntry) == 6168, "V4 outbound entry = 24 + 256 × 24 (F3/F7)"); /* - * cluster_tt_status_hint_emit_multixact_overlay (spec-3.6 D4): - * Enqueue V4 sidecar emit for cross-node MultiXact member overlay - * broadcast. Used by D5 multixact.c hook end of MultiXactIdCreate / - * Expand (local-all-member path). Caller must have already installed - * local overlay via cluster_multixact_member_overlay_install. - * Sender member_count > GUC cap → fail-closed (no partial emit). + * ClusterTTStatusHintSourceOp -- dormant hint-source operations. + * + * Every old hint producer and consumer enters the common semantic + * activation gate through cluster_tt_status_hint_source_dispatch. */ -extern void cluster_tt_status_hint_emit_multixact_overlay(const ClusterMultiXactKey *key, - uint16 member_count, - const ClusterMultiXactMember *members); +typedef enum ClusterTTStatusHintSourceOp { + CLUSTER_TT_HINT_SOURCE_EMIT = 0, + CLUSTER_TT_HINT_SOURCE_EMIT_SUBCOMMITTED = 1, + CLUSTER_TT_HINT_SOURCE_EMIT_MULTIXACT_OVERLAY = 2, + CLUSTER_TT_HINT_SOURCE_HANDLE_ENVELOPE = 3, + CLUSTER_TT_HINT_SOURCE_DRAIN_OUTBOUND = 4 +} ClusterTTStatusHintSourceOp; + +typedef struct ClusterTTStatusHintSourceRequest { + const ClusterTTStatusKey *key; + const ClusterTTStatusKey *parent_key; + ClusterTTStatus status; + SCN commit_scn; + const struct ClusterICEnvelope *env; + const void *payload; + const ClusterMultiXactKey *multi_key; + uint16 member_count; + const ClusterMultiXactMember *members; +} ClusterTTStatusHintSourceRequest; + +extern ClusterSemanticAdmissionResult +cluster_tt_status_hint_source_dispatch(ClusterTTStatusHintSourceOp op, + const ClusterTTStatusHintSourceRequest *request); extern uint64 cluster_tt_status_hint_get_v4_drop_unknown_count(void); diff --git a/src/test/cluster_unit/test_cluster_r4_d10_hint_source.c b/src/test/cluster_unit/test_cluster_r4_d10_hint_source.c new file mode 100644 index 00000000000..0f060091189 --- /dev/null +++ b/src/test/cluster_unit/test_cluster_r4_d10_hint_source.c @@ -0,0 +1,375 @@ +/*------------------------------------------------------------------------- + * + * test_cluster_r4_d10_hint_source.c + * R4 D10 dormant-source admission tests for TT status hints. + * + * 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_r4_d10_hint_source.c + * + * NOTES + * This is a pgrac-original file. It executes the product dispatch + * against real hint queue state while replacing only adjacent process, + * transport and shared-admission boundaries. + * + *------------------------------------------------------------------------- + */ +#define USE_PGRAC_CLUSTER 1 + +#include "postgres.h" + +#include "cluster/cluster_conf.h" +#include "cluster/cluster_epoch.h" +#include "cluster/cluster_guc.h" +#include "cluster/cluster_ic_router.h" +#include "cluster/cluster_lmon.h" +#include "cluster/cluster_semantic_activation.h" +#include "cluster/cluster_shmem.h" +#include "cluster/cluster_tt_status_hint.h" +#include "cluster/cluster_tx_enqueue.h" +#include "miscadmin.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" + +#undef printf +#undef fprintf +#undef snprintf +#undef sprintf +#undef vsnprintf +#undef vfprintf +#undef vprintf +#undef vsprintf +#undef strerror +#undef strerror_r + +#include "unit_test.h" + +UT_DEFINE_GLOBALS(); + +#define TEST_HINT_RING_BYTES (64 * 1024) + +typedef union TestHintStorage { + uint64 align; + uint8 bytes[TEST_HINT_RING_BYTES]; +} TestHintStorage; + +static TestHintStorage test_hint_ring; +static TestHintStorage test_hint_counters; +static TestHintStorage test_multi_ring; +static ClusterConf test_cluster_conf; +static ClusterSemanticAdmissionResult test_admission_result; +static bool test_recheck_result; +static int test_leave_count; +static const ClusterICMsgTypeInfo *test_registered_msg_info; + +bool cluster_enabled = true; +int cluster_node_id = 0; +int cluster_tt_status_hint_outbound_capacity = 4; +int cluster_tt_status_hint_emit_mode = CLUSTER_TT_STATUS_HINT_EMIT_ALL_STATUS; +int cluster_multixact_hint_outbound_slots = 4; +int cluster_multixact_member_overlay_max_members = 4; +ClusterConf *ClusterConfShmem = &test_cluster_conf; +ProcessingMode Mode = NormalProcessing; +BackendType MyBackendType = B_BACKEND; +sigjmp_buf *PG_exception_stack = NULL; +ErrorContextCallback *error_context_stack = NULL; + +void +ExceptionalCondition(const char *condition_name pg_attribute_unused(), + const char *file_name pg_attribute_unused(), + int line_number pg_attribute_unused()) +{ + abort(); +} + +void * +ShmemInitStruct(const char *name, Size size, bool *found_ptr) +{ + TestHintStorage *storage; + + if (strcmp(name, "ClusterTTStatusHintOutbound") == 0) + storage = &test_hint_ring; + else if (strcmp(name, "ClusterTTStatusHintState") == 0) + storage = &test_hint_counters; + else if (strcmp(name, "ClusterMultiXactHintOutbound") == 0) + storage = &test_multi_ring; + else + abort(); + if (size > sizeof(storage->bytes)) + abort(); + memset(storage->bytes, 0, sizeof(storage->bytes)); + *found_ptr = false; + return storage->bytes; +} + +void +LWLockInitialize(LWLock *lock pg_attribute_unused(), int tranche_id pg_attribute_unused()) +{} + +bool +LWLockAcquire(LWLock *lock pg_attribute_unused(), LWLockMode mode pg_attribute_unused()) +{ + return true; +} + +void +LWLockRelease(LWLock *lock pg_attribute_unused()) +{} + +void +cluster_lmon_duty_mark_dirty(ClusterLmonDuty duty pg_attribute_unused()) +{} + +void +cluster_lmon_wakeup(void) +{} + +void +cluster_ic_send_envelope_fanout(uint8 msg_type pg_attribute_unused(), + const void *payload pg_attribute_unused(), + uint32 payload_len pg_attribute_unused(), + ClusterICFanoutResult per_peer[] pg_attribute_unused()) +{} + +void +cluster_ic_register_msg_type(const ClusterICMsgTypeInfo *info) +{ + test_registered_msg_info = info; +} + +void +cluster_shmem_register_region(const ClusterShmemRegion *region pg_attribute_unused()) +{} + +uint64 +cluster_epoch_get_current(void) +{ + return 1; +} + +bool +cluster_multixact_member_overlay_install( + const ClusterMultiXactKey *key pg_attribute_unused(), uint16 member_count pg_attribute_unused(), + const ClusterMultiXactMember *members pg_attribute_unused()) +{ + return true; +} + +bool +cluster_tt_status_install_local(const ClusterTTStatusKey *key pg_attribute_unused(), + ClusterTTStatus status pg_attribute_unused(), + SCN commit_scn pg_attribute_unused()) +{ + return true; +} + +bool +cluster_tt_status_install_subcommitted(const ClusterTTStatusKey *child_key pg_attribute_unused(), + const ClusterTTStatusKey *parent_key pg_attribute_unused()) +{ + return true; +} + +void +cluster_txw_wake_waiters(const ClusterTTStatusKey *holder_key pg_attribute_unused()) +{} + +bool +errstart(int elevel pg_attribute_unused(), const char *domain pg_attribute_unused()) +{ + return false; +} + +bool +errstart_cold(int elevel pg_attribute_unused(), const char *domain pg_attribute_unused()) +{ + return false; +} + +void +errfinish(const char *filename pg_attribute_unused(), int lineno pg_attribute_unused(), + const char *funcname pg_attribute_unused()) +{} + +int +errcode(int sqlerrcode pg_attribute_unused()) +{ + return 0; +} + +int +errmsg(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +int +errhint(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +ErrorData * +CopyErrorData(void) +{ + static ErrorData error_data; + + return &error_data; +} + +void +FlushErrorState(void) +{} + +void +ReThrowError(ErrorData *edata pg_attribute_unused()) +{ + abort(); +} + +Size +mul_size(Size s1, Size s2) +{ + return s1 * s2; +} + +ClusterSemanticAdmissionResult +cluster_semantic_activation_enter(uint64 feature_bit pg_attribute_unused(), + ClusterSemanticAdmissionSide side pg_attribute_unused(), + ClusterSemanticAdmissionToken *token) +{ + memset(token, 0, sizeof(*token)); + if (test_admission_result == CLUSTER_SEMANTIC_ADMISSION_OK) { + token->feature_bit = CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1; + token->side = CLUSTER_SEMANTIC_SOURCE_SIDE; + token->entered = true; + } + return test_admission_result; +} + +bool +cluster_semantic_activation_recheck( + const ClusterSemanticAdmissionToken *token pg_attribute_unused()) +{ + return test_recheck_result; +} + +void +cluster_semantic_activation_leave(ClusterSemanticAdmissionToken *token) +{ + test_leave_count++; + memset(token, 0, sizeof(*token)); +} + +static void +test_reset_gate(ClusterSemanticAdmissionResult admission_result, bool recheck_result) +{ + test_admission_result = admission_result; + test_recheck_result = recheck_result; + test_leave_count = 0; +} + +static void +test_prepare_hint_state(void) +{ + memset(&test_cluster_conf, 0, sizeof(test_cluster_conf)); + test_cluster_conf.node_count = 2; + cluster_tt_status_hint_shmem_init(); +} + +UT_TEST(test_active_refuses_all_ops_before_request_inspection) +{ + const ClusterTTStatusHintSourceRequest *poison_request + = (const ClusterTTStatusHintSourceRequest *)(uintptr_t)1; + ClusterTTStatusHintSourceOp op; + + test_reset_gate(CLUSTER_SEMANTIC_ADMISSION_SOURCE_DORMANT, true); + for (op = CLUSTER_TT_HINT_SOURCE_EMIT; op <= CLUSTER_TT_HINT_SOURCE_DRAIN_OUTBOUND; op++) + UT_ASSERT_EQ(cluster_tt_status_hint_source_dispatch(op, poison_request), + CLUSTER_SEMANTIC_ADMISSION_SOURCE_DORMANT); + UT_ASSERT_EQ(test_leave_count, 0); + UT_ASSERT_EQ(cluster_tt_status_hint_get_emit_count(), 0); +} + +UT_TEST(test_disabled_emit_mutates_real_source_queue) +{ + ClusterTTStatusKey key; + ClusterTTStatusHintSourceRequest request; + + memset(&key, 0, sizeof(key)); + memset(&request, 0, sizeof(request)); + request.key = &key; + request.status = CLUSTER_TT_STATUS_COMMITTED; + request.commit_scn = 42; + test_reset_gate(CLUSTER_SEMANTIC_ADMISSION_OK, true); + + UT_ASSERT_EQ(cluster_tt_status_hint_source_dispatch(CLUSTER_TT_HINT_SOURCE_EMIT, &request), + CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT_EQ(cluster_tt_status_hint_get_emit_count(), 1); + UT_ASSERT_EQ(test_leave_count, 1); +} + +UT_TEST(test_validation_happens_after_admission_and_leaves_once) +{ + ClusterTTStatusHintSourceRequest request; + + memset(&request, 0, sizeof(request)); + test_reset_gate(CLUSTER_SEMANTIC_ADMISSION_OK, true); + + UT_ASSERT_EQ(cluster_tt_status_hint_source_dispatch(CLUSTER_TT_HINT_SOURCE_EMIT, &request), + CLUSTER_SEMANTIC_ADMISSION_CLOSED); + UT_ASSERT_EQ(test_leave_count, 1); + UT_ASSERT_EQ(cluster_tt_status_hint_get_emit_count(), 1); +} + +UT_TEST(test_recheck_drift_returns_generation_changed_and_leaves) +{ + ClusterTTStatusKey key; + ClusterTTStatusHintSourceRequest request; + + memset(&key, 0, sizeof(key)); + memset(&request, 0, sizeof(request)); + request.key = &key; + request.status = CLUSTER_TT_STATUS_ABORTED; + request.commit_scn = InvalidScn; + test_reset_gate(CLUSTER_SEMANTIC_ADMISSION_OK, false); + + UT_ASSERT_EQ(cluster_tt_status_hint_source_dispatch(CLUSTER_TT_HINT_SOURCE_EMIT, &request), + CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED); + UT_ASSERT_EQ(cluster_tt_status_hint_get_emit_count(), 2); + UT_ASSERT_EQ(test_leave_count, 1); +} + +UT_TEST(test_registration_adapter_uses_source_dispatch) +{ + const ClusterICEnvelope *poison_env = (const ClusterICEnvelope *)(uintptr_t)1; + const void *poison_payload = (const void *)(uintptr_t)1; + + test_registered_msg_info = NULL; + cluster_tt_status_hint_register_msg_type(); + UT_ASSERT_NOT_NULL(test_registered_msg_info); + UT_ASSERT_NOT_NULL(test_registered_msg_info->handler); + test_reset_gate(CLUSTER_SEMANTIC_ADMISSION_SOURCE_DORMANT, true); + test_registered_msg_info->handler(poison_env, poison_payload); + UT_ASSERT_EQ(test_leave_count, 0); + UT_ASSERT_EQ(cluster_tt_status_hint_get_receive_count(), 0); +} + +int +main(void) +{ + test_prepare_hint_state(); + UT_PLAN(5); + UT_RUN(test_active_refuses_all_ops_before_request_inspection); + UT_RUN(test_disabled_emit_mutates_real_source_queue); + UT_RUN(test_validation_happens_after_admission_and_leaves_once); + UT_RUN(test_recheck_drift_returns_generation_changed_and_leaves); + UT_RUN(test_registration_adapter_uses_source_dispatch); + UT_DONE(); + return ut_failed_count == 0 ? 0 : 1; +} From 9731e49b589985392d9b28bacd5c5ab53cf8aa73 Mon Sep 17 00:00:00 2001 From: SqlRush Date: Mon, 10 Aug 2026 10:07:54 +0800 Subject: [PATCH 4/8] feat(cluster): gate TT source dispatch --- src/backend/cluster/cluster_tt_status.c | 135 +++- src/include/cluster/cluster_tt_status.h | 50 +- .../test_cluster_r4_d10_tt_source.c | 579 ++++++++++++++++++ 3 files changed, 737 insertions(+), 27 deletions(-) create mode 100644 src/test/cluster_unit/test_cluster_r4_d10_tt_source.c diff --git a/src/backend/cluster/cluster_tt_status.c b/src/backend/cluster/cluster_tt_status.c index 8e32d235efa..d9d27a2e4ea 100644 --- a/src/backend/cluster/cluster_tt_status.c +++ b/src/backend/cluster/cluster_tt_status.c @@ -151,6 +151,17 @@ typedef struct ClusterTTStatusShmem { pg_atomic_uint64 remote_uba_resolved; /* materialized remote undo reads */ } ClusterTTStatusShmem; +static bool cluster_tt_status_lookup_exact(const ClusterTTStatusKey *key, + ClusterTTStatusResult *result); +static bool cluster_tt_status_install_local(const ClusterTTStatusKey *key, ClusterTTStatus status, + SCN commit_scn); +static bool cluster_tt_status_install_subcommitted(const ClusterTTStatusKey *child_key, + const ClusterTTStatusKey *parent_key); +static bool cluster_tt_status_delete_exact(const ClusterTTStatusKey *key); +static int cluster_tt_status_resolve_prepared_commit(TransactionId xid, SCN commit_scn); +static void cluster_tt_status_bump_self_consumer_hit(void); +static void cluster_tt_status_bump_parent_chain_follow(void); + #ifdef USE_PGRAC_CLUSTER static HTAB *ClusterTTStatusHTAB = NULL; @@ -497,7 +508,7 @@ note_overlay_full(const char *dropped_kind) /* public API */ /* ------------------------------------------------------------ */ -bool +static bool cluster_tt_status_lookup_exact(const ClusterTTStatusKey *key, ClusterTTStatusResult *result) { const ClusterTTOverlayEntry *e; @@ -644,7 +655,7 @@ cluster_tt_status_lookup_exact(const ClusterTTStatusKey *key, ClusterTTStatusRes return true; } -bool +static bool cluster_tt_status_install_local(const ClusterTTStatusKey *key, ClusterTTStatus status, SCN commit_scn) { @@ -689,7 +700,7 @@ cluster_tt_status_install_local(const ClusterTTStatusKey *key, ClusterTTStatus s return true; } -int +static int cluster_tt_status_resolve_prepared_commit(TransactionId xid, SCN commit_scn) { HASH_SEQ_STATUS seq; @@ -788,7 +799,7 @@ cluster_tt_status_resolve_prepared_commit(TransactionId xid, SCN commit_scn) * MUST first ensure parent_key has its own overlay binding via * cluster_subtrans_ensure_parent_binding(). */ -bool +static bool cluster_tt_status_install_subcommitted(const ClusterTTStatusKey *child_key, const ClusterTTStatusKey *parent_key) { @@ -834,7 +845,7 @@ cluster_tt_status_install_subcommitted(const ClusterTTStatusKey *child_key, * clear by writing ABORTED — semantic conflict with the real * TT status state machine. */ -bool +static bool cluster_tt_status_delete_exact(const ClusterTTStatusKey *key) { bool found; @@ -949,7 +960,7 @@ cluster_tt_status_generation(void) * by D6 commit hook to record the runtime self-consumer lookup * (spec-3.1 v0.4 N7). Only D5/D6 should call this. */ -void +static void cluster_tt_status_bump_self_consumer_hit(void) { if (!cluster_enabled || ClusterTTStatusState == NULL) @@ -981,7 +992,7 @@ CLUSTER_TT_STATUS_COUNTER_GETTER(subcommitted_install_count) CLUSTER_TT_STATUS_COUNTER_GETTER(subcommitted_lookup_hit_count) CLUSTER_TT_STATUS_COUNTER_GETTER(parent_chain_follow_count) -void +static void cluster_tt_status_bump_parent_chain_follow(void) { if (ClusterTTStatusState != NULL) @@ -1056,7 +1067,7 @@ void cluster_tt_status_shmem_register(void) {} -bool +static bool cluster_tt_status_lookup_exact(const ClusterTTStatusKey *key, ClusterTTStatusResult *result) { if (result != NULL) { @@ -1072,7 +1083,7 @@ cluster_tt_status_lookup_exact(const ClusterTTStatusKey *key, ClusterTTStatusRes return false; } -bool +static bool cluster_tt_status_install_local(const ClusterTTStatusKey *key, ClusterTTStatus status, SCN commit_scn) { @@ -1082,7 +1093,7 @@ cluster_tt_status_install_local(const ClusterTTStatusKey *key, ClusterTTStatus s return false; } -int +static int cluster_tt_status_resolve_prepared_commit(TransactionId xid, SCN commit_scn) { (void)xid; @@ -1090,7 +1101,7 @@ cluster_tt_status_resolve_prepared_commit(TransactionId xid, SCN commit_scn) return 0; } -bool +static bool cluster_tt_status_install_subcommitted(const ClusterTTStatusKey *child_key, const ClusterTTStatusKey *parent_key) { @@ -1099,7 +1110,7 @@ cluster_tt_status_install_subcommitted(const ClusterTTStatusKey *child_key, return false; } -bool +static bool cluster_tt_status_delete_exact(const ClusterTTStatusKey *key) { (void)key; @@ -1122,7 +1133,7 @@ cluster_tt_status_generation(void) return 0; } -void +static void cluster_tt_status_bump_self_consumer_hit(void) {} @@ -1144,9 +1155,105 @@ CLUSTER_TT_STATUS_COUNTER_GETTER_STUB(subcommitted_install_count) CLUSTER_TT_STATUS_COUNTER_GETTER_STUB(subcommitted_lookup_hit_count) CLUSTER_TT_STATUS_COUNTER_GETTER_STUB(parent_chain_follow_count) -void +static void cluster_tt_status_bump_parent_chain_follow(void) {} #endif /* USE_PGRAC_CLUSTER */ + +/* + * cluster_tt_status_source_dispatch -- Admit one legacy TT source operation. + * + * The caller-visible result is canonical zero unless the operation completes + * under one stable semantic-activation generation. PG_FINALLY supplies the + * single leave funnel for both normal and ERROR paths. + * + * Spec: spec-8.4-oracle-synchronous-consistent-read.md + */ +ClusterSemanticAdmissionResult +cluster_tt_status_source_dispatch(ClusterTTStatusSourceOp op, + const ClusterTTStatusSourceRequest *request, + ClusterTTStatusSourceResult *result) +{ + ClusterSemanticAdmissionToken token; + ClusterSemanticAdmissionResult admission; + ClusterTTStatusSourceResult local_result; + + if (result != NULL) + memset(result, 0, sizeof(*result)); + memset(&local_result, 0, sizeof(local_result)); + + admission = cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &token); + if (admission != CLUSTER_SEMANTIC_ADMISSION_OK) + return admission; + + PG_TRY(); + { + if (result == NULL) + admission = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + else { + switch (op) { + case CLUSTER_TT_SOURCE_LOOKUP: + if (request == NULL || request->key == NULL) + admission = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + else + local_result.bool_value + = cluster_tt_status_lookup_exact(request->key, &local_result.lookup); + break; + case CLUSTER_TT_SOURCE_INSTALL_LOCAL: + if (request == NULL || request->key == NULL) + admission = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + else + local_result.bool_value = cluster_tt_status_install_local( + request->key, request->status, request->commit_scn); + break; + case CLUSTER_TT_SOURCE_INSTALL_SUBCOMMITTED: + if (request == NULL || request->key == NULL || request->parent_key == NULL) + admission = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + else + local_result.bool_value + = cluster_tt_status_install_subcommitted(request->key, request->parent_key); + break; + case CLUSTER_TT_SOURCE_DELETE_EXACT: + if (request == NULL || request->key == NULL) + admission = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + else + local_result.bool_value = cluster_tt_status_delete_exact(request->key); + break; + case CLUSTER_TT_SOURCE_RESOLVE_PREPARED_COMMIT: + if (request == NULL) + admission = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + else + local_result.int_value = cluster_tt_status_resolve_prepared_commit( + request->xid, request->commit_scn); + break; + case CLUSTER_TT_SOURCE_BUMP_SELF_CONSUMER_HIT: + cluster_tt_status_bump_self_consumer_hit(); + break; + case CLUSTER_TT_SOURCE_BUMP_PARENT_CHAIN_FOLLOW: + cluster_tt_status_bump_parent_chain_follow(); + break; + default: + admission = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + break; + } + } + + if (admission == CLUSTER_SEMANTIC_ADMISSION_OK + && !cluster_semantic_activation_recheck(&token)) { + memset(&local_result, 0, sizeof(local_result)); + admission = CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED; + } + } + PG_FINALLY(); + { + cluster_semantic_activation_leave(&token); + } + PG_END_TRY(); + + if (admission == CLUSTER_SEMANTIC_ADMISSION_OK) + *result = local_result; + return admission; +} diff --git a/src/include/cluster/cluster_tt_status.h b/src/include/cluster/cluster_tt_status.h index 3be07aa34fc..7ffb1eda839 100644 --- a/src/include/cluster/cluster_tt_status.h +++ b/src/include/cluster/cluster_tt_status.h @@ -64,6 +64,7 @@ #include "c.h" #include "access/transam.h" +#include "cluster/cluster_semantic_activation.h" #include "cluster/cluster_scn.h" /* SCN */ /* @@ -179,6 +180,42 @@ typedef enum ClusterVisibilityDecision { CLUSTER_VISIBILITY_UNKNOWN = 2 } ClusterVisibilityDecision; +/* + * R4 dormant-source TT dispatch. + * + * The legacy overlay operations remain available only behind the common + * semantic-activation admission token. Control-plane flush, shmem and + * counter-reader APIs remain outside this operation domain. + */ +typedef enum ClusterTTStatusSourceOp { + CLUSTER_TT_SOURCE_LOOKUP = 0, + CLUSTER_TT_SOURCE_INSTALL_LOCAL = 1, + CLUSTER_TT_SOURCE_INSTALL_SUBCOMMITTED = 2, + CLUSTER_TT_SOURCE_DELETE_EXACT = 3, + CLUSTER_TT_SOURCE_RESOLVE_PREPARED_COMMIT = 4, + CLUSTER_TT_SOURCE_BUMP_SELF_CONSUMER_HIT = 5, + CLUSTER_TT_SOURCE_BUMP_PARENT_CHAIN_FOLLOW = 6 +} ClusterTTStatusSourceOp; + +typedef struct ClusterTTStatusSourceRequest { + const ClusterTTStatusKey *key; + const ClusterTTStatusKey *parent_key; + ClusterTTStatus status; + SCN commit_scn; + TransactionId xid; +} ClusterTTStatusSourceRequest; + +typedef struct ClusterTTStatusSourceResult { + bool bool_value; + int int_value; + ClusterTTStatusResult lookup; +} ClusterTTStatusSourceResult; + +extern ClusterSemanticAdmissionResult +cluster_tt_status_source_dispatch(ClusterTTStatusSourceOp op, + const ClusterTTStatusSourceRequest *request, + ClusterTTStatusSourceResult *result); + /* * cluster_visibility_decide_by_scn -- spec-3.3 D5 inline helper. * @@ -227,11 +264,6 @@ cluster_visibility_decide_by_scn(SCN commit_scn, SCN read_scn) * cluster_tt_status_shmem_size / _shmem_init / _shmem_register: * shmem layout (D2). */ -extern bool cluster_tt_status_lookup_exact(const ClusterTTStatusKey *key, - ClusterTTStatusResult *result); -extern bool cluster_tt_status_install_local(const ClusterTTStatusKey *key, ClusterTTStatus status, - SCN commit_scn); -extern int cluster_tt_status_resolve_prepared_commit(TransactionId xid, SCN commit_scn); extern void cluster_tt_status_flush_all(uint32 new_epoch); /* @@ -243,8 +275,6 @@ extern void cluster_tt_status_flush_all(uint32 new_epoch); * actually deleted. Spec-3.4c F4: required so test-only clear does * not fake-clear via writing ABORTED (semantic conflict). */ -extern bool cluster_tt_status_delete_exact(const ClusterTTStatusKey *key); - /* * cluster_tt_status_flush_all_at_activation: * spec-3.4b D8 / Q4 HC (L191): code-enforced automatic flush wired @@ -266,8 +296,6 @@ extern void cluster_tt_status_shmem_register(void); * ONLY by D5/D6 commit hook to record the runtime N7 self-consumer * lookup (spec-3.1 v0.4 N7). Do not call from unrelated paths. */ -extern void cluster_tt_status_bump_self_consumer_hit(void); - /* * Counter getters — exposed via pg_cluster_state "tt_status" category * (cluster_debug.c). Always linked (return 0 in disabled-cluster @@ -351,15 +379,11 @@ extern uint64 cluster_tt_status_get_evict_fail_count(void); * ensure parent binding exists first (cluster_subtrans_ensure_parent_binding). * Returns true on install, false if overlay full / unavailable. */ -extern bool cluster_tt_status_install_subcommitted(const ClusterTTStatusKey *child_key, - const ClusterTTStatusKey *parent_key); - /* * Counter getters for spec-3.5 SUBCOMMITTED path (always linked). */ extern uint64 cluster_tt_status_get_subcommitted_install_count(void); extern uint64 cluster_tt_status_get_subcommitted_lookup_hit_count(void); extern uint64 cluster_tt_status_get_parent_chain_follow_count(void); -extern void cluster_tt_status_bump_parent_chain_follow(void); #endif /* CLUSTER_TT_STATUS_H */ diff --git a/src/test/cluster_unit/test_cluster_r4_d10_tt_source.c b/src/test/cluster_unit/test_cluster_r4_d10_tt_source.c new file mode 100644 index 00000000000..bf298f33466 --- /dev/null +++ b/src/test/cluster_unit/test_cluster_r4_d10_tt_source.c @@ -0,0 +1,579 @@ +/*------------------------------------------------------------------------- + * + * test_cluster_r4_d10_tt_source.c + * R4 D10 TT dormant-source dispatch tests. + * + * 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_r4_d10_tt_source.c + * + * NOTES + * This is a pgrac-original file. It links the production TT-status + * object and replaces only its PostgreSQL runtime dependencies. + * + *------------------------------------------------------------------------- + */ +#define USE_PGRAC_CLUSTER 1 + +#include "postgres.h" + +#include "access/xlog.h" +#include "cluster/cluster_epoch.h" +#include "cluster/cluster_guc.h" +#include "cluster/cluster_recovery_merge.h" +#include "cluster/cluster_remote_xact.h" +#include "cluster/cluster_semantic_activation.h" +#include "cluster/cluster_shmem.h" +#include "cluster/cluster_tt_durable.h" +#include "cluster/cluster_tt_status.h" +#include "miscadmin.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" +#include "utils/elog.h" +#include "utils/hsearch.h" +#include "utils/timestamp.h" + +#undef printf +#undef fprintf +#undef snprintf +#undef sprintf +#undef vsnprintf +#undef vfprintf +#undef vprintf +#undef vsprintf +#undef strerror +#undef strerror_r + +#include "unit_test.h" + +#include + + +UT_DEFINE_GLOBALS(); + + +bool cluster_enabled = true; +int cluster_node_id = 1; +bool cluster_enable_adg = false; +int cluster_dg_role = CLUSTER_DG_ROLE_PRIMARY; +bool cluster_tt_durable_lookup = false; +bool cluster_cf_terminal_authority = false; +int cluster_tt_status_overlay_max_entries = 16; +int cluster_tt_status_overlay_ttl_ms = 0; +ProcessingMode Mode = NormalProcessing; + +sigjmp_buf *PG_exception_stack = NULL; +ErrorContextCallback *error_context_stack = NULL; + +static char fake_state_storage[4096] pg_attribute_aligned(64); +static char fake_lock_storage[sizeof(LWLockPadded)] pg_attribute_aligned(64); +static char fake_hash_storage[256] pg_attribute_aligned(64); +static char fake_hash_handle[64] pg_attribute_aligned(64); +static bool fake_state_found; +static bool fake_lock_found; +static bool fake_hash_present; +static bool fake_hash_raise; +static bool fake_seq_returned; +static TimestampTz fake_now = 1000000; + +static ClusterSemanticAdmissionResult fake_admission; +static bool fake_recheck; +static int fake_enter_count; +static int fake_recheck_count; +static int fake_leave_count; + + +void +ExceptionalCondition(const char *condition_name pg_attribute_unused(), + const char *file_name pg_attribute_unused(), + int line_number pg_attribute_unused()) +{ + abort(); +} + +void +pg_re_throw(void) +{ + if (PG_exception_stack != NULL) + siglongjmp(*PG_exception_stack, 1); + abort(); +} + +bool +errstart(int elevel pg_attribute_unused(), const char *domain pg_attribute_unused()) +{ + return true; +} + +bool +errstart_cold(int elevel, const char *domain) +{ + return errstart(elevel, domain); +} + +void +errfinish(const char *filename pg_attribute_unused(), int lineno pg_attribute_unused(), + const char *funcname pg_attribute_unused()) +{ + abort(); +} + +int +errcode(int sqlerrcode pg_attribute_unused()) +{ + return 0; +} + +int +errmsg(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +int +errdetail(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +int +errhint(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +ClusterSemanticAdmissionResult +cluster_semantic_activation_enter(uint64 feature_bit, ClusterSemanticAdmissionSide side, + ClusterSemanticAdmissionToken *token) +{ + fake_enter_count++; + UT_ASSERT_EQ(feature_bit, CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1); + UT_ASSERT_EQ(side, CLUSTER_SEMANTIC_SOURCE_SIDE); + memset(token, 0, sizeof(*token)); + if (fake_admission == CLUSTER_SEMANTIC_ADMISSION_OK) { + token->feature_bit = feature_bit; + token->record_generation = 19; + token->formation_epoch = 7; + token->side = (uint8)side; + token->entered = true; + } + return fake_admission; +} + +bool +cluster_semantic_activation_recheck(const ClusterSemanticAdmissionToken *token) +{ + fake_recheck_count++; + UT_ASSERT(token->entered); + return fake_recheck; +} + +void +cluster_semantic_activation_leave(ClusterSemanticAdmissionToken *token) +{ + fake_leave_count++; + UT_ASSERT(token->entered); + token->entered = false; +} + +void * +ShmemInitStruct(const char *name, Size size pg_attribute_unused(), bool *found_ptr) +{ + if (strcmp(name, "ClusterTTStatusState") == 0) { + *found_ptr = fake_state_found; + fake_state_found = true; + return fake_state_storage; + } + + UT_ASSERT(strcmp(name, "ClusterTTStatusLock") == 0); + *found_ptr = fake_lock_found; + fake_lock_found = true; + return fake_lock_storage; +} + +HTAB * +ShmemInitHash(const char *name pg_attribute_unused(), long init_size pg_attribute_unused(), + long max_size pg_attribute_unused(), HASHCTL *info_ptr pg_attribute_unused(), + int hash_flags pg_attribute_unused()) +{ + return (HTAB *)fake_hash_handle; +} + +void * +hash_search(HTAB *hashp pg_attribute_unused(), const void *key_ptr pg_attribute_unused(), + HASHACTION action, bool *found_ptr) +{ + if (fake_hash_raise) + siglongjmp(*PG_exception_stack, 1); + + switch (action) { + case HASH_FIND: + if (found_ptr != NULL) + *found_ptr = fake_hash_present; + return fake_hash_present ? fake_hash_storage : NULL; + case HASH_ENTER: + case HASH_ENTER_NULL: + if (found_ptr != NULL) + *found_ptr = fake_hash_present; + fake_hash_present = true; + return fake_hash_storage; + case HASH_REMOVE: + if (found_ptr != NULL) + *found_ptr = fake_hash_present; + if (!fake_hash_present) + return NULL; + fake_hash_present = false; + return fake_hash_storage; + default: + abort(); + } +} + +void +hash_seq_init(HASH_SEQ_STATUS *status pg_attribute_unused(), HTAB *hashp pg_attribute_unused()) +{ + fake_seq_returned = false; +} + +void * +hash_seq_search(HASH_SEQ_STATUS *status pg_attribute_unused()) +{ + if (!fake_hash_present || fake_seq_returned) + return NULL; + fake_seq_returned = true; + return fake_hash_storage; +} + +void +hash_seq_term(HASH_SEQ_STATUS *status pg_attribute_unused()) +{} + +Size +hash_estimate_size(long num_entries, Size entry_size) +{ + return (Size)num_entries * entry_size; +} + +Size +add_size(Size size_a, Size size_b) +{ + return size_a + size_b; +} + +bool +LWLockAcquire(LWLock *lock pg_attribute_unused(), LWLockMode mode pg_attribute_unused()) +{ + return true; +} + +void +LWLockRelease(LWLock *lock pg_attribute_unused()) +{} + +void +LWLockInitialize(LWLock *lock pg_attribute_unused(), int tranche_id pg_attribute_unused()) +{} + +TimestampTz +GetCurrentTimestamp(void) +{ + return fake_now; +} + +bool +RecoveryInProgress(void) +{ + return false; +} + +bool +TransactionIdDidCommit(TransactionId xid pg_attribute_unused()) +{ + return false; +} + +uint64 +cluster_epoch_get_current(void) +{ + return 7; +} + +bool +cluster_merged_instance_is_materialized(int node_id pg_attribute_unused()) +{ + return false; +} + +ClusterRemoteXactOutcome +cluster_remote_outcome_terminal_authorized(int origin_node pg_attribute_unused(), + TransactionId xid pg_attribute_unused(), + uint64 observed_epoch pg_attribute_unused(), + uint64 current_epoch pg_attribute_unused(), + bool retention_required pg_attribute_unused(), + bool retention_proven pg_attribute_unused(), + SCN *out_scn pg_attribute_unused()) +{ + return CLUSTER_REMOTE_XACT_INDOUBT; +} + +ClusterRemoteXactOutcome +cluster_remote_outcome_durable_checked(int origin_node pg_attribute_unused(), + TransactionId xid pg_attribute_unused(), + SCN *out_scn pg_attribute_unused()) +{ + return CLUSTER_REMOTE_XACT_INDOUBT; +} + +bool +cluster_tt_slot_durable_lookup_committed_stable( + uint32 segment_id pg_attribute_unused(), uint16 slot_offset pg_attribute_unused(), + TransactionId xid pg_attribute_unused(), uint32 expected_wrap pg_attribute_unused(), + ClusterTTDurableXidCommitCheck xid_committed pg_attribute_unused(), + SCN *commit_scn pg_attribute_unused()) +{ + return false; +} + +ClusterTTDurableResolve +cluster_tt_slot_durable_resolve_by_xid_origin(int origin_node pg_attribute_unused(), + TransactionId xid pg_attribute_unused(), + uint32 expected_wrap pg_attribute_unused(), + SCN *commit_scn pg_attribute_unused(), + uint16 *out_seg pg_attribute_unused(), + uint16 *out_slot pg_attribute_unused(), + uint16 *out_wrap pg_attribute_unused()) +{ + return CLUSTER_TT_DURABLE_RECYCLED_ZERO_MATCH; +} + +void +cluster_shmem_register_region(const ClusterShmemRegion *region pg_attribute_unused()) +{} + +static void +reset_fixture(void) +{ + memset(fake_state_storage, 0, sizeof(fake_state_storage)); + memset(fake_lock_storage, 0, sizeof(fake_lock_storage)); + memset(fake_hash_storage, 0, sizeof(fake_hash_storage)); + memset(fake_hash_handle, 0, sizeof(fake_hash_handle)); + fake_state_found = false; + fake_lock_found = false; + fake_hash_present = false; + fake_hash_raise = false; + fake_seq_returned = false; + fake_admission = CLUSTER_SEMANTIC_ADMISSION_OK; + fake_recheck = true; + fake_enter_count = 0; + fake_recheck_count = 0; + fake_leave_count = 0; + cluster_tt_status_shmem_init(); +} + +static ClusterTTStatusKey +make_key(TransactionId xid) +{ + ClusterTTStatusKey key; + + memset(&key, 0, sizeof(key)); + key.origin_node_id = 1; + key.undo_segment_id = 2; + key.tt_slot_id = 3; + key.cluster_epoch = 7; + key.local_xid = xid; + return key; +} + +static bool +bytes_are_zero(const void *ptr, Size size) +{ + const unsigned char *bytes = ptr; + + while (size-- > 0) { + if (*bytes++ != 0) + return false; + } + return true; +} + +UT_TEST(test_frozen_tt_source_operation_domain) +{ + UT_ASSERT_EQ(CLUSTER_TT_SOURCE_LOOKUP, 0); + UT_ASSERT_EQ(CLUSTER_TT_SOURCE_INSTALL_LOCAL, 1); + UT_ASSERT_EQ(CLUSTER_TT_SOURCE_INSTALL_SUBCOMMITTED, 2); + UT_ASSERT_EQ(CLUSTER_TT_SOURCE_DELETE_EXACT, 3); + UT_ASSERT_EQ(CLUSTER_TT_SOURCE_RESOLVE_PREPARED_COMMIT, 4); + UT_ASSERT_EQ(CLUSTER_TT_SOURCE_BUMP_SELF_CONSUMER_HIT, 5); + UT_ASSERT_EQ(CLUSTER_TT_SOURCE_BUMP_PARENT_CHAIN_FOLLOW, 6); +} + +UT_TEST(test_active_refuses_before_request_inspection_and_mutation) +{ + ClusterTTStatusSourceResult result; + ClusterSemanticAdmissionResult admission; + + reset_fixture(); + fake_admission = CLUSTER_SEMANTIC_ADMISSION_SOURCE_DORMANT; + memset(&result, 0xA5, sizeof(result)); + admission = cluster_tt_status_source_dispatch( + CLUSTER_TT_SOURCE_LOOKUP, (const ClusterTTStatusSourceRequest *)(uintptr_t)1, &result); + + UT_ASSERT_EQ(admission, CLUSTER_SEMANTIC_ADMISSION_SOURCE_DORMANT); + UT_ASSERT(bytes_are_zero(&result, sizeof(result))); + UT_ASSERT_EQ(fake_enter_count, 1); + UT_ASSERT_EQ(fake_recheck_count, 0); + UT_ASSERT_EQ(fake_leave_count, 0); + UT_ASSERT_EQ(cluster_tt_status_get_lookup_hit_count(), 0); +} + +UT_TEST(test_disabled_source_executes_matching_counter_arm) +{ + ClusterTTStatusSourceResult result; + + reset_fixture(); + memset(&result, 0xA5, sizeof(result)); + UT_ASSERT_EQ( + cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_BUMP_SELF_CONSUMER_HIT, NULL, &result), + CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT(bytes_are_zero(&result, sizeof(result))); + UT_ASSERT_EQ(cluster_tt_status_get_self_consumer_hit_count(), 1); + UT_ASSERT_EQ(fake_enter_count, 1); + UT_ASSERT_EQ(fake_recheck_count, 1); + UT_ASSERT_EQ(fake_leave_count, 1); +} + +UT_TEST(test_install_lookup_and_delete_publish_only_after_recheck) +{ + ClusterTTStatusKey key = make_key((TransactionId)601); + ClusterTTStatusSourceRequest request; + ClusterTTStatusSourceResult result; + + reset_fixture(); + memset(&request, 0, sizeof(request)); + request.key = &key; + request.status = CLUSTER_TT_STATUS_COMMITTED; + request.commit_scn = UINT64_C(9001); + UT_ASSERT_EQ( + cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_LOCAL, &request, &result), + CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT(result.bool_value); + + memset(&result, 0xA5, sizeof(result)); + UT_ASSERT_EQ(cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, &request, &result), + CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT(result.bool_value); + UT_ASSERT(result.lookup.authoritative); + UT_ASSERT_EQ(result.lookup.status, CLUSTER_TT_STATUS_COMMITTED); + UT_ASSERT_EQ(result.lookup.commit_scn, UINT64_C(9001)); + + UT_ASSERT_EQ( + cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_DELETE_EXACT, &request, &result), + CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT(result.bool_value); + UT_ASSERT_EQ(fake_enter_count, 3); + UT_ASSERT_EQ(fake_recheck_count, 3); + UT_ASSERT_EQ(fake_leave_count, 3); +} + +UT_TEST(test_missing_required_pointer_closes_after_admission) +{ + ClusterTTStatusSourceRequest request; + ClusterTTStatusSourceResult result; + + reset_fixture(); + memset(&request, 0, sizeof(request)); + memset(&result, 0xA5, sizeof(result)); + UT_ASSERT_EQ(cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, &request, &result), + CLUSTER_SEMANTIC_ADMISSION_CLOSED); + UT_ASSERT(bytes_are_zero(&result, sizeof(result))); + UT_ASSERT_EQ(fake_enter_count, 1); + UT_ASSERT_EQ(fake_recheck_count, 0); + UT_ASSERT_EQ(fake_leave_count, 1); +} + +UT_TEST(test_unknown_operation_closes_after_admission) +{ + ClusterTTStatusSourceResult result; + + reset_fixture(); + memset(&result, 0xA5, sizeof(result)); + UT_ASSERT_EQ(cluster_tt_status_source_dispatch((ClusterTTStatusSourceOp)99, NULL, &result), + CLUSTER_SEMANTIC_ADMISSION_CLOSED); + UT_ASSERT(bytes_are_zero(&result, sizeof(result))); + UT_ASSERT_EQ(fake_enter_count, 1); + UT_ASSERT_EQ(fake_recheck_count, 0); + UT_ASSERT_EQ(fake_leave_count, 1); +} + +UT_TEST(test_generation_drift_discards_fixed_result) +{ + ClusterTTStatusKey key = make_key((TransactionId)602); + ClusterTTStatusSourceRequest request; + ClusterTTStatusSourceResult result; + + reset_fixture(); + fake_recheck = false; + memset(&request, 0, sizeof(request)); + request.key = &key; + request.status = CLUSTER_TT_STATUS_COMMITTED; + request.commit_scn = UINT64_C(9002); + memset(&result, 0xA5, sizeof(result)); + UT_ASSERT_EQ( + cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_LOCAL, &request, &result), + CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED); + UT_ASSERT(bytes_are_zero(&result, sizeof(result))); + UT_ASSERT_EQ(cluster_tt_status_get_install_count(), 1); + UT_ASSERT_EQ(fake_recheck_count, 1); + UT_ASSERT_EQ(fake_leave_count, 1); +} + +UT_TEST(test_error_path_runs_single_leave_funnel) +{ + ClusterTTStatusKey key = make_key((TransactionId)603); + ClusterTTStatusSourceRequest request; + ClusterTTStatusSourceResult result; + volatile bool caught = false; + + reset_fixture(); + memset(&request, 0, sizeof(request)); + request.key = &key; + request.status = CLUSTER_TT_STATUS_COMMITTED; + request.commit_scn = UINT64_C(9003); + fake_hash_raise = true; + + PG_TRY(); + { + (void)cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_LOCAL, &request, &result); + } + PG_CATCH(); + { + caught = true; + } + PG_END_TRY(); + + UT_ASSERT(caught); + UT_ASSERT_EQ(fake_enter_count, 1); + UT_ASSERT_EQ(fake_recheck_count, 0); + UT_ASSERT_EQ(fake_leave_count, 1); +} + +int +main(void) +{ + UT_PLAN(8); + UT_RUN(test_frozen_tt_source_operation_domain); + UT_RUN(test_active_refuses_before_request_inspection_and_mutation); + UT_RUN(test_disabled_source_executes_matching_counter_arm); + UT_RUN(test_install_lookup_and_delete_publish_only_after_recheck); + UT_RUN(test_missing_required_pointer_closes_after_admission); + UT_RUN(test_unknown_operation_closes_after_admission); + UT_RUN(test_generation_drift_discards_fixed_result); + UT_RUN(test_error_path_runs_single_leave_funnel); + UT_DONE(); + return ut_failed_count == 0 ? 0 : 1; +} From b506ffe588f1980551ac2634655335f826d725f3 Mon Sep 17 00:00:00 2001 From: SqlRush Date: Mon, 10 Aug 2026 10:08:01 +0800 Subject: [PATCH 5/8] feat(cluster): gate MultiXact source dispatch --- src/backend/cluster/cluster_multixact.c | 192 ++++-- src/include/cluster/cluster_multixact.h | 116 +--- .../test_cluster_r4_d10_multi_source.c | 647 ++++++++++++++++++ 3 files changed, 837 insertions(+), 118 deletions(-) create mode 100644 src/test/cluster_unit/test_cluster_r4_d10_multi_source.c diff --git a/src/backend/cluster/cluster_multixact.c b/src/backend/cluster/cluster_multixact.c index ca700dc563c..dc53e18bd17 100644 --- a/src/backend/cluster/cluster_multixact.c +++ b/src/backend/cluster/cluster_multixact.c @@ -156,9 +156,9 @@ cluster_multixact_shmem_register(void) /* Public API */ /* ------------------------------------------------------------ */ -bool -cluster_multixact_member_overlay_install(const ClusterMultiXactKey *key, uint16 member_count, - const ClusterMultiXactMember *members) +static bool +cluster_multixact_member_overlay_install_raw(const ClusterMultiXactKey *key, uint16 member_count, + const ClusterMultiXactMember *members) { ClusterMultiXactOverlayEntry *e; bool found; @@ -195,10 +195,10 @@ cluster_multixact_member_overlay_install(const ClusterMultiXactKey *key, uint16 return true; } -bool -cluster_multixact_member_overlay_lookup(const ClusterMultiXactKey *key, - ClusterMultiXactMemberOverlayResult *out, - int max_members_buf) +static bool +cluster_multixact_member_overlay_lookup_raw(const ClusterMultiXactKey *key, + ClusterMultiXactMemberOverlayResult *out, + int max_members_buf) { const ClusterMultiXactOverlayEntry *e; uint32 current_epoch; @@ -260,9 +260,9 @@ cluster_multixact_member_overlay_lookup(const ClusterMultiXactKey *key, * exact key fields carried in ClusterMultiXactMember; miss -> UNKNOWN * per L199. */ -ClusterVisibilityDecision -cluster_multixact_resolve_visibility(const ClusterMultiXactMemberOverlayResult *overlay, - const Snapshot snap) +static ClusterVisibilityDecision +cluster_multixact_resolve_visibility_raw(const ClusterMultiXactMemberOverlayResult *overlay, + const Snapshot snap) { uint16 i; @@ -438,9 +438,9 @@ cluster_multixact_remote_xmax_ask_origin(uint16 origin_slot, MultiXactId mxid, S * updater-multi, IN-12) the D3-b origin member-verdict ask decides. See * header; UNKNOWN always means fail closed at the caller. */ -ClusterVisibilityDecision -cluster_multixact_remote_xmax_resolve(uint16 origin_slot, MultiXactId mxid, Snapshot snap, - bool *overlay_hit) +static ClusterVisibilityDecision +cluster_multixact_remote_xmax_resolve_raw(uint16 origin_slot, MultiXactId mxid, Snapshot snap, + bool *overlay_hit) { ClusterMultiXactKey mxkey; ClusterMultiXactMemberOverlayResult *mxres; @@ -458,7 +458,8 @@ cluster_multixact_remote_xmax_resolve(uint16 origin_slot, MultiXactId mxid, Snap mxres = (ClusterMultiXactMemberOverlayResult *)palloc0(resbuf_sz); - if (!cluster_multixact_member_overlay_lookup(&mxkey, mxres, CLUSTER_MULTIXACT_MAX_MEMBERS)) { + if (!cluster_multixact_member_overlay_lookup_raw(&mxkey, mxres, + CLUSTER_MULTIXACT_MAX_MEMBERS)) { pfree(mxres); /* spec-7.1 D3-b: overlay miss -> ask the origin (banner). */ return cluster_multixact_remote_xmax_ask_origin(origin_slot, mxid, snap); @@ -466,13 +467,13 @@ cluster_multixact_remote_xmax_resolve(uint16 origin_slot, MultiXactId mxid, Snap if (overlay_hit) *overlay_hit = true; - decision = cluster_multixact_resolve_visibility(mxres, snap); + decision = cluster_multixact_resolve_visibility_raw(mxres, snap); pfree(mxres); return decision; } -uint16 -cluster_multixact_get_member_count(const ClusterMultiXactKey *key) +static uint16 +cluster_multixact_get_member_count_raw(const ClusterMultiXactKey *key) { const ClusterMultiXactOverlayEntry *e; uint16 count = 0; @@ -533,15 +534,15 @@ CLUSTER_MULTIXACT_GETTER(mxid_underivable_read_count) * legs in heapam_visibility.c (underivable read); atomics, no lock of * this module taken. */ -void -cluster_multixact_note_halfspace_refuse(void) +static void +cluster_multixact_note_halfspace_refuse_raw(void) { if (ClusterMultiXactState != NULL) pg_atomic_fetch_add_u64(&ClusterMultiXactState->mxid_halfspace_refuse_count, 1); } -void -cluster_multixact_note_underivable_read(void) +static void +cluster_multixact_note_underivable_read_raw(void) { if (ClusterMultiXactState != NULL) pg_atomic_fetch_add_u64(&ClusterMultiXactState->mxid_underivable_read_count, 1); @@ -561,9 +562,9 @@ void cluster_multixact_shmem_register(void) {} -bool -cluster_multixact_member_overlay_install(const ClusterMultiXactKey *key, uint16 member_count, - const ClusterMultiXactMember *members) +static bool +cluster_multixact_member_overlay_install_raw(const ClusterMultiXactKey *key, uint16 member_count, + const ClusterMultiXactMember *members) { (void)key; (void)member_count; @@ -571,10 +572,10 @@ cluster_multixact_member_overlay_install(const ClusterMultiXactKey *key, uint16 return false; } -bool -cluster_multixact_member_overlay_lookup(const ClusterMultiXactKey *key, - ClusterMultiXactMemberOverlayResult *out, - int max_members_buf) +static bool +cluster_multixact_member_overlay_lookup_raw(const ClusterMultiXactKey *key, + ClusterMultiXactMemberOverlayResult *out, + int max_members_buf) { (void)key; (void)max_members_buf; @@ -586,18 +587,18 @@ cluster_multixact_member_overlay_lookup(const ClusterMultiXactKey *key, return false; } -ClusterVisibilityDecision -cluster_multixact_resolve_visibility(const ClusterMultiXactMemberOverlayResult *overlay, - const Snapshot snap) +static ClusterVisibilityDecision +cluster_multixact_resolve_visibility_raw(const ClusterMultiXactMemberOverlayResult *overlay, + const Snapshot snap) { (void)overlay; (void)snap; return CLUSTER_VISIBILITY_UNKNOWN; } -ClusterVisibilityDecision -cluster_multixact_remote_xmax_resolve(uint16 origin_slot, MultiXactId mxid, Snapshot snap, - bool *overlay_hit) +static ClusterVisibilityDecision +cluster_multixact_remote_xmax_resolve_raw(uint16 origin_slot, MultiXactId mxid, Snapshot snap, + bool *overlay_hit) { (void)origin_slot; (void)mxid; @@ -607,8 +608,8 @@ cluster_multixact_remote_xmax_resolve(uint16 origin_slot, MultiXactId mxid, Snap return CLUSTER_VISIBILITY_UNKNOWN; } -uint16 -cluster_multixact_get_member_count(const ClusterMultiXactKey *key) +static uint16 +cluster_multixact_get_member_count_raw(const ClusterMultiXactKey *key) { (void)key; return 0; @@ -634,12 +635,123 @@ CLUSTER_MULTIXACT_GETTER_STUB(resolve_visibility_count) CLUSTER_MULTIXACT_GETTER_STUB(mxid_halfspace_refuse_count) CLUSTER_MULTIXACT_GETTER_STUB(mxid_underivable_read_count) -void -cluster_multixact_note_halfspace_refuse(void) +static void +cluster_multixact_note_halfspace_refuse_raw(void) {} -void -cluster_multixact_note_underivable_read(void) +static void +cluster_multixact_note_underivable_read_raw(void) {} #endif /* USE_PGRAC_CLUSTER */ + +static ClusterSemanticAdmissionResult +cluster_multixact_source_dispatch_body(ClusterMultiXactSourceOp op, + const ClusterMultiXactSourceRequest *request, + ClusterMultiXactSourceResult *result) +{ + switch (op) { + case CLUSTER_MULTI_SOURCE_OVERLAY_INSTALL: + if (request == NULL || request->key == NULL || request->members == NULL) + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; + result->bool_value = cluster_multixact_member_overlay_install_raw( + request->key, request->member_count, request->members); + break; + case CLUSTER_MULTI_SOURCE_OVERLAY_LOOKUP: + if (request == NULL || request->key == NULL || request->overlay_out == NULL) + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; + result->bool_value = cluster_multixact_member_overlay_lookup_raw( + request->key, request->overlay_out, request->max_members_buf); + if (result->bool_value) + result->member_count = request->overlay_out->member_count; + break; + case CLUSTER_MULTI_SOURCE_RESOLVE_VISIBILITY: + if (request == NULL || request->overlay_in == NULL || request->snapshot == NULL) + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; + result->visibility + = cluster_multixact_resolve_visibility_raw(request->overlay_in, request->snapshot); + break; + case CLUSTER_MULTI_SOURCE_GET_MEMBER_COUNT: + if (request == NULL || request->key == NULL) + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; + result->member_count = cluster_multixact_get_member_count_raw(request->key); + break; + case CLUSTER_MULTI_SOURCE_REMOTE_XMAX_RESOLVE: + if (request == NULL || request->snapshot == NULL) + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; + result->visibility = cluster_multixact_remote_xmax_resolve_raw( + request->origin_slot, request->mxid, request->snapshot, &result->overlay_hit); + break; + case CLUSTER_MULTI_SOURCE_NOTE_HALFSPACE_REFUSE: + cluster_multixact_note_halfspace_refuse_raw(); + break; + case CLUSTER_MULTI_SOURCE_NOTE_UNDERIVABLE_READ: + cluster_multixact_note_underivable_read_raw(); + break; + default: + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; + } + + return CLUSTER_SEMANTIC_ADMISSION_OK; +} + +/* + * cluster_multixact_source_dispatch -- gate one legacy source operation. + * + * Inputs: + * op: typed operation selector. + * request: operation arguments; required pointers are checked after entry. + * result: fixed result storage, canonical-zeroed before admission. + * + * Returns: + * The semantic admission result. OK means the fixed result is consumable. + * + * Side Effects: + * On OK admission, invokes exactly one module-private source operation and + * balances its admission token on normal and ERROR paths. + */ +ClusterSemanticAdmissionResult +cluster_multixact_source_dispatch(ClusterMultiXactSourceOp op, + const ClusterMultiXactSourceRequest *request, + ClusterMultiXactSourceResult *result) +{ + ClusterSemanticAdmissionToken token; + ClusterMultiXactSourceResult local_result; + ClusterSemanticAdmissionResult admission; + volatile bool caught_error = false; + + memset(&token, 0, sizeof(token)); + memset(&local_result, 0, sizeof(local_result)); + if (result != NULL) + memset(result, 0, sizeof(*result)); + + admission = cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &token); + if (admission != CLUSTER_SEMANTIC_ADMISSION_OK) + return admission; + + PG_TRY(); + { + if (result == NULL) + admission = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + else + admission = cluster_multixact_source_dispatch_body(op, request, &local_result); + + if (admission == CLUSTER_SEMANTIC_ADMISSION_OK) { + if (!cluster_semantic_activation_recheck(&token)) + admission = CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED; + else + *result = local_result; + } + } + PG_CATCH(); + { + caught_error = true; + } + PG_END_TRY(); + + cluster_semantic_activation_leave(&token); + if (caught_error) + PG_RE_THROW(); + return admission; +} diff --git a/src/include/cluster/cluster_multixact.h b/src/include/cluster/cluster_multixact.h index 9c7aaa91154..8f145da083f 100644 --- a/src/include/cluster/cluster_multixact.h +++ b/src/include/cluster/cluster_multixact.h @@ -56,6 +56,7 @@ #include "c.h" #include "access/transam.h" #include "access/multixact.h" /* MultiXactId + MultiXactStatus */ +#include "cluster/cluster_semantic_activation.h" #include "cluster/cluster_tt_status.h" #include "utils/snapshot.h" @@ -167,56 +168,49 @@ typedef struct ClusterMultiXactMemberOverlayResult { ClusterMultiXactMember members[FLEXIBLE_ARRAY_MEMBER]; } ClusterMultiXactMemberOverlayResult; +typedef enum ClusterMultiXactSourceOp { + CLUSTER_MULTI_SOURCE_OVERLAY_INSTALL = 0, + CLUSTER_MULTI_SOURCE_OVERLAY_LOOKUP = 1, + CLUSTER_MULTI_SOURCE_RESOLVE_VISIBILITY = 2, + CLUSTER_MULTI_SOURCE_GET_MEMBER_COUNT = 3, + CLUSTER_MULTI_SOURCE_REMOTE_XMAX_RESOLVE = 4, + CLUSTER_MULTI_SOURCE_NOTE_HALFSPACE_REFUSE = 5, + CLUSTER_MULTI_SOURCE_NOTE_UNDERIVABLE_READ = 6 +} ClusterMultiXactSourceOp; + +typedef struct ClusterMultiXactSourceRequest { + const ClusterMultiXactKey *key; + uint16 member_count; + const ClusterMultiXactMember *members; + ClusterMultiXactMemberOverlayResult *overlay_out; + int max_members_buf; + const ClusterMultiXactMemberOverlayResult *overlay_in; + Snapshot snapshot; + uint16 origin_slot; + MultiXactId mxid; +} ClusterMultiXactSourceRequest; + +typedef struct ClusterMultiXactSourceResult { + bool bool_value; + uint16 member_count; + ClusterVisibilityDecision visibility; + bool overlay_hit; +} ClusterMultiXactSourceResult; + /* ------------------------------------------------------------ */ /* Public API */ /* ------------------------------------------------------------ */ /* - * cluster_multixact_member_overlay_install (spec-3.6 D2) - * - * Install or overwrite an overlay entry for `key` with `member_count` - * members. Caller is D5 multixact.c hook (local-all-member emit path) - * or D4 V4 wire receiver. Returns true on success, false on overlay - * full / member_count > GUC cap (caller increments overlay_overflow_count). - */ -extern bool cluster_multixact_member_overlay_install(const ClusterMultiXactKey *key, - uint16 member_count, - const ClusterMultiXactMember *members); - -/* - * cluster_multixact_member_overlay_lookup (spec-3.6 D2) + * cluster_multixact_source_dispatch -- admit one dormant source operation. * - * Look up overlay entry by exact key. Writes result + copies members - * into caller buffer (up to max_members_buf entries). Returns true on - * hit + authoritative=true; false on miss + bumps overlay_miss_count. - * Caller raises 53R9C on miss (per L199 fail-closed). + * The typed result is canonical-zero on refusal or generation drift. + * Callers may consume overlay_out only when the return value is OK and + * the operation's fixed success field is positive. */ -extern bool cluster_multixact_member_overlay_lookup(const ClusterMultiXactKey *key, - ClusterMultiXactMemberOverlayResult *out, - int max_members_buf); - -/* - * cluster_multixact_resolve_visibility (spec-3.6 D2 core helper) - * - * Given a hit overlay result + current snapshot, compute the - * visibility decision combining per-member MultiXactStatus - * (0-3 lock-only; 4-5 Update/NoKeyUpdate) with per-member commit/ - * abort/in-progress status (via spec-3.2 cluster_tt_status_lookup_exact). - * - * Truth table (per OBS-1 amend MVCC-accurate): - * lock-only ANY state → VISIBLE - * Update/NoKeyUpdate ABORTED → VISIBLE - * Update/NoKeyUpdate IN_PROGRESS (authoritative) → VISIBLE - * Update/NoKeyUpdate COMMITTED + scn <= read_scn → INVISIBLE - * Update/NoKeyUpdate COMMITTED + scn > read_scn → VISIBLE - * ANY UNKNOWN / TT miss / overlay miss → UNKNOWN - * - * UNKNOWN → caller raises 53R9C (per L199 NOT PG-native fallback). - * Pure / no syscall / no wait (L177 hot path). - */ -extern ClusterVisibilityDecision -cluster_multixact_resolve_visibility(const ClusterMultiXactMemberOverlayResult *overlay, - const Snapshot snap); +extern ClusterSemanticAdmissionResult cluster_multixact_source_dispatch( + ClusterMultiXactSourceOp op, const ClusterMultiXactSourceRequest *request, + ClusterMultiXactSourceResult *result); /* * spec-7.1 D3-b: one multixact member's origin-SERVED terminal verdict. @@ -243,7 +237,7 @@ typedef struct ClusterMultiXactServedMember { * * Pure combination resolver for a foreign multixact xmax whose members' * terminal states were SERVED by the origin (no local TT lookup). Mirrors - * cluster_multixact_resolve_visibility's decision structure verbatim, but + * the local overlay resolver's decision structure verbatim, but * the per-updater-member terminal comes from the served verdict instead of * cluster_tt_status_lookup_exact. 8.A: any updater member without a proven * terminal (verdict 0 / inadmissible below-horizon / unknown) -> UNKNOWN @@ -254,14 +248,6 @@ extern ClusterVisibilityDecision cluster_multixact_resolve_visibility_served(const ClusterMultiXactServedMember *members, uint16 member_count, SCN read_scn); -/* - * cluster_multixact_get_member_count (spec-3.6 D2) - * - * Return member_count of overlay entry for `key`, or 0 on miss. - * Used by D6 to size lookup buffer. - */ -extern uint16 cluster_multixact_get_member_count(const ClusterMultiXactKey *key); - /* * cluster_multixact_purge_epoch (spec-3.6 D2) * @@ -270,22 +256,6 @@ extern uint16 cluster_multixact_get_member_count(const ClusterMultiXactKey *key) */ extern void cluster_multixact_purge_epoch(uint32 obsolete_epoch); -/* - * cluster_multixact_remote_xmax_resolve (spec-7.1 D3-a) - * - * One-call reader helper for a DERIVED-foreign multixact xmax: - * builds the overlay key {origin_slot, mxid, current epoch}, looks - * up the member overlay and resolves visibility against snap per - * the OBS-1 truth table. *overlay_hit reports whether the overlay - * held the entry (miss -> UNKNOWN; the member-serve wire that would - * answer a miss positively is a later deliverable). UNKNOWN always - * means the caller must fail closed (rule 8.A). - */ -extern ClusterVisibilityDecision cluster_multixact_remote_xmax_resolve(uint16 origin_slot, - MultiXactId mxid, - Snapshot snap, - bool *overlay_hit); - /* * Counter getters (always linked;return 0 in disable-cluster build). */ @@ -297,16 +267,6 @@ extern uint64 cluster_multixact_get_resolve_visibility_count(void); extern uint64 cluster_multixact_get_mxid_halfspace_refuse_count(void); extern uint64 cluster_multixact_get_mxid_underivable_read_count(void); -/* - * spec-7.1 D3-a guardrail bumps (no-op in disable-cluster build). - * halfspace_refuse: the striped allocator refused a candidate at or - * beyond floor + 2^31 (53RB4). underivable_read: a reader met a - * foreign-evidence multixact whose origin could not be derived - * (below-floor / unlatched / beyond-half-space) and failed closed. - */ -extern void cluster_multixact_note_halfspace_refuse(void); -extern void cluster_multixact_note_underivable_read(void); - /* * Shmem hooks (defined in cluster_multixact.c when USE_PGRAC_CLUSTER; * disable-cluster stubs return 0 / no-op). diff --git a/src/test/cluster_unit/test_cluster_r4_d10_multi_source.c b/src/test/cluster_unit/test_cluster_r4_d10_multi_source.c new file mode 100644 index 00000000000..c6620ff89a2 --- /dev/null +++ b/src/test/cluster_unit/test_cluster_r4_d10_multi_source.c @@ -0,0 +1,647 @@ +/*------------------------------------------------------------------------- + * + * test_cluster_r4_d10_multi_source.c + * Shared-admission tests for the dormant MultiXact source dispatcher. + * + * 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_r4_d10_multi_source.c + * + * NOTES + * This is a pgrac-original test file. It links the production + * cluster_multixact object and replaces only its process/external + * dependencies with deterministic in-process fixtures. + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include +#include +#include + +#include "cluster/cluster_cr.h" +#include "cluster/cluster_cr_server.h" +#include "cluster/cluster_epoch.h" +#include "cluster/cluster_multixact.h" +#include "cluster/cluster_semantic_activation.h" +#include "cluster/cluster_shmem.h" +#include "cluster/cluster_subtrans.h" +#include "cluster/cluster_visibility_resolve.h" +#include "miscadmin.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" +#include "utils/elog.h" +#include "utils/hsearch.h" + +#undef printf +#undef fprintf +#undef snprintf + +#include "unit_test.h" + +UT_DEFINE_GLOBALS(); + +ProcessingMode Mode = NormalProcessing; +sigjmp_buf *PG_exception_stack = NULL; +ErrorContextCallback *error_context_stack = NULL; + +bool cluster_enabled = true; +int cluster_node_id = 0; +int cluster_multixact_member_overlay_max_entries = 16; +int cluster_multixact_member_overlay_max_members = 32; +bool cluster_crossnode_runtime_visibility = false; +bool cluster_multi_xmax_remote_resolve = false; +int cluster_subtrans_max_chain_depth = 8; + +static ClusterSemanticAdmissionResult admission_result; +static bool admission_recheck_ok; +static int admission_enter_count; +static int admission_recheck_count; +static int admission_leave_count; +static uint64 admission_feature; +static ClusterSemanticAdmissionSide admission_side; + +static int fake_hash_search_count; +static bool fake_force_error; +static bool fake_state_found; +static bool fake_lock_found; + +static union +{ + uint64 align; + unsigned char bytes[128]; +} fake_state; + +static LWLockPadded fake_lock; + +static union +{ + uint64 align; + unsigned char bytes[8192]; +} fake_hash_entry; + +static HTAB *const fake_hash = (HTAB *)(uintptr_t)1; + +static void +reset_admission(ClusterSemanticAdmissionResult result, bool recheck_ok) +{ + admission_result = result; + admission_recheck_ok = recheck_ok; + admission_enter_count = 0; + admission_recheck_count = 0; + admission_leave_count = 0; + admission_feature = 0; + admission_side = CLUSTER_SEMANTIC_TARGET_SIDE; +} + +void +ExceptionalCondition(const char *condition_name pg_attribute_unused(), + const char *file_name pg_attribute_unused(), + int line_number pg_attribute_unused()) +{ + abort(); +} + +ClusterSemanticAdmissionResult +cluster_semantic_activation_enter(uint64 feature_bit, ClusterSemanticAdmissionSide side, + ClusterSemanticAdmissionToken *token) +{ + admission_enter_count++; + admission_feature = feature_bit; + admission_side = side; + memset(token, 0, sizeof(*token)); + if (admission_result == CLUSTER_SEMANTIC_ADMISSION_OK) + { + token->feature_bit = feature_bit; + token->record_generation = 7; + token->formation_epoch = 11; + token->side = (uint8) side; + token->entered = true; + } + return admission_result; +} + +bool +cluster_semantic_activation_recheck(const ClusterSemanticAdmissionToken *token) +{ + admission_recheck_count++; + return token != NULL && token->entered && admission_recheck_ok; +} + +void +cluster_semantic_activation_leave(ClusterSemanticAdmissionToken *token) +{ + admission_leave_count++; + if (token != NULL) + token->entered = false; +} + +void * +ShmemInitStruct(const char *name, Size size, bool *found_ptr) +{ + if (strcmp(name, "ClusterMultiXactState") == 0) + { + UT_ASSERT(size <= sizeof(fake_state.bytes)); + *found_ptr = fake_state_found; + fake_state_found = true; + return fake_state.bytes; + } + UT_ASSERT(strcmp(name, "ClusterMultiXactLock") == 0); + UT_ASSERT(size <= sizeof(fake_lock)); + *found_ptr = fake_lock_found; + fake_lock_found = true; + return &fake_lock; +} + +HTAB * +ShmemInitHash(const char *name pg_attribute_unused(), long init_size pg_attribute_unused(), + long max_size pg_attribute_unused(), HASHCTL *info pg_attribute_unused(), + int hash_flags pg_attribute_unused()) +{ + return fake_hash; +} + +Size +hash_estimate_size(long num_entries, Size entry_size) +{ + return (Size) num_entries * entry_size; +} + +Size +add_size(Size first, Size second) +{ + return first + second; +} + +void * +hash_search(HTAB *hashp, const void *key_ptr, HASHACTION action, bool *found_ptr) +{ + fake_hash_search_count++; + UT_ASSERT(hashp == fake_hash); + + if (action == HASH_ENTER_NULL) + { + if (found_ptr != NULL) + *found_ptr = memcmp(fake_hash_entry.bytes, key_ptr, + sizeof(ClusterMultiXactKey)) == 0; + memcpy(fake_hash_entry.bytes, key_ptr, sizeof(ClusterMultiXactKey)); + return fake_hash_entry.bytes; + } + if (action == HASH_FIND) + { + if (memcmp(fake_hash_entry.bytes, key_ptr, sizeof(ClusterMultiXactKey)) == 0) + return fake_hash_entry.bytes; + return NULL; + } + if (action == HASH_REMOVE) + { + memset(fake_hash_entry.bytes, 0, sizeof(fake_hash_entry.bytes)); + return fake_hash_entry.bytes; + } + return NULL; +} + +void +hash_seq_init(HASH_SEQ_STATUS *status pg_attribute_unused(), HTAB *hashp pg_attribute_unused()) +{} + +void * +hash_seq_search(HASH_SEQ_STATUS *status pg_attribute_unused()) +{ + return NULL; +} + +bool +LWLockAcquire(LWLock *lock pg_attribute_unused(), LWLockMode mode pg_attribute_unused()) +{ + return true; +} + +void +LWLockRelease(LWLock *lock pg_attribute_unused()) +{} + +void +LWLockInitialize(LWLock *lock pg_attribute_unused(), int tranche_id pg_attribute_unused()) +{} + +TimestampTz +GetCurrentTimestamp(void) +{ + if (fake_force_error && PG_exception_stack != NULL) + siglongjmp(*PG_exception_stack, 1); + return (TimestampTz) 12345; +} + +uint64 +cluster_epoch_get_current(void) +{ + return 19; +} + +void +cluster_shmem_register_region(const ClusterShmemRegion *region pg_attribute_unused()) +{} + +bool +cluster_tt_status_lookup_exact(const ClusterTTStatusKey *key pg_attribute_unused(), + ClusterTTStatusResult *result pg_attribute_unused()) +{ + return false; +} + +ClusterTTStatusResult +cluster_subtrans_lookup_parent(const ClusterTTStatusResult *child, int depth pg_attribute_unused()) +{ + return *child; +} + +ClusterVisVerdict +cluster_vis_cr_xmax_verdict(ClusterTTStatus status pg_attribute_unused(), + ClusterVisibilityDecision decision) +{ + return decision == CLUSTER_VISIBILITY_INVISIBLE ? CVV_INVISIBLE : CVV_VISIBLE; +} + +bool +cluster_gcs_block_undo_multi_verdict_fetch_and_wait(int32 origin_node pg_attribute_unused(), + MultiXactId mxid pg_attribute_unused(), + char *page_out pg_attribute_unused(), + ClusterLiveAuthority *auth_out pg_attribute_unused()) +{ + return false; +} + +bool +cluster_vis_live_authority_covers(SCN demand_scn pg_attribute_unused(), + ClusterLiveAuthority auth pg_attribute_unused()) +{ + return false; +} + +void +cluster_vis53r97_note_covers_refuse(void) +{} + +void +cluster_vis53r97_note_multi_member_serve_ask(void) +{} + +void +cluster_vis53r97_note_multi_member_serve_hit(void) +{} + +void +cluster_vis_bump_covers_scn_refuse_count(void) +{} + +ClusterVisibilityDecision +cluster_multixact_resolve_visibility_served( + const ClusterMultiXactServedMember *members pg_attribute_unused(), + uint16 member_count pg_attribute_unused(), SCN read_scn pg_attribute_unused()) +{ + return CLUSTER_VISIBILITY_UNKNOWN; +} + +int +scn_time_cmp(SCN first, SCN second) +{ + return first < second ? -1 : first > second ? 1 : 0; +} + +void * +palloc0(Size size) +{ + return calloc(1, size); +} + +void +pfree(void *pointer) +{ + free(pointer); +} + +bool +errstart(int elevel pg_attribute_unused(), const char *domain pg_attribute_unused()) +{ + return false; +} + +int +errcode(int sqlerrcode) +{ + return sqlerrcode; +} + +int +errmsg(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +int +errhint(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +void +errfinish(const char *filename pg_attribute_unused(), int lineno pg_attribute_unused(), + const char *funcname pg_attribute_unused()) +{} + +void +pg_re_throw(void) +{ + if (PG_exception_stack != NULL) + siglongjmp(*PG_exception_stack, 1); + abort(); +} + +static ClusterMultiXactSourceRequest +make_install_request(ClusterMultiXactKey *key, ClusterMultiXactMember *member) +{ + ClusterMultiXactSourceRequest request; + + memset(&request, 0, sizeof(request)); + request.key = key; + request.member_count = 1; + request.members = member; + return request; +} + +UT_TEST(t1_frozen_dispatch_surface) +{ + UT_ASSERT_EQ((int) CLUSTER_MULTI_SOURCE_OVERLAY_INSTALL, 0); + UT_ASSERT_EQ((int) CLUSTER_MULTI_SOURCE_OVERLAY_LOOKUP, 1); + UT_ASSERT_EQ((int) CLUSTER_MULTI_SOURCE_RESOLVE_VISIBILITY, 2); + UT_ASSERT_EQ((int) CLUSTER_MULTI_SOURCE_GET_MEMBER_COUNT, 3); + UT_ASSERT_EQ((int) CLUSTER_MULTI_SOURCE_REMOTE_XMAX_RESOLVE, 4); + UT_ASSERT_EQ((int) CLUSTER_MULTI_SOURCE_NOTE_HALFSPACE_REFUSE, 5); + UT_ASSERT_EQ((int) CLUSTER_MULTI_SOURCE_NOTE_UNDERIVABLE_READ, 6); +} + +UT_TEST(t2_dormant_refuses_before_request_and_mutation) +{ + ClusterMultiXactSourceResult result; + uint64 before = cluster_multixact_get_overlay_install_count(); + int searches_before = fake_hash_search_count; + + memset(&result, 0x7f, sizeof(result)); + reset_admission(CLUSTER_SEMANTIC_ADMISSION_SOURCE_DORMANT, true); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + (ClusterMultiXactSourceOp) CLUSTER_MULTI_SOURCE_OVERLAY_INSTALL, + (const ClusterMultiXactSourceRequest *) (uintptr_t) 1, &result), + (int) CLUSTER_SEMANTIC_ADMISSION_SOURCE_DORMANT); + UT_ASSERT_EQ(admission_enter_count, 1); + UT_ASSERT_EQ(admission_recheck_count, 0); + UT_ASSERT_EQ(admission_leave_count, 0); + UT_ASSERT_EQ((uint64) admission_feature, + (uint64) CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1); + UT_ASSERT_EQ((int) admission_side, (int) CLUSTER_SEMANTIC_SOURCE_SIDE); + UT_ASSERT_EQ((int) result.bool_value, 0); + UT_ASSERT_EQ((int) result.member_count, 0); + UT_ASSERT_EQ((int) result.visibility, 0); + UT_ASSERT_EQ((int) result.overlay_hit, 0); + UT_ASSERT_EQ((uint64) cluster_multixact_get_overlay_install_count(), before); + UT_ASSERT_EQ(fake_hash_search_count, searches_before); +} + +UT_TEST(t3_invalid_after_admission_closes_and_leaves) +{ + ClusterMultiXactSourceResult result; + + memset(&result, 0x7f, sizeof(result)); + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, true); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_OVERLAY_INSTALL, NULL, &result), + (int) CLUSTER_SEMANTIC_ADMISSION_CLOSED); + UT_ASSERT_EQ(admission_enter_count, 1); + UT_ASSERT_EQ(admission_recheck_count, 0); + UT_ASSERT_EQ(admission_leave_count, 1); + UT_ASSERT(!result.bool_value); + + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, true); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + (ClusterMultiXactSourceOp) 99, NULL, &result), + (int) CLUSTER_SEMANTIC_ADMISSION_CLOSED); + UT_ASSERT_EQ(admission_leave_count, 1); +} + +UT_TEST(t4_source_install_and_lookup_are_positive) +{ + ClusterMultiXactKey key; + ClusterMultiXactMember member; + ClusterMultiXactSourceRequest request; + ClusterMultiXactSourceResult result; + union + { + uint64 align; + unsigned char bytes[offsetof(ClusterMultiXactMemberOverlayResult, members) + + sizeof(ClusterMultiXactMember)]; + } output; + ClusterMultiXactMemberOverlayResult *overlay + = (ClusterMultiXactMemberOverlayResult *) output.bytes; + + memset(&key, 0, sizeof(key)); + key.origin_node_id = 3; + key.multixact_id = 71; + key.cluster_epoch = 19; + memset(&member, 0, sizeof(member)); + member.xid = FirstNormalTransactionId; + member.status = MultiXactStatusForShare; + request = make_install_request(&key, &member); + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, true); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_OVERLAY_INSTALL, &request, &result), + (int) CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT(result.bool_value); + UT_ASSERT_EQ(admission_recheck_count, 1); + UT_ASSERT_EQ(admission_leave_count, 1); + + memset(&request, 0, sizeof(request)); + memset(overlay, 0, sizeof(output.bytes)); + request.key = &key; + request.overlay_out = overlay; + request.max_members_buf = 1; + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, true); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_OVERLAY_LOOKUP, &request, &result), + (int) CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT(result.bool_value); + UT_ASSERT_EQ((int) result.member_count, 1); + UT_ASSERT(overlay->authoritative); + UT_ASSERT_EQ((int) overlay->members[0].xid, (int) member.xid); +} + +UT_TEST(t5_source_visibility_count_and_remote_map_results) +{ + ClusterMultiXactSourceRequest request; + ClusterMultiXactSourceResult result; + SnapshotData snapshot; + union + { + uint64 align; + unsigned char bytes[offsetof(ClusterMultiXactMemberOverlayResult, members) + + sizeof(ClusterMultiXactMember)]; + } input; + ClusterMultiXactMemberOverlayResult *overlay + = (ClusterMultiXactMemberOverlayResult *) input.bytes; + ClusterMultiXactKey key; + + memset(&snapshot, 0, sizeof(snapshot)); + memset(overlay, 0, sizeof(input.bytes)); + overlay->authoritative = true; + overlay->member_count = 1; + overlay->members[0].xid = FirstNormalTransactionId; + overlay->members[0].status = MultiXactStatusForShare; + memset(&request, 0, sizeof(request)); + request.overlay_in = overlay; + request.snapshot = &snapshot; + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, true); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_RESOLVE_VISIBILITY, &request, &result), + (int) CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT_EQ((int) result.visibility, (int) CLUSTER_VISIBILITY_VISIBLE); + + memset(&key, 0, sizeof(key)); + key.origin_node_id = 3; + key.multixact_id = 71; + key.cluster_epoch = 19; + memset(&request, 0, sizeof(request)); + request.key = &key; + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, true); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_GET_MEMBER_COUNT, &request, &result), + (int) CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT_EQ((int) result.member_count, 1); + + memset(&request, 0, sizeof(request)); + request.snapshot = &snapshot; + request.origin_slot = 3; + request.mxid = 71; + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, true); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_REMOTE_XMAX_RESOLVE, &request, &result), + (int) CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT_EQ((int) result.visibility, (int) CLUSTER_VISIBILITY_VISIBLE); + UT_ASSERT(result.overlay_hit); +} + +UT_TEST(t6_source_counter_ops_execute_only_after_admission) +{ + ClusterMultiXactSourceResult result; + uint64 halfspace_before = cluster_multixact_get_mxid_halfspace_refuse_count(); + uint64 underivable_before = cluster_multixact_get_mxid_underivable_read_count(); + + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, true); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_NOTE_HALFSPACE_REFUSE, NULL, &result), + (int) CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT_EQ((uint64) cluster_multixact_get_mxid_halfspace_refuse_count(), + halfspace_before + 1); + UT_ASSERT_EQ(admission_leave_count, 1); + + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, true); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_NOTE_UNDERIVABLE_READ, NULL, &result), + (int) CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT_EQ((uint64) cluster_multixact_get_mxid_underivable_read_count(), + underivable_before + 1); +} + +UT_TEST(t7_generation_drift_keeps_fixed_result_canonical) +{ + ClusterMultiXactKey key; + ClusterMultiXactMember member; + ClusterMultiXactSourceRequest request; + ClusterMultiXactSourceResult result; + + memset(&key, 0, sizeof(key)); + key.origin_node_id = 4; + key.multixact_id = 72; + key.cluster_epoch = 19; + memset(&member, 0, sizeof(member)); + member.xid = FirstNormalTransactionId + 1; + request = make_install_request(&key, &member); + memset(&result, 0x7f, sizeof(result)); + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, false); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_OVERLAY_INSTALL, &request, &result), + (int) CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED); + UT_ASSERT(!result.bool_value); + UT_ASSERT_EQ((int) result.member_count, 0); + UT_ASSERT_EQ((int) result.visibility, 0); + UT_ASSERT(!result.overlay_hit); + UT_ASSERT_EQ(admission_recheck_count, 1); + UT_ASSERT_EQ(admission_leave_count, 1); +} + +UT_TEST(t8_error_path_leaves_once_before_rethrow) +{ + ClusterMultiXactKey key; + ClusterMultiXactMember member; + ClusterMultiXactSourceRequest request; + ClusterMultiXactSourceResult result; + volatile bool caught = false; + + memset(&key, 0, sizeof(key)); + key.cluster_epoch = 19; + memset(&member, 0, sizeof(member)); + member.xid = FirstNormalTransactionId; + request = make_install_request(&key, &member); + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, true); + fake_force_error = true; + PG_TRY(); + { + (void) cluster_multixact_source_dispatch(CLUSTER_MULTI_SOURCE_OVERLAY_INSTALL, + &request, &result); + } + PG_CATCH(); + { + caught = true; + } + PG_END_TRY(); + fake_force_error = false; + UT_ASSERT(caught); + UT_ASSERT_EQ(admission_leave_count, 1); +} + +UT_TEST(t9_null_result_is_closed_after_balanced_admission) +{ + reset_admission(CLUSTER_SEMANTIC_ADMISSION_OK, true); + UT_ASSERT_EQ((int) cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_NOTE_HALFSPACE_REFUSE, NULL, NULL), + (int) CLUSTER_SEMANTIC_ADMISSION_CLOSED); + UT_ASSERT_EQ(admission_enter_count, 1); + UT_ASSERT_EQ(admission_recheck_count, 0); + UT_ASSERT_EQ(admission_leave_count, 1); +} + +int +main(void) +{ + memset(&fake_state, 0, sizeof(fake_state)); + memset(&fake_lock, 0, sizeof(fake_lock)); + memset(&fake_hash_entry, 0, sizeof(fake_hash_entry)); + cluster_multixact_shmem_init(); + + UT_PLAN(9); + UT_RUN(t1_frozen_dispatch_surface); + UT_RUN(t2_dormant_refuses_before_request_and_mutation); + UT_RUN(t3_invalid_after_admission_closes_and_leaves); + UT_RUN(t4_source_install_and_lookup_are_positive); + UT_RUN(t5_source_visibility_count_and_remote_map_results); + UT_RUN(t6_source_counter_ops_execute_only_after_admission); + UT_RUN(t7_generation_drift_keeps_fixed_result_canonical); + UT_RUN(t8_error_path_leaves_once_before_rethrow); + UT_RUN(t9_null_result_is_closed_after_balanced_admission); + UT_DONE(); + return ut_failed_count == 0 ? 0 : 1; +} From ef9bec22e90f93bb204fd477773b91202216051b Mon Sep 17 00:00:00 2001 From: SqlRush Date: Mon, 10 Aug 2026 10:41:57 +0800 Subject: [PATCH 6/8] feat(cluster): complete D10 source dispatch integration --- src/backend/access/heap/heapam.c | 28 ++++- src/backend/access/heap/heapam_visibility.c | 48 +++++++-- src/backend/access/transam/multixact.c | 30 +++++- src/backend/cluster/cluster_cr.c | 12 ++- src/backend/cluster/cluster_lmon.c | 6 +- src/backend/cluster/cluster_multixact.c | 10 +- src/backend/cluster/cluster_subtrans.c | 80 ++++++++++++-- src/backend/cluster/cluster_tt_2pc.c | 79 ++++++++++++-- src/backend/cluster/cluster_tt_local.c | 40 ++++++- src/backend/cluster/cluster_tt_status_hint.c | 34 +++++- src/backend/cluster/cluster_tx_enqueue.c | 10 +- .../cluster/cluster_visibility_inject.c | 38 ++++++- .../cluster/cluster_visibility_resolve.c | 9 +- src/test/cluster_unit/Makefile | 22 ++++ src/test/cluster_unit/test_cluster_lmon.c | 13 ++- .../cluster_unit/test_cluster_multixact.c | 73 ++++--------- src/test/cluster_unit/test_cluster_qvotec.c | 1 + .../test_cluster_r4_d10_hint_source.c | 31 +++--- .../test_cluster_r4_d10_multi_source.c | 10 +- .../test_cluster_r4_static_model.c | 101 ++++++++++++------ .../cluster_unit/test_cluster_r4_tx_enqueue.c | 23 ++-- src/test/cluster_unit/test_cluster_subtrans.c | 41 +++---- .../cluster_unit/test_cluster_tt_status.c | 44 +++----- .../test_cluster_tt_status_hint.c | 26 ++--- .../test_cluster_visibility_inject.c | 36 +++---- 25 files changed, 577 insertions(+), 268 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index a7c542af222..91f04ca63ab 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -3218,9 +3218,21 @@ cluster_heap_writer_wait_failclosed(Relation relation, Buffer buffer, HeapTuple * holder (lock-only / writer). */ { - bool tt_found = cluster_tt_status_lookup_exact(&ckey, &cres); - bool tt_resolved = tt_found && cres.authoritative; - bool tt_terminal = tt_resolved + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; + bool tt_found; + bool tt_resolved; + bool tt_terminal; + + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &ckey; + tt_found = cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, &source_request, + &source_result) + == CLUSTER_SEMANTIC_ADMISSION_OK + && source_result.bool_value; + cres = source_result.lookup; + tt_resolved = tt_found && cres.authoritative; + tt_terminal = tt_resolved && (cres.status == CLUSTER_TT_STATUS_COMMITTED || cres.status == CLUSTER_TT_STATUS_ABORTED || cres.status == CLUSTER_TT_STATUS_CLEANED_OUT); @@ -6677,6 +6689,8 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, { ClusterTTStatusKey ckey; ClusterTTStatusResult cres; + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; bool tt_found; bool tt_resolved; bool tt_terminal; @@ -6714,7 +6728,13 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, * the HOLDER node); never the generic resid encoder * which would key on the local node (G1). */ - tt_found = cluster_tt_status_lookup_exact(&ckey, &cres); + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &ckey; + tt_found = cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, + &source_request, &source_result) + == CLUSTER_SEMANTIC_ADMISSION_OK + && source_result.bool_value; + cres = source_result.lookup; tt_resolved = tt_found && cres.authoritative; tt_terminal = tt_resolved && (cres.status == CLUSTER_TT_STATUS_COMMITTED diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c index 4520189f19c..47ff8c9faeb 100644 --- a/src/backend/access/heap/heapam_visibility.c +++ b/src/backend/access/heap/heapam_visibility.c @@ -1633,17 +1633,31 @@ cluster_remote_live_xmax_keeps_visible(Buffer buffer, HeapTupleHeader tuple, Sna mx_origin = cluster_mxid_origin_slot((MultiXactId)HeapTupleHeaderGetRawXmax(tuple)); if (mx_origin < 0) { + ClusterMultiXactSourceResult source_result; + /* PGRAC: spec-7.1 D0 census — foreign-multi refuse leg. */ - cluster_multixact_note_underivable_read(); + (void)cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_NOTE_UNDERIVABLE_READ, NULL, &source_result); cluster_vis53r97_note_multi_unresolvable(); return -1; } if (mx_origin != cluster_node_id) { - bool mx_hit = false; + ClusterMultiXactSourceRequest source_request; + ClusterMultiXactSourceResult source_result; + ClusterSemanticAdmissionResult source_admission; + ClusterVisibilityDecision mx_decision = CLUSTER_VISIBILITY_UNKNOWN; + + memset(&source_request, 0, sizeof(source_request)); + source_request.origin_slot = (uint16)mx_origin; + source_request.mxid = (MultiXactId)HeapTupleHeaderGetRawXmax(tuple); + source_request.snapshot = snapshot; + source_admission = cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_REMOTE_XMAX_RESOLVE, &source_request, &source_result); + if (source_admission == CLUSTER_SEMANTIC_ADMISSION_OK) { + mx_decision = source_result.visibility; + } - switch (cluster_multixact_remote_xmax_resolve( - (uint16)mx_origin, (MultiXactId)HeapTupleHeaderGetRawXmax(tuple), snapshot, - &mx_hit)) { + switch (mx_decision) { case CLUSTER_VISIBILITY_VISIBLE: return 1; /* no committed updater hides the row */ case CLUSTER_VISIBILITY_INVISIBLE: @@ -2158,8 +2172,11 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, Buffer buffer) mx_origin = cluster_mxid_origin_slot((MultiXactId)raw_xmax_multi); if (mx_origin < 0) { + ClusterMultiXactSourceResult source_result; + /* D3-0 floor: origin not provable -> fail closed */ - cluster_multixact_note_underivable_read(); + (void)cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_NOTE_UNDERIVABLE_READ, NULL, &source_result); ereport(ERROR, (errcode(ERRCODE_CLUSTER_MULTIXACT_MEMBER_OVERLAY_MISS), errmsg("cluster multixact %u cannot be attributed to an origin node", @@ -2170,10 +2187,25 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, Buffer buffer) "cluster.multi_xmax_remote_resolve. Retry the transaction."))); } if (mx_origin != cluster_node_id) { + ClusterMultiXactSourceRequest source_request; + ClusterMultiXactSourceResult source_result; + ClusterSemanticAdmissionResult source_admission; + ClusterVisibilityDecision mx_decision = CLUSTER_VISIBILITY_UNKNOWN; bool mx_hit = false; - switch (cluster_multixact_remote_xmax_resolve( - (uint16)mx_origin, (MultiXactId)raw_xmax_multi, snapshot, &mx_hit)) { + memset(&source_request, 0, sizeof(source_request)); + source_request.origin_slot = (uint16)mx_origin; + source_request.mxid = (MultiXactId)raw_xmax_multi; + source_request.snapshot = snapshot; + source_admission = cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_REMOTE_XMAX_RESOLVE, &source_request, + &source_result); + if (source_admission == CLUSTER_SEMANTIC_ADMISSION_OK) { + mx_decision = source_result.visibility; + mx_hit = source_result.overlay_hit; + } + + switch (mx_decision) { case CLUSTER_VISIBILITY_VISIBLE: return true; case CLUSTER_VISIBILITY_INVISIBLE: diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c index 5fb26cdb582..e407caee79c 100644 --- a/src/backend/access/transam/multixact.c +++ b/src/backend/access/transam/multixact.c @@ -896,6 +896,9 @@ MultiXactIdCreateFromMembers(int nmembers, MultiXactMember *members) && nmembers <= CLUSTER_MULTIXACT_HINT_MAX_MEMBERS) { ClusterMultiXactMember c_members[CLUSTER_MULTIXACT_HINT_MAX_MEMBERS]; ClusterMultiXactKey c_key; + ClusterMultiXactSourceRequest multi_request; + ClusterMultiXactSourceResult multi_result; + ClusterTTStatusHintSourceRequest hint_request; bool all_local = true; int i; @@ -926,8 +929,21 @@ MultiXactIdCreateFromMembers(int nmembers, MultiXactMember *members) c_key.multixact_id = multi; c_key.cluster_epoch = (uint32)cluster_epoch_get_current(); - if (cluster_multixact_member_overlay_install(&c_key, (uint16)nmembers, c_members)) - cluster_tt_status_hint_emit_multixact_overlay(&c_key, (uint16)nmembers, c_members); + memset(&multi_request, 0, sizeof(multi_request)); + multi_request.key = &c_key; + multi_request.member_count = (uint16)nmembers; + multi_request.members = c_members; + if (cluster_multixact_source_dispatch(CLUSTER_MULTI_SOURCE_OVERLAY_INSTALL, + &multi_request, &multi_result) + == CLUSTER_SEMANTIC_ADMISSION_OK + && multi_result.bool_value) { + memset(&hint_request, 0, sizeof(hint_request)); + hint_request.multi_key = &c_key; + hint_request.member_count = (uint16)nmembers; + hint_request.members = c_members; + (void)cluster_tt_status_hint_source_dispatch( + CLUSTER_TT_HINT_SOURCE_EMIT_MULTIXACT_OVERLAY, &hint_request); + } } } #endif @@ -1200,7 +1216,10 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset) */ if (cluster_mxid_halfspace_exceeded(result, floor_mxid) || cluster_cr_injection_armed("cluster-mxid-halfspace-hard-limit", NULL)) { - cluster_multixact_note_halfspace_refuse(); + ClusterMultiXactSourceResult source_result; + + (void)cluster_multixact_source_dispatch(CLUSTER_MULTI_SOURCE_NOTE_HALFSPACE_REFUSE, + NULL, &source_result); ereport( ERROR, (errcode(ERRCODE_CLUSTER_MXID_HALFSPACE_LIMIT), @@ -1329,7 +1348,10 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset) result = floor_mxid; result = cluster_mxid_next_striped(result, mxid_stripe_slot); if (cluster_mxid_halfspace_exceeded(result, floor_mxid)) { - cluster_multixact_note_halfspace_refuse(); + ClusterMultiXactSourceResult source_result; + + (void)cluster_multixact_source_dispatch( + CLUSTER_MULTI_SOURCE_NOTE_HALFSPACE_REFUSE, NULL, &source_result); ereport( ERROR, (errcode(ERRCODE_CLUSTER_MXID_HALFSPACE_LIMIT), diff --git a/src/backend/cluster/cluster_cr.c b/src/backend/cluster/cluster_cr.c index 949c6ed1767..21a75d9bb40 100644 --- a/src/backend/cluster/cluster_cr.c +++ b/src/backend/cluster/cluster_cr.c @@ -2424,6 +2424,9 @@ cluster_cr_resolve_xmax_commit_scn(const char *cr_page, uint8 itl_idx, Transacti if (cluster_itl_get_tt_ref(page, itl_idx, &ref)) { ClusterTTStatusKey key; ClusterTTStatusResult result; + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; + bool found; memset(&key, 0, sizeof(key)); key.origin_node_id = ref.origin_node_id; @@ -2432,7 +2435,14 @@ cluster_cr_resolve_xmax_commit_scn(const char *cr_page, uint8 itl_idx, Transacti key.cluster_epoch = ref.cluster_epoch; key.local_xid = cr_xmax; - if (cluster_tt_status_lookup_exact(&key, &result) && result.authoritative + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &key; + found = cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, &source_request, + &source_result) + == CLUSTER_SEMANTIC_ADMISSION_OK + && source_result.bool_value; + result = source_result.lookup; + if (found && result.authoritative && (result.status == CLUSTER_TT_STATUS_COMMITTED || result.status == CLUSTER_TT_STATUS_CLEANED_OUT) && SCN_VALID(result.commit_scn)) { diff --git a/src/backend/cluster/cluster_lmon.c b/src/backend/cluster/cluster_lmon.c index 6b415a558e4..8d086f6035d 100644 --- a/src/backend/cluster/cluster_lmon.c +++ b/src/backend/cluster/cluster_lmon.c @@ -1309,7 +1309,8 @@ LmonMain(void) /* spec-3.2 D6: LMON drain cross-node TT status hint outbound. * Fire-and-forget; L172 family — only LMON owns tier1 fds. */ if (cluster_lmon_duty_should_run(CLUSTER_LMON_DUTY_TT_HINT, force_all_duties)) - cluster_tt_status_hint_drain_outbound(); + (void)cluster_tt_status_hint_source_dispatch( + CLUSTER_TT_HINT_SOURCE_DRAIN_OUTBOUND, NULL); if (cluster_lmon_duty_should_run(CLUSTER_LMON_DUTY_BACKUP, force_all_duties)) cluster_backup_lmon_tick(); /* spec-5.22e D5-2: publish this node's undo retention horizon @@ -1975,7 +1976,8 @@ LmonMain(void) /* spec-3.2 D6: LMON drain cross-node TT status hint outbound. * Fire-and-forget; L172 family — only LMON owns tier1 fds. */ if (cluster_lmon_duty_should_run(CLUSTER_LMON_DUTY_TT_HINT, force_all_duties)) - cluster_tt_status_hint_drain_outbound(); + (void)cluster_tt_status_hint_source_dispatch( + CLUSTER_TT_HINT_SOURCE_DRAIN_OUTBOUND, NULL); if (cluster_lmon_duty_should_run(CLUSTER_LMON_DUTY_BACKUP, force_all_duties)) cluster_backup_lmon_tick(); /* spec-5.22e D5-2: publish this node's undo retention horizon diff --git a/src/backend/cluster/cluster_multixact.c b/src/backend/cluster/cluster_multixact.c index dc53e18bd17..06d1d5c58fb 100644 --- a/src/backend/cluster/cluster_multixact.c +++ b/src/backend/cluster/cluster_multixact.c @@ -291,6 +291,8 @@ cluster_multixact_resolve_visibility_raw(const ClusterMultiXactMemberOverlayResu { ClusterTTStatusKey ttkey; ClusterTTStatusResult ttres; + ClusterTTStatusSourceRequest tt_request; + ClusterTTStatusSourceResult tt_source_result; memset(&ttkey, 0, sizeof(ttkey)); ttkey.origin_node_id = m->origin_node_id; @@ -299,8 +301,14 @@ cluster_multixact_resolve_visibility_raw(const ClusterMultiXactMemberOverlayResu ttkey.cluster_epoch = m->epoch; ttkey.local_xid = m->xid; - if (!cluster_tt_status_lookup_exact(&ttkey, &ttres) || !ttres.authoritative) + memset(&tt_request, 0, sizeof(tt_request)); + tt_request.key = &ttkey; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, &tt_request, + &tt_source_result) + != CLUSTER_SEMANTIC_ADMISSION_OK + || !tt_source_result.bool_value || !tt_source_result.lookup.authoritative) return CLUSTER_VISIBILITY_UNKNOWN; + ttres = tt_source_result.lookup; if (ttres.status == CLUSTER_TT_STATUS_SUBCOMMITTED && ttres.has_parent_key) ttres = cluster_subtrans_lookup_parent(&ttres, cluster_subtrans_max_chain_depth); diff --git a/src/backend/cluster/cluster_subtrans.c b/src/backend/cluster/cluster_subtrans.c index a91708ee86e..504855bec34 100644 --- a/src/backend/cluster/cluster_subtrans.c +++ b/src/backend/cluster/cluster_subtrans.c @@ -176,6 +176,9 @@ bool cluster_subtrans_ensure_parent_binding(TransactionId parent_xid, ClusterTTStatusKey *parent_key_out) { ClusterTTStatusKey key; + ClusterTTStatusSourceRequest tt_request; + ClusterTTStatusSourceResult tt_result; + ClusterTTStatusHintSourceRequest hint_request; if (parent_key_out == NULL) return false; @@ -195,8 +198,19 @@ cluster_subtrans_ensure_parent_binding(TransactionId parent_xid, ClusterTTStatus * rather than missing through to 53R97 prematurely. Idempotent: * install_local overwrites existing entry without bumping eviction. */ - cluster_tt_status_install_local(&key, CLUSTER_TT_STATUS_IN_PROGRESS, InvalidScn); - cluster_tt_status_hint_emit(&key, CLUSTER_TT_STATUS_IN_PROGRESS, InvalidScn); + memset(&tt_request, 0, sizeof(tt_request)); + tt_request.key = &key; + tt_request.status = CLUSTER_TT_STATUS_IN_PROGRESS; + tt_request.commit_scn = InvalidScn; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_LOCAL, &tt_request, &tt_result) + != CLUSTER_SEMANTIC_ADMISSION_OK) + return false; + + memset(&hint_request, 0, sizeof(hint_request)); + hint_request.key = &key; + hint_request.status = CLUSTER_TT_STATUS_IN_PROGRESS; + hint_request.commit_scn = InvalidScn; + (void)cluster_tt_status_hint_source_dispatch(CLUSTER_TT_HINT_SOURCE_EMIT, &hint_request); *parent_key_out = key; return true; @@ -207,6 +221,9 @@ cluster_subtrans_emit_subcommit(TransactionId child_xid, TransactionId parent_xi { ClusterTTStatusKey child_key; ClusterTTStatusKey parent_key; + ClusterTTStatusSourceRequest tt_request; + ClusterTTStatusSourceResult tt_result; + ClusterTTStatusHintSourceRequest hint_request; if (!cluster_peer_mode_enabled()) return false; @@ -219,10 +236,20 @@ cluster_subtrans_emit_subcommit(TransactionId child_xid, TransactionId parent_xi return false; /* Install local SUBCOMMITTED + emit V3 hint to peers. */ - if (!cluster_tt_status_install_subcommitted(&child_key, &parent_key)) + memset(&tt_request, 0, sizeof(tt_request)); + tt_request.key = &child_key; + tt_request.parent_key = &parent_key; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_SUBCOMMITTED, &tt_request, + &tt_result) + != CLUSTER_SEMANTIC_ADMISSION_OK + || !tt_result.bool_value) return false; - cluster_tt_status_hint_emit_subcommitted(&child_key, &parent_key); + memset(&hint_request, 0, sizeof(hint_request)); + hint_request.key = &child_key; + hint_request.parent_key = &parent_key; + (void)cluster_tt_status_hint_source_dispatch(CLUSTER_TT_HINT_SOURCE_EMIT_SUBCOMMITTED, + &hint_request); /* spec-3.15 D7: track the link for a potential PREPARE. */ { @@ -244,6 +271,9 @@ bool cluster_subtrans_emit_subabort(TransactionId child_xid) { ClusterTTStatusKey child_key; + ClusterTTStatusSourceRequest tt_request; + ClusterTTStatusSourceResult tt_result; + ClusterTTStatusHintSourceRequest hint_request; if (!cluster_peer_mode_enabled()) return false; @@ -257,8 +287,19 @@ cluster_subtrans_emit_subabort(TransactionId child_xid) * ABORTED uses the existing V2 emit path (commit_scn=InvalidScn). * install_local covers the local overlay entry. */ - cluster_tt_status_install_local(&child_key, CLUSTER_TT_STATUS_ABORTED, InvalidScn); - cluster_tt_status_hint_emit(&child_key, CLUSTER_TT_STATUS_ABORTED, InvalidScn); + memset(&tt_request, 0, sizeof(tt_request)); + tt_request.key = &child_key; + tt_request.status = CLUSTER_TT_STATUS_ABORTED; + tt_request.commit_scn = InvalidScn; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_LOCAL, &tt_request, &tt_result) + != CLUSTER_SEMANTIC_ADMISSION_OK) + return false; + + memset(&hint_request, 0, sizeof(hint_request)); + hint_request.key = &child_key; + hint_request.status = CLUSTER_TT_STATUS_ABORTED; + hint_request.commit_scn = InvalidScn; + (void)cluster_tt_status_hint_source_dispatch(CLUSTER_TT_HINT_SOURCE_EMIT, &hint_request); return true; } @@ -293,8 +334,17 @@ cluster_subtrans_lookup_parent(const ClusterTTStatusResult *child_result, int de while (budget-- > 0) { ClusterTTStatusResult parent_res; - - if (!cluster_tt_status_lookup_exact(&next_key, &parent_res)) { + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; + ClusterTTStatusSourceRequest bump_request; + ClusterTTStatusSourceResult bump_result; + + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &next_key; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, &source_request, + &source_result) + != CLUSTER_SEMANTIC_ADMISSION_OK + || !source_result.bool_value) { /* * parent overlay miss → caller must fail-closed (53R97 per * L199). Return UNKNOWN authoritative=false. @@ -304,8 +354,11 @@ cluster_subtrans_lookup_parent(const ClusterTTStatusResult *child_result, int de cur.authoritative = false; return cur; } + parent_res = source_result.lookup; - cluster_tt_status_bump_parent_chain_follow(); + memset(&bump_request, 0, sizeof(bump_request)); + (void)cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_BUMP_PARENT_CHAIN_FOLLOW, + &bump_request, &bump_result); if (parent_res.status != CLUSTER_TT_STATUS_SUBCOMMITTED) return parent_res; @@ -334,6 +387,8 @@ cluster_subtrans_xact_has_state(TransactionId top_xid) { ClusterTTStatusKey key; ClusterTTStatusResult res; + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; if (!cluster_peer_mode_enabled()) return false; @@ -365,8 +420,13 @@ cluster_subtrans_xact_has_state(TransactionId top_xid) key.local_xid = top_xid; } - if (!cluster_tt_status_lookup_exact(&key, &res)) + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &key; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, &source_request, &source_result) + != CLUSTER_SEMANTIC_ADMISSION_OK + || !source_result.bool_value) return false; + res = source_result.lookup; /* * Any installed overlay state for this xid counts as "has state": diff --git a/src/backend/cluster/cluster_tt_2pc.c b/src/backend/cluster/cluster_tt_2pc.c index bfcc3d14f37..2c2957b8723 100644 --- a/src/backend/cluster/cluster_tt_2pc.c +++ b/src/backend/cluster/cluster_tt_2pc.c @@ -216,8 +216,16 @@ cluster_tt_twophase_recover(TransactionId xid, uint16 info, void *recdata, uint3 for (j = 0; j < p.nsublinks; j++) { const ClusterTT2PCSubLink *l = &p.sublinks[j]; - - if (!cluster_tt_status_install_subcommitted(&l->child_key, &l->parent_key)) + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; + + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &l->child_key; + source_request.parent_key = &l->parent_key; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_SUBCOMMITTED, + &source_request, &source_result) + != CLUSTER_SEMANTIC_ADMISSION_OK + || !source_result.bool_value) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("cannot rebuild SUBCOMMITTED overlay for prepared " "transaction %u (subxid %u)", @@ -293,6 +301,8 @@ cluster_tt_twophase_standby_recover(TransactionId xid, uint16 info, void *recdat for (i = 0; i < p.nbindings; i++) { const ClusterTT2PCBinding *b = &p.bindings[i]; ClusterTTStatusKey key; + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; uint16 origin_node_id; if (!cluster_tt_2pc_binding_origin_node(b, &origin_node_id)) { @@ -309,7 +319,14 @@ cluster_tt_twophase_standby_recover(TransactionId xid, uint16 info, void *recdat key.cluster_epoch = b->cluster_epoch; key.local_xid = b->xid; - if (!cluster_tt_status_install_local(&key, CLUSTER_TT_STATUS_IN_PROGRESS, InvalidScn)) { + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &key; + source_request.status = CLUSTER_TT_STATUS_IN_PROGRESS; + source_request.commit_scn = InvalidScn; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_LOCAL, &source_request, + &source_result) + != CLUSTER_SEMANTIC_ADMISSION_OK + || !source_result.bool_value) { /* capacity / shmem unavailable: degrade, do NOT PANIC the * standby; affected reads fail-closed 53R97 + we count it. */ cluster_vis_bump_recovery_overlay_rebuild_count(); @@ -323,8 +340,16 @@ cluster_tt_twophase_standby_recover(TransactionId xid, uint16 info, void *recdat for (j = 0; j < p.nsublinks; j++) { const ClusterTT2PCSubLink *l = &p.sublinks[j]; - - if (!cluster_tt_status_install_subcommitted(&l->child_key, &l->parent_key)) { + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; + + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &l->child_key; + source_request.parent_key = &l->parent_key; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_SUBCOMMITTED, + &source_request, &source_result) + != CLUSTER_SEMANTIC_ADMISSION_OK + || !source_result.bool_value) { /* capacity / shmem unavailable: degrade, do NOT PANIC the * standby; affected reads fail-closed 53R97 + we count it. */ cluster_vis_bump_recovery_overlay_rebuild_count(); @@ -342,9 +367,20 @@ cluster_tt_twophase_standby_recover(TransactionId xid, uint16 info, void *recdat int cluster_tt_twophase_standby_commit_prepared(TransactionId xid, SCN commit_scn) { + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; + if (!cluster_enabled || cluster_node_id < 0 || !SCN_VALID(commit_scn)) return 0; - return cluster_tt_status_resolve_prepared_commit(xid, commit_scn); + + memset(&source_request, 0, sizeof(source_request)); + source_request.xid = xid; + source_request.commit_scn = commit_scn; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_RESOLVE_PREPARED_COMMIT, + &source_request, &source_result) + != CLUSTER_SEMANTIC_ADMISSION_OK) + return 0; + return source_result.int_value; } @@ -389,6 +425,9 @@ cluster_tt_twophase_prefinish(TransactionId xid, SCN final_scn, bool is_commit, for (i = 0; i < p.nbindings; i++) { const ClusterTT2PCBinding *b = &p.bindings[i]; ClusterTTStatusKey key; + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; + ClusterTTStatusHintSourceRequest hint_request; memset(&key, 0, sizeof(key)); key.origin_node_id = (uint16)cluster_node_id; @@ -401,8 +440,18 @@ cluster_tt_twophase_prefinish(TransactionId xid, SCN final_scn, bool is_commit, cluster_tt_slot_durable_commit(b->undo_segment_id, b->slot_offset, b->xid, b->wrap, final_scn); cluster_tt_slot_mark_committed(b->undo_segment_id, b->slot_offset, b->xid, final_scn); - (void)cluster_tt_status_install_local(&key, CLUSTER_TT_STATUS_COMMITTED, final_scn); - cluster_tt_status_hint_emit(&key, CLUSTER_TT_STATUS_COMMITTED, final_scn); + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &key; + source_request.status = CLUSTER_TT_STATUS_COMMITTED; + source_request.commit_scn = final_scn; + (void)cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_LOCAL, &source_request, + &source_result); + memset(&hint_request, 0, sizeof(hint_request)); + hint_request.key = &key; + hint_request.status = CLUSTER_TT_STATUS_COMMITTED; + hint_request.commit_scn = final_scn; + (void)cluster_tt_status_hint_source_dispatch(CLUSTER_TT_HINT_SOURCE_EMIT, + &hint_request); } else { cluster_tt_slot_durable_abort(b->undo_segment_id, b->slot_offset, b->xid, b->wrap); /* @@ -418,8 +467,18 @@ cluster_tt_twophase_prefinish(TransactionId xid, SCN final_scn, bool is_commit, cluster_tt_slot_durable_set_head(b->undo_segment_id, b->slot_offset, b->xid, b->wrap, p.heads[i]); cluster_tt_slot_mark_aborted(b->undo_segment_id, b->slot_offset, b->xid); - (void)cluster_tt_status_install_local(&key, CLUSTER_TT_STATUS_ABORTED, InvalidScn); - cluster_tt_status_hint_emit(&key, CLUSTER_TT_STATUS_ABORTED, InvalidScn); + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &key; + source_request.status = CLUSTER_TT_STATUS_ABORTED; + source_request.commit_scn = InvalidScn; + (void)cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_LOCAL, &source_request, + &source_result); + memset(&hint_request, 0, sizeof(hint_request)); + hint_request.key = &key; + hint_request.status = CLUSTER_TT_STATUS_ABORTED; + hint_request.commit_scn = InvalidScn; + (void)cluster_tt_status_hint_source_dispatch(CLUSTER_TT_HINT_SOURCE_EMIT, + &hint_request); } } } diff --git a/src/backend/cluster/cluster_tt_local.c b/src/backend/cluster/cluster_tt_local.c index b44282ecb81..d5bcd647b54 100644 --- a/src/backend/cluster/cluster_tt_local.c +++ b/src/backend/cluster/cluster_tt_local.c @@ -508,7 +508,19 @@ build_local_key(TransactionId xid, ClusterTTStatusKey *out) static void install_key(const ClusterTTStatusKey *key, ClusterTTStatus status, SCN commit_scn) { - bool installed = cluster_tt_status_install_local(key, status, commit_scn); + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; + ClusterTTStatusHintSourceRequest hint_request; + bool installed; + + memset(&source_request, 0, sizeof(source_request)); + source_request.key = key; + source_request.status = status; + source_request.commit_scn = commit_scn; + installed = cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_LOCAL, &source_request, + &source_result) + == CLUSTER_SEMANTIC_ADMISSION_OK + && source_result.bool_value; if (!installed) return; @@ -516,21 +528,39 @@ install_key(const ClusterTTStatusKey *key, ClusterTTStatus status, SCN commit_sc #ifdef USE_ASSERT_CHECKING { ClusterTTStatusResult res; + ClusterTTStatusSourceRequest lookup_request; + ClusterTTStatusSourceResult lookup_result; bool hit = false; bool epoch_stable = ((uint32)cluster_epoch_get_current() == key->cluster_epoch); if (epoch_stable) { - hit = cluster_tt_status_lookup_exact(key, &res); + memset(&lookup_request, 0, sizeof(lookup_request)); + lookup_request.key = key; + hit = cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, &lookup_request, + &lookup_result) + == CLUSTER_SEMANTIC_ADMISSION_OK + && lookup_result.bool_value; + res = lookup_result.lookup; epoch_stable = ((uint32)cluster_epoch_get_current() == key->cluster_epoch); } if (epoch_stable) Assert(hit && res.authoritative && res.status == status); - if (hit && res.authoritative && res.status == status) - cluster_tt_status_bump_self_consumer_hit(); + if (hit && res.authoritative && res.status == status) { + ClusterTTStatusSourceRequest bump_request; + ClusterTTStatusSourceResult bump_result; + + memset(&bump_request, 0, sizeof(bump_request)); + (void)cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_BUMP_SELF_CONSUMER_HIT, + &bump_request, &bump_result); + } } #endif - cluster_tt_status_hint_emit(key, status, commit_scn); + memset(&hint_request, 0, sizeof(hint_request)); + hint_request.key = key; + hint_request.status = status; + hint_request.commit_scn = commit_scn; + (void)cluster_tt_status_hint_source_dispatch(CLUSTER_TT_HINT_SOURCE_EMIT, &hint_request); } static void diff --git a/src/backend/cluster/cluster_tt_status_hint.c b/src/backend/cluster/cluster_tt_status_hint.c index d7fe208caab..0dfa500096e 100644 --- a/src/backend/cluster/cluster_tt_status_hint.c +++ b/src/backend/cluster/cluster_tt_status_hint.c @@ -570,6 +570,9 @@ cluster_tt_status_hint_handle_envelope_raw(const ClusterICEnvelope *env, const v /* PGRAC spec-3.5: V3 SUBCOMMITTED carries parent_key. */ bool is_v3_subcommitted = false; ClusterTTStatusKey parent_key_local; + ClusterTTStatusSourceRequest tt_request; + ClusterTTStatusSourceResult tt_result; + ClusterSemanticAdmissionResult tt_admission; memset(&parent_key_local, 0, sizeof(parent_key_local)); @@ -656,6 +659,8 @@ cluster_tt_status_hint_handle_envelope_raw(const ClusterICEnvelope *env, const v */ const ClusterTTStatusHintMsgV4Header *v4hdr; const ClusterMultiXactMember *v4_members; + ClusterMultiXactSourceRequest multi_request; + ClusterMultiXactSourceResult multi_result; uint16 v4_member_count; Size expected_len; @@ -705,7 +710,14 @@ cluster_tt_status_hint_handle_envelope_raw(const ClusterICEnvelope *env, const v } } - (void)cluster_multixact_member_overlay_install(&v4hdr->key, v4_member_count, v4_members); + memset(&multi_request, 0, sizeof(multi_request)); + multi_request.key = &v4hdr->key; + multi_request.member_count = v4_member_count; + multi_request.members = v4_members; + if (cluster_multixact_source_dispatch(CLUSTER_MULTI_SOURCE_OVERLAY_INSTALL, &multi_request, + &multi_result) + != CLUSTER_SEMANTIC_ADMISSION_OK) + return; pg_atomic_fetch_add_u64(&ClusterTTHintCounters->receive_count, 1); pg_atomic_fetch_add_u64(&ClusterTTHintCounters->install_count, 1); @@ -819,10 +831,22 @@ cluster_tt_status_hint_handle_envelope_raw(const ClusterICEnvelope *env, const v * PGRAC spec-3.5: V3 SUBCOMMITTED dispatches to install_subcommitted * which records parent_key for lazy reader follow. */ - if (is_v3_subcommitted) - cluster_tt_status_install_subcommitted(key, &parent_key_local); - else { - cluster_tt_status_install_local(key, (ClusterTTStatus)status_raw, commit_scn); + memset(&tt_request, 0, sizeof(tt_request)); + tt_request.key = key; + if (is_v3_subcommitted) { + tt_request.parent_key = &parent_key_local; + tt_admission = cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_SUBCOMMITTED, + &tt_request, &tt_result); + } else { + tt_request.status = (ClusterTTStatus)status_raw; + tt_request.commit_scn = commit_scn; + tt_admission = cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_LOCAL, &tt_request, + &tt_result); + } + if (tt_admission != CLUSTER_SEMANTIC_ADMISSION_OK) + return; + + if (!is_v3_subcommitted) { /* * spec-5.2 D6: a remote holder just became terminal on this node's diff --git a/src/backend/cluster/cluster_tx_enqueue.c b/src/backend/cluster/cluster_tx_enqueue.c index 2390d107390..8da97785d84 100644 --- a/src/backend/cluster/cluster_tx_enqueue.c +++ b/src/backend/cluster/cluster_tx_enqueue.c @@ -486,6 +486,8 @@ cluster_tx_enqueue_wait(const ClusterTTStatusKey *holder_key, int effective_time else for (;;) { ClusterTTStatusResult cres; + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; bool found; TimestampTz now; long wait_ms; @@ -494,7 +496,13 @@ cluster_tx_enqueue_wait(const ClusterTTStatusKey *holder_key, int effective_time /* Re-check the holder's TT status (closes the register/wake race: * a terminal status published before we slept is seen here). */ - found = cluster_tt_status_lookup_exact(holder_key, &cres); + memset(&source_request, 0, sizeof(source_request)); + source_request.key = holder_key; + found = cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, + &source_request, &source_result) + == CLUSTER_SEMANTIC_ADMISSION_OK + && source_result.bool_value; + cres = source_result.lookup; if (found && cres.authoritative && txw_status_is_terminal(cres.status)) { result = CLUSTER_TXW_RESOLVED; break; diff --git a/src/backend/cluster/cluster_visibility_inject.c b/src/backend/cluster/cluster_visibility_inject.c index 6c93a64a094..f84565b7fad 100644 --- a/src/backend/cluster/cluster_visibility_inject.c +++ b/src/backend/cluster/cluster_visibility_inject.c @@ -181,8 +181,11 @@ cluster_test_inject_visibility_tt_ref(PG_FUNCTION_ARGS) ClusterTTStatus install_status; ClusterTTStatusKey key; ClusterTTStatusResult res; + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; ClusterVisibilityInjectEntry *e; bool found; + bool looked_up; if (!superuser()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), @@ -249,7 +252,14 @@ cluster_test_inject_visibility_tt_ref(PG_FUNCTION_ARGS) key.cluster_epoch = epoch; key.local_xid = xid; - installed = cluster_tt_status_install_local(&key, install_status, commit_scn); + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &key; + source_request.status = install_status; + source_request.commit_scn = commit_scn; + installed = cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_LOCAL, &source_request, + &source_result) + == CLUSTER_SEMANTIC_ADMISSION_OK + && source_result.bool_value; /* * 3. spec-3.4c F5 + spec-3.4d D9: install_local() is best-effort and @@ -259,7 +269,14 @@ cluster_test_inject_visibility_tt_ref(PG_FUNCTION_ARGS) * IN_PROGRESS + InvalidScn commit_scn (not COMMITTED + valid * commit_scn). */ - if (!installed || !cluster_tt_status_lookup_exact(&key, &res) || res.status != install_status + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &key; + looked_up = cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, &source_request, + &source_result) + == CLUSTER_SEMANTIC_ADMISSION_OK + && source_result.bool_value; + res = source_result.lookup; + if (!installed || !looked_up || res.status != install_status || res.commit_scn != commit_scn) ereport(ERROR, (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("cluster TT status overlay install verification failed"), @@ -294,6 +311,8 @@ cluster_test_clear_visibility_injects(PG_FUNCTION_ARGS) hash_seq_init(&hseq, ClusterVisibilityInjectHTAB); while ((e = (ClusterVisibilityInjectEntry *)hash_seq_search(&hseq)) != NULL) { ClusterTTStatusKey key; + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; /* F4: build exact key + real delete_exact (NOT fake ABORT install). */ memset(&key, 0, sizeof(key)); @@ -302,7 +321,10 @@ cluster_test_clear_visibility_injects(PG_FUNCTION_ARGS) key.tt_slot_id = e->ref.tt_slot_id; key.cluster_epoch = e->ref.cluster_epoch; key.local_xid = e->ref.local_xid; - (void)cluster_tt_status_delete_exact(&key); + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &key; + (void)cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_DELETE_EXACT, &source_request, + &source_result); hash_search(ClusterVisibilityInjectHTAB, &e->xid, HASH_REMOVE, NULL); removed++; @@ -340,6 +362,8 @@ cluster_test_inject_subtrans_subcommitted(PG_FUNCTION_ARGS) uint32 epoch; ClusterTTStatusKey child_key; ClusterTTStatusKey parent_key; + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; if (!superuser()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), @@ -367,7 +391,13 @@ cluster_test_inject_subtrans_subcommitted(PG_FUNCTION_ARGS) parent_key.cluster_epoch = epoch; parent_key.local_xid = parent_xid; - if (!cluster_tt_status_install_subcommitted(&child_key, &parent_key)) + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &child_key; + source_request.parent_key = &parent_key; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_INSTALL_SUBCOMMITTED, &source_request, + &source_result) + != CLUSTER_SEMANTIC_ADMISSION_OK + || !source_result.bool_value) ereport(ERROR, (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("cluster_test_inject_subtrans_subcommitted: install failed"), errhint("Raise cluster.tt_status_overlay_max_entries or lower TTL."))); diff --git a/src/backend/cluster/cluster_visibility_resolve.c b/src/backend/cluster/cluster_visibility_resolve.c index 537acbef124..56dc9b28dd0 100644 --- a/src/backend/cluster/cluster_visibility_resolve.c +++ b/src/backend/cluster/cluster_visibility_resolve.c @@ -193,6 +193,8 @@ resolve_from_remote_ref(TransactionId raw_xid, const ClusterUndoTTSlotRef *ref, { ClusterTTStatusKey key; ClusterTTStatusResult result; + ClusterTTStatusSourceRequest source_request; + ClusterTTStatusSourceResult source_result; ClusterXpScope xp_scope; /* PGRAC: spec-5.59 D3 profiling */ cluster_xp_begin(&xp_scope, CLXP_R_TT_VISIBILITY_RESOLVE); @@ -254,7 +256,11 @@ resolve_from_remote_ref(TransactionId raw_xid, const ClusterUndoTTSlotRef *ref, } } - if (!cluster_tt_status_lookup_exact(&key, &result) || !result.authoritative) { + memset(&source_request, 0, sizeof(source_request)); + source_request.key = &key; + if (cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP, &source_request, &source_result) + != CLUSTER_SEMANTIC_ADMISSION_OK + || !source_result.bool_value || !source_result.lookup.authoritative) { /* PGRAC: spec-6.12c D0 -- lookup performed; no terminal verdict. */ cluster_lever_c_note_tt_lookup(ref->has_cached_status, false); /* PGRAC: spec-7.1a D4 -- overlay miss on a LIVE remote ref: pull the @@ -263,6 +269,7 @@ resolve_from_remote_ref(TransactionId raw_xid, const ClusterUndoTTSlotRef *ref, cluster_xp_end(&xp_scope); /* PGRAC: spec-5.59 D3 profiling */ return; /* UNKNOWN -> caller 53R97 (C-V2: no PG-native fallback) */ } + result = source_result.lookup; /* * spec-3.5: follow a SUBCOMMITTED subxact to its parent so the caller diff --git a/src/test/cluster_unit/Makefile b/src/test/cluster_unit/Makefile index c78517a5e6f..6eb1dad705c 100644 --- a/src/test/cluster_unit/Makefile +++ b/src/test/cluster_unit/Makefile @@ -55,6 +55,7 @@ TESTS = test_cluster_basic test_cluster_version test_cluster_backend_types \ test_cluster_itl_cleanout test_cluster_visibility_inject test_cluster_itl_cleanout_perf \ test_cluster_heap_lock_tuple test_cluster_perf_gates \ test_cluster_subtrans test_cluster_multixact test_cluster_multixact_served test_cluster_mxid_stripe \ + test_cluster_r4_d10_hint_source test_cluster_r4_d10_tt_source test_cluster_r4_d10_multi_source \ test_cluster_undo_format \ test_cluster_undo_record test_cluster_undo_lifecycle test_cluster_undo_block0 test_cluster_cr test_cluster_cr_cache test_cluster_cr_key test_cluster_cr_pool test_cluster_cr_lifecycle test_cluster_cr_admit test_cluster_cr_admit_stat test_cluster_cr_tuple test_cluster_cr_tuple_stat test_cluster_resolver_cache test_cluster_cr_coordinator test_cluster_r4_static_model test_cluster_r4_tx_locator test_cluster_r4_tx_outcome test_cluster_r4_tx_enqueue test_cluster_r4_wire_codec test_cluster_r4_route_policy test_cluster_r4_slot_machine test_cluster_r4_cr_walk test_cluster_r4_multi_subx_2pc test_cluster_r4_activation_record test_cluster_r4_activation_fsm test_cluster_r4_lock_order test_cluster_tt_durable test_cluster_terminal_authority test_cluster_sf_dep \ test_cluster_retention test_cluster_undo_cleaner test_cluster_visibility_variants test_cluster_writer_chain test_cluster_tt_2pc \ @@ -247,6 +248,8 @@ test_cluster_backup: test_cluster_backup.c unit_test.h $(CLUSTER_VERSION_O) \ # objects (the test files stub the PG backend symbols those # objects reference). SIMPLE_TESTS = $(filter-out test_cluster_ic_tier1_partial test_cluster_lms_outbound 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_scn_frontier test_cluster_adg test_cluster_epoch test_cluster_fence test_cluster_reconfig test_cluster_ges test_cluster_grd_outbound 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_writer_chain 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_mxid_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_block_dedup_reclaim test_cluster_gcs_block_dedup_htab 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_active_itl_transfer test_cluster_itl_wal test_cluster_multixact test_cluster_multixact_served test_cluster_pcm_lock test_cluster_pcm_own test_cluster_pcm_direct_init test_cluster_pcm_x_convert test_cluster_pcm_x_image_fetch 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_undo_block0 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_xid_authority test_cluster_recovery_anchor test_cluster_relmap_authority test_cluster_lms_shard test_cluster_gcs_block_dedup test_cluster_gcs_block_shard test_cluster_undo_resid test_cluster_undo_authority test_cluster_undo_gcs test_cluster_undo_verdict test_cluster_vis_undo_verdict_map test_cluster_undo_horizon test_cluster_r4_static_model test_cluster_r4_tx_locator test_cluster_r4_tx_outcome test_cluster_r4_cr_walk test_cluster_r4_activation_record test_cluster_r4_activation_fsm test_cluster_r4_lock_order,$(TESTS)) +SIMPLE_TESTS := $(filter-out test_cluster_r4_d10_hint_source test_cluster_r4_d10_tt_source \ + test_cluster_r4_d10_multi_source,$(SIMPLE_TESTS)) # spec-2.4 D16: test_cluster_epoch links cluster_epoch.o standalone. # cluster_epoch.c references ShmemInitStruct + cluster_shmem_register_region @@ -1918,6 +1921,25 @@ test_cluster_multixact: test_cluster_multixact.c unit_test.h \ $(top_builddir)/src/common/libpgcommon_srv.a \ $(top_builddir)/src/port/libpgport_srv.a -o $@ +# spec-8.4 R4 D10: real-module behavior tests for the three non-CR +# typed SOURCE dispatch families. Each fixture replaces only adjacent +# PostgreSQL/runtime boundaries and executes its owning product object. +CLUSTER_R4_D10_HINT_SOURCE_O = $(top_builddir)/src/backend/cluster/cluster_tt_status_hint.o +CLUSTER_R4_D10_TT_SOURCE_O = $(top_builddir)/src/backend/cluster/cluster_tt_status.o +CLUSTER_R4_D10_MULTI_SOURCE_O = $(top_builddir)/src/backend/cluster/cluster_multixact.o + +test_cluster_r4_d10_hint_source: test_cluster_r4_d10_hint_source.c unit_test.h \ + $(CLUSTER_R4_D10_HINT_SOURCE_O) + $(CC) $(CFLAGS) $(CPPFLAGS) $< $(CLUSTER_R4_D10_HINT_SOURCE_O) -o $@ + +test_cluster_r4_d10_tt_source: test_cluster_r4_d10_tt_source.c unit_test.h \ + $(CLUSTER_R4_D10_TT_SOURCE_O) + $(CC) $(CFLAGS) $(CPPFLAGS) $< $(CLUSTER_R4_D10_TT_SOURCE_O) -o $@ + +test_cluster_r4_d10_multi_source: test_cluster_r4_d10_multi_source.c unit_test.h \ + $(CLUSTER_R4_D10_MULTI_SOURCE_O) + $(CC) $(CFLAGS) $(CPPFLAGS) $< $(CLUSTER_R4_D10_MULTI_SOURCE_O) -o $@ + # spec-3.7 D12: test_cluster_undo_format — 12 pure ABI / offsetof / # sizeof / magic / enum / SQLSTATE tests for Undo Record Format + # Allocator module + HC211-HC217 ABI lock. Behavioural / DML emit / diff --git a/src/test/cluster_unit/test_cluster_lmon.c b/src/test/cluster_unit/test_cluster_lmon.c index a6c4501da3b..0fbd1ff1139 100644 --- a/src/test/cluster_unit/test_cluster_lmon.c +++ b/src/test/cluster_unit/test_cluster_lmon.c @@ -40,6 +40,7 @@ #include "cluster/cluster_ic_rdma.h" #include "cluster/cluster_lmon.h" +#include "cluster/cluster_tt_status_hint.h" #undef printf #undef fprintf @@ -448,10 +449,14 @@ void cluster_sinval_broadcast_reset_all(void) {} -/* spec-3.2 D6 + D1: LMON drain hook + msg_type register. */ -void -cluster_tt_status_hint_drain_outbound(void) -{} +/* spec-3.2 D6 + D1 / spec-8.4 D10: gated drain + msg_type register. */ +ClusterSemanticAdmissionResult +cluster_tt_status_hint_source_dispatch( + ClusterTTStatusHintSourceOp op pg_attribute_unused(), + const ClusterTTStatusHintSourceRequest *request pg_attribute_unused()) +{ + return CLUSTER_SEMANTIC_ADMISSION_OK; +} void cluster_tt_status_hint_register_msg_type(void) {} diff --git a/src/test/cluster_unit/test_cluster_multixact.c b/src/test/cluster_unit/test_cluster_multixact.c index 3781878610f..6825b0fb1b0 100644 --- a/src/test/cluster_unit/test_cluster_multixact.c +++ b/src/test/cluster_unit/test_cluster_multixact.c @@ -67,38 +67,12 @@ ExceptionalCondition(const char *conditionName pg_attribute_unused(), /* ===== Local stubs — pure-ABI binary does not link real cluster.o ===== */ -bool -cluster_multixact_member_overlay_install( - const ClusterMultiXactKey *key pg_attribute_unused(), uint16 member_count pg_attribute_unused(), - const ClusterMultiXactMember *members pg_attribute_unused()) +ClusterSemanticAdmissionResult +cluster_multixact_source_dispatch(ClusterMultiXactSourceOp op pg_attribute_unused(), + const ClusterMultiXactSourceRequest *request pg_attribute_unused(), + ClusterMultiXactSourceResult *result pg_attribute_unused()) { - return false; -} - -bool -cluster_multixact_member_overlay_lookup(const ClusterMultiXactKey *key pg_attribute_unused(), - ClusterMultiXactMemberOverlayResult *out, - int max_members_buf pg_attribute_unused()) -{ - if (out != NULL) { - out->authoritative = false; - out->member_count = 0; - } - return false; -} - -ClusterVisibilityDecision -cluster_multixact_resolve_visibility(const ClusterMultiXactMemberOverlayResult *overlay - pg_attribute_unused(), - const Snapshot snap pg_attribute_unused()) -{ - return CLUSTER_VISIBILITY_UNKNOWN; -} - -uint16 -cluster_multixact_get_member_count(const ClusterMultiXactKey *key pg_attribute_unused()) -{ - return 0; + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; } void @@ -131,11 +105,13 @@ cluster_multixact_get_resolve_visibility_count(void) return 0; } -void -cluster_tt_status_hint_emit_multixact_overlay( - const ClusterMultiXactKey *key pg_attribute_unused(), uint16 member_count pg_attribute_unused(), - const ClusterMultiXactMember *members pg_attribute_unused()) -{} +ClusterSemanticAdmissionResult +cluster_tt_status_hint_source_dispatch( + ClusterTTStatusHintSourceOp op pg_attribute_unused(), + const ClusterTTStatusHintSourceRequest *request pg_attribute_unused()) +{ + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; +} uint64 cluster_tt_status_hint_get_v4_drop_unknown_count(void) @@ -204,18 +180,11 @@ UT_TEST(t5_v4_outbound_entry_sizeof) UT_TEST(t6_cluster_multixact_api_link) { - ClusterMultiXactKey key; - ClusterMultiXactMember member; - ClusterMultiXactMemberOverlayResult res; - - memset(&key, 0, sizeof(key)); - memset(&member, 0, sizeof(member)); - memset(&res, 0, sizeof(res)); - - UT_ASSERT(!cluster_multixact_member_overlay_install(&key, 1, &member)); - UT_ASSERT(!cluster_multixact_member_overlay_lookup(&key, &res, 1)); - (void)cluster_multixact_resolve_visibility(&res, NULL); - UT_ASSERT_EQ((int)cluster_multixact_get_member_count(&key), 0); + UT_ASSERT_EQ((int)CLUSTER_MULTI_SOURCE_OVERLAY_INSTALL, 0); + UT_ASSERT_EQ((int)CLUSTER_MULTI_SOURCE_OVERLAY_LOOKUP, 1); + UT_ASSERT_EQ((int)CLUSTER_MULTI_SOURCE_RESOLVE_VISIBILITY, 2); + UT_ASSERT_EQ((int)CLUSTER_MULTI_SOURCE_GET_MEMBER_COUNT, 3); + UT_ASSERT_NE((void *)cluster_multixact_source_dispatch, NULL); cluster_multixact_purge_epoch(1); } @@ -224,12 +193,8 @@ UT_TEST(t6_cluster_multixact_api_link) UT_TEST(t7_hint_emit_multixact_overlay_link) { - ClusterMultiXactKey key; - ClusterMultiXactMember member; - - memset(&key, 0, sizeof(key)); - memset(&member, 0, sizeof(member)); - cluster_tt_status_hint_emit_multixact_overlay(&key, 1, &member); + UT_ASSERT_EQ((int)CLUSTER_TT_HINT_SOURCE_EMIT_MULTIXACT_OVERLAY, 2); + UT_ASSERT_NE((void *)cluster_tt_status_hint_source_dispatch, NULL); } diff --git a/src/test/cluster_unit/test_cluster_qvotec.c b/src/test/cluster_unit/test_cluster_qvotec.c index 7cff7988ee7..bfcb110929e 100644 --- a/src/test/cluster_unit/test_cluster_qvotec.c +++ b/src/test/cluster_unit/test_cluster_qvotec.c @@ -109,6 +109,7 @@ extern ClusterSemanticActivationResult cluster_qvotec_test_semantic_activation_r bool IsUnderPostmaster = false; volatile sig_atomic_t ConfigReloadPending = false; volatile sig_atomic_t ShutdownRequestPending = false; +volatile uint32 InterruptHoldoffCount = 0; int MyProcPid = 0; int cluster_node_id = 0; diff --git a/src/test/cluster_unit/test_cluster_r4_d10_hint_source.c b/src/test/cluster_unit/test_cluster_r4_d10_hint_source.c index 0f060091189..5c658af9c4d 100644 --- a/src/test/cluster_unit/test_cluster_r4_d10_hint_source.c +++ b/src/test/cluster_unit/test_cluster_r4_d10_hint_source.c @@ -152,27 +152,24 @@ cluster_epoch_get_current(void) return 1; } -bool -cluster_multixact_member_overlay_install( - const ClusterMultiXactKey *key pg_attribute_unused(), uint16 member_count pg_attribute_unused(), - const ClusterMultiXactMember *members pg_attribute_unused()) -{ - return true; -} - -bool -cluster_tt_status_install_local(const ClusterTTStatusKey *key pg_attribute_unused(), - ClusterTTStatus status pg_attribute_unused(), - SCN commit_scn pg_attribute_unused()) +ClusterSemanticAdmissionResult +cluster_multixact_source_dispatch(ClusterMultiXactSourceOp op pg_attribute_unused(), + const ClusterMultiXactSourceRequest *request pg_attribute_unused(), + ClusterMultiXactSourceResult *result) { - return true; + memset(result, 0, sizeof(*result)); + result->bool_value = true; + return CLUSTER_SEMANTIC_ADMISSION_OK; } -bool -cluster_tt_status_install_subcommitted(const ClusterTTStatusKey *child_key pg_attribute_unused(), - const ClusterTTStatusKey *parent_key pg_attribute_unused()) +ClusterSemanticAdmissionResult +cluster_tt_status_source_dispatch(ClusterTTStatusSourceOp op pg_attribute_unused(), + const ClusterTTStatusSourceRequest *request pg_attribute_unused(), + ClusterTTStatusSourceResult *result) { - return true; + memset(result, 0, sizeof(*result)); + result->bool_value = true; + return CLUSTER_SEMANTIC_ADMISSION_OK; } void diff --git a/src/test/cluster_unit/test_cluster_r4_d10_multi_source.c b/src/test/cluster_unit/test_cluster_r4_d10_multi_source.c index c6620ff89a2..3866cf85364 100644 --- a/src/test/cluster_unit/test_cluster_r4_d10_multi_source.c +++ b/src/test/cluster_unit/test_cluster_r4_d10_multi_source.c @@ -249,11 +249,13 @@ void cluster_shmem_register_region(const ClusterShmemRegion *region pg_attribute_unused()) {} -bool -cluster_tt_status_lookup_exact(const ClusterTTStatusKey *key pg_attribute_unused(), - ClusterTTStatusResult *result pg_attribute_unused()) +ClusterSemanticAdmissionResult +cluster_tt_status_source_dispatch(ClusterTTStatusSourceOp op pg_attribute_unused(), + const ClusterTTStatusSourceRequest *request pg_attribute_unused(), + ClusterTTStatusSourceResult *result) { - return false; + memset(result, 0, sizeof(*result)); + return CLUSTER_SEMANTIC_ADMISSION_OK; } ClusterTTStatusResult diff --git a/src/test/cluster_unit/test_cluster_r4_static_model.c b/src/test/cluster_unit/test_cluster_r4_static_model.c index 4ee26f6a9e4..e53ee7b7da2 100644 --- a/src/test/cluster_unit/test_cluster_r4_static_model.c +++ b/src/test/cluster_unit/test_cluster_r4_static_model.c @@ -329,50 +329,88 @@ r4_sources_have(const char *needle) } static bool -legacy_requester_origin_edge_present(void) +d10_requester_source_edge_present(void) { const char *const ordered[] = { "NodeId head_origin = uba_origin_node_id(chains[0].undo_segment_head);", "cluster_cr_coordinator_classify_origin(head_origin)", - "cluster_gcs_block_cr_fetch_and_wait(tag, read_scn, (int32)head_origin", + "cluster_r4_source_cr_dispatch(CLUSTER_R4_SOURCE_CR_FETCH", "/* PARTIAL: continue on the shipped page" }; return source_has_ordered(sources.cr_source, ordered, lengthof(ordered)) - && source_has_definition(sources.gcs_source, "cluster_gcs_block_cr_fetch_and_wait") + && source_has_definition(sources.gcs_source, "cluster_gcs_block_cr_fetch_and_wait_raw") + && source_has_definition(sources.gcs_source, "cluster_r4_source_cr_dispatch") && source_has(sources.gcs_source, "int32 origin_node"); } static bool -legacy_tt_overlay_edge_present(void) +d10_tt_source_edge_present(void) { return source_has_definition(sources.tt_status_source, "cluster_tt_status_lookup_exact") - && source_has(sources.vis_resolve_source, "cluster_tt_status_lookup_exact(&key"); + && source_has_definition(sources.tt_status_source, "cluster_tt_status_source_dispatch") + && source_has(sources.vis_resolve_source, + "cluster_tt_status_source_dispatch(CLUSTER_TT_SOURCE_LOOKUP"); } static bool -legacy_hint_queue_edge_present(void) +d10_hint_source_edge_present(void) { - return source_has_definition(sources.tt_hint_source, "cluster_tt_status_hint_emit") + return source_has_definition(sources.tt_hint_source, "cluster_tt_status_hint_emit_raw") && source_has_definition(sources.tt_hint_source, - "cluster_tt_status_hint_drain_outbound"); + "cluster_tt_status_hint_drain_outbound_raw") + && source_has_definition(sources.tt_hint_source, + "cluster_tt_status_hint_source_dispatch"); } static bool -legacy_multi_overlay_edge_present(void) +d10_multi_source_edge_present(void) { return source_has_definition(sources.multixact_source, - "cluster_multixact_member_overlay_install") + "cluster_multixact_member_overlay_install_raw") && source_has_definition(sources.multixact_source, - "cluster_multixact_member_overlay_lookup") + "cluster_multixact_member_overlay_lookup_raw") && source_has(sources.multixact_source, - "cluster_multixact_member_overlay_lookup(&mxkey"); + "cluster_multixact_source_dispatch_body(") + && source_has_definition(sources.multixact_source, + "cluster_multixact_source_dispatch"); +} + +static bool +all_four_d10_source_edges_present(void) +{ + return d10_requester_source_edge_present() && d10_tt_source_edge_present() + && d10_hint_source_edge_present() && d10_multi_source_edge_present(); +} + +static bool +dispatch_orders_gate_before_body(const char *source, const char *dispatch, const char *body) +{ + char marker[128]; + const char *start; + const char *const ordered[] = { "cluster_semantic_activation_enter", body }; + int n; + + n = snprintf(marker, sizeof(marker), "\n%s(", dispatch); + if (n <= 0 || n >= (int)sizeof(marker) || source == NULL) + return false; + start = strstr(source, marker); + return start != NULL && source_has_ordered(start, ordered, lengthof(ordered)); } static bool -all_four_legacy_source_edges_present(void) +all_four_d10_dispatches_gate_before_body(void) { - return legacy_requester_origin_edge_present() && legacy_tt_overlay_edge_present() - && legacy_hint_queue_edge_present() && legacy_multi_overlay_edge_present(); + return dispatch_orders_gate_before_body(sources.gcs_source, "cluster_r4_source_cr_dispatch", + "cluster_gcs_block_cr_fetch_and_wait_raw") + && dispatch_orders_gate_before_body(sources.tt_status_source, + "cluster_tt_status_source_dispatch", + "cluster_tt_status_lookup_exact") + && dispatch_orders_gate_before_body(sources.tt_hint_source, + "cluster_tt_status_hint_source_dispatch", + "cluster_tt_status_hint_emit_raw") + && dispatch_orders_gate_before_body(sources.multixact_source, + "cluster_multixact_source_dispatch", + "cluster_multixact_source_dispatch_body"); } static bool @@ -457,7 +495,7 @@ contract_actual(int contract_number) && (source_has_definition(sources.cr_source, "cluster_cr_build_on_holder") || source_has_definition(sources.cr_server_source, "cluster_cr_build_on_holder")) && source_has_ordered(sources.gcs_source, holder_order, lengthof(holder_order)); - bool legacy_origin_route = legacy_requester_origin_edge_present(); + bool legacy_origin_route = d10_requester_source_edge_present(); bool full_builder = (source_has_definition(sources.cr_source, "cluster_cr_build_on_holder") || source_has_definition(sources.cr_server_source, "cluster_cr_build_on_holder")) @@ -489,13 +527,13 @@ contract_actual(int contract_number) case 5: return locator_shape_is_exact() ? contract_required[4] : "ABSENT"; case 6: - if (semantic_gate && source_has(sources.semantic_source, "r4.requester_cr_dormant_source") - && source_has(sources.semantic_source, "r4.tt_overlay_dormant_source") - && source_has(sources.semantic_source, "r4.hint_queue_dormant_source") - && source_has(sources.semantic_source, "r4.multi_overlay_dormant_source")) + if (semantic_gate && all_four_d10_source_edges_present() + && source_has(sources.semantic_source, "active_bits") + && source_has(sources.tt_hint_source, "CLUSTER_TT_STATUS_HINT_V1") + && source_has(sources.tt_hint_source, "CLUSTER_TT_STATUS_HINT_V4")) return contract_required[5]; - return all_four_legacy_source_edges_present() ? "LEGACY_ORDINARY_AND_V1_V4_REACHABLE" - : "ABSENT"; + return all_four_d10_source_edges_present() ? "FOUR_SOURCE_EDGES_WITHOUT_ACTIVE_ZERO_PROOF" + : "ABSENT"; case 7: return semantic_gate && holder_route && sources.tx_resolve_source != NULL ? contract_required[6] @@ -532,13 +570,10 @@ contract_actual(int contract_number) ? contract_required[11] : "ABSENT"; case 13: - if (semantic_gate && all_four_legacy_source_edges_present() - && source_has(sources.semantic_source, "r4.requester_cr_dormant_source") - && source_has(sources.semantic_source, "r4.tt_overlay_dormant_source") - && source_has(sources.semantic_source, "r4.hint_queue_dormant_source") - && source_has(sources.semantic_source, "r4.multi_overlay_dormant_source")) + if (semantic_gate && all_four_d10_source_edges_present()) return contract_required[12]; - return all_four_legacy_source_edges_present() ? "FOUR_UNGATED_RAW_SOURCES" : "ABSENT"; + return all_four_d10_source_edges_present() ? "FOUR_SOURCE_EDGES_WITHOUT_COMMON_GATE" + : "ABSENT"; case 14: if (source_has(sources.semantic_source, "DefineCustomBoolVariable") || source_has(sources.semantic_source, "StartChildProcess") @@ -557,7 +592,7 @@ contract_actual(int contract_number) return contract_required[14]; return "RAW_OLD_SOURCE_LINK_VISIBLE"; case 16: - return semantic_gate && source_has(sources.semantic_source, "before mutation") + return semantic_gate && all_four_d10_dispatches_gate_before_body() ? contract_required[15] : "ABSENT"; case 17: @@ -888,10 +923,10 @@ UT_TEST(test_real_source_observation_controls) UT_ASSERT_NOT_NULL(sources.multixact_source); UT_ASSERT(source_has(sources.cr_source, "NodeId head_origin")); UT_ASSERT(source_has(sources.cr_source, "CLUSTER_CR_SPLIT_PARTIAL")); - UT_ASSERT(legacy_requester_origin_edge_present()); - UT_ASSERT(legacy_tt_overlay_edge_present()); - UT_ASSERT(legacy_hint_queue_edge_present()); - UT_ASSERT(legacy_multi_overlay_edge_present()); + UT_ASSERT(d10_requester_source_edge_present()); + UT_ASSERT(d10_tt_source_edge_present()); + UT_ASSERT(d10_hint_source_edge_present()); + UT_ASSERT(d10_multi_source_edge_present()); UT_ASSERT(legacy_d6_xmin_route_present()); UT_ASSERT(legacy_d6_live_page_semantics_present()); } diff --git a/src/test/cluster_unit/test_cluster_r4_tx_enqueue.c b/src/test/cluster_unit/test_cluster_r4_tx_enqueue.c index 7c06460a0cf..f9d54c6e775 100644 --- a/src/test/cluster_unit/test_cluster_r4_tx_enqueue.c +++ b/src/test/cluster_unit/test_cluster_r4_tx_enqueue.c @@ -249,16 +249,21 @@ cluster_tx_resolve_exact(const ClusterTxLocator *locator pg_attribute_unused(), return test_resolve_outcomes[pos]; } -bool -cluster_tt_status_lookup_exact(const ClusterTTStatusKey *key pg_attribute_unused(), - ClusterTTStatusResult *out) -{ +ClusterSemanticAdmissionResult +cluster_tt_status_source_dispatch(ClusterTTStatusSourceOp op, + const ClusterTTStatusSourceRequest *request, + ClusterTTStatusSourceResult *result) +{ + UT_ASSERT_EQ((int)op, (int)CLUSTER_TT_SOURCE_LOOKUP); + UT_ASSERT_NOT_NULL(request); + UT_ASSERT_NOT_NULL(request->key); + memset(result, 0, sizeof(*result)); if (!test_legacy_tt_found) - return false; - memset(out, 0, sizeof(*out)); - out->authoritative = true; - out->status = test_legacy_tt_status; - return true; + return CLUSTER_SEMANTIC_ADMISSION_OK; + result->bool_value = true; + result->lookup.authoritative = true; + result->lookup.status = test_legacy_tt_status; + return CLUSTER_SEMANTIC_ADMISSION_OK; } uint64 diff --git a/src/test/cluster_unit/test_cluster_subtrans.c b/src/test/cluster_unit/test_cluster_subtrans.c index e493b5a07b1..869e93e830d 100644 --- a/src/test/cluster_unit/test_cluster_subtrans.c +++ b/src/test/cluster_unit/test_cluster_subtrans.c @@ -116,17 +116,21 @@ cluster_subtrans_get_xact_has_state_check_count(void) return 0; } -bool -cluster_tt_status_install_subcommitted(const ClusterTTStatusKey *child_key pg_attribute_unused(), - const ClusterTTStatusKey *parent_key pg_attribute_unused()) +ClusterSemanticAdmissionResult +cluster_tt_status_source_dispatch(ClusterTTStatusSourceOp op pg_attribute_unused(), + const ClusterTTStatusSourceRequest *request pg_attribute_unused(), + ClusterTTStatusSourceResult *result pg_attribute_unused()) { - return false; + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; } -void -cluster_tt_status_hint_emit_subcommitted(const ClusterTTStatusKey *child_key pg_attribute_unused(), - const ClusterTTStatusKey *parent_key pg_attribute_unused()) -{} +ClusterSemanticAdmissionResult +cluster_tt_status_hint_source_dispatch( + ClusterTTStatusHintSourceOp op pg_attribute_unused(), + const ClusterTTStatusHintSourceRequest *request pg_attribute_unused()) +{ + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; +} uint64 cluster_tt_status_get_subcommitted_install_count(void) @@ -256,15 +260,8 @@ UT_TEST(t9_cluster_subtrans_api_symbols_link) UT_TEST(t10_install_subcommitted_links) { - ClusterTTStatusKey child; - ClusterTTStatusKey parent; - bool ok; - - memset(&child, 0, sizeof(child)); - memset(&parent, 0, sizeof(parent)); - - ok = cluster_tt_status_install_subcommitted(&child, &parent); - UT_ASSERT(!ok); /* stub returns false */ + UT_ASSERT_EQ((int)CLUSTER_TT_SOURCE_INSTALL_SUBCOMMITTED, 2); + UT_ASSERT_NE((void *)cluster_tt_status_source_dispatch, NULL); } @@ -272,14 +269,8 @@ UT_TEST(t10_install_subcommitted_links) UT_TEST(t11_hint_emit_subcommitted_links) { - ClusterTTStatusKey child; - ClusterTTStatusKey parent; - - memset(&child, 0, sizeof(child)); - memset(&parent, 0, sizeof(parent)); - - /* Stub no-op; test passes if call links. */ - cluster_tt_status_hint_emit_subcommitted(&child, &parent); + UT_ASSERT_EQ((int)CLUSTER_TT_HINT_SOURCE_EMIT_SUBCOMMITTED, 1); + UT_ASSERT_NE((void *)cluster_tt_status_hint_source_dispatch, NULL); /* Counter getter also links. */ UT_ASSERT_EQ((uint64)cluster_tt_status_hint_get_v3_downgrade_count(), (uint64)0); diff --git a/src/test/cluster_unit/test_cluster_tt_status.c b/src/test/cluster_unit/test_cluster_tt_status.c index 5cfffd42ffe..d8d297b07d1 100644 --- a/src/test/cluster_unit/test_cluster_tt_status.c +++ b/src/test/cluster_unit/test_cluster_tt_status.c @@ -13,8 +13,8 @@ * T5 CLUSTER_TT_STATUS_COMMITTED == 2 * T6 CLUSTER_TT_STATUS_ABORTED == 3 * T7 CLUSTER_TT_STATUS_CLEANED_OUT == 4 - * T8 public API: cluster_tt_status_lookup_exact prototype linkable - * T9 public API: cluster_tt_status_install_local prototype linkable + * T8 D10 typed lookup operation + dispatch prototype linkable + * T9 D10 typed install operation + dispatch prototype linkable * T10 public API: cluster_tt_status_flush_all prototype linkable * T11 public API: cluster_tt_status_generation prototype linkable * T12 ClusterTTStatusKey field offsets locked (HC183 wire-stable) @@ -101,26 +101,12 @@ ExceptionalCondition(const char *conditionName pg_attribute_unused(), int cluster_tt_status_overlay_max_entries = 32768; int cluster_tt_status_overlay_ttl_ms = 30000; -bool -cluster_tt_status_lookup_exact(const ClusterTTStatusKey *key pg_attribute_unused(), - ClusterTTStatusResult *result pg_attribute_unused()) -{ - return false; -} - -bool -cluster_tt_status_install_local(const ClusterTTStatusKey *key pg_attribute_unused(), - ClusterTTStatus status pg_attribute_unused(), - SCN commit_scn pg_attribute_unused()) +ClusterSemanticAdmissionResult +cluster_tt_status_source_dispatch(ClusterTTStatusSourceOp op pg_attribute_unused(), + const ClusterTTStatusSourceRequest *request pg_attribute_unused(), + ClusterTTStatusSourceResult *result pg_attribute_unused()) { - return false; -} - -int -cluster_tt_status_resolve_prepared_commit(TransactionId xid pg_attribute_unused(), - SCN commit_scn pg_attribute_unused()) -{ - return 0; + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; } void @@ -133,10 +119,6 @@ cluster_tt_status_generation(void) return 0; } -void -cluster_tt_status_bump_self_consumer_hit(void) -{} - Size cluster_tt_status_shmem_size(void) { @@ -227,15 +209,18 @@ UT_TEST(test_t7_enum_cleaned_out_four) /* ===== T8-T11: public API linkable ===== */ UT_TEST(test_t8_lookup_exact_linkable) { - UT_ASSERT_NE((void *)cluster_tt_status_lookup_exact, NULL); + UT_ASSERT_EQ((int)CLUSTER_TT_SOURCE_LOOKUP, 0); + UT_ASSERT_NE((void *)cluster_tt_status_source_dispatch, NULL); } UT_TEST(test_t9_install_local_linkable) { - UT_ASSERT_NE((void *)cluster_tt_status_install_local, NULL); + UT_ASSERT_EQ((int)CLUSTER_TT_SOURCE_INSTALL_LOCAL, 1); + UT_ASSERT_NE((void *)cluster_tt_status_source_dispatch, NULL); } UT_TEST(test_t9b_resolve_prepared_commit_linkable) { - UT_ASSERT_NE((void *)cluster_tt_status_resolve_prepared_commit, NULL); + UT_ASSERT_EQ((int)CLUSTER_TT_SOURCE_RESOLVE_PREPARED_COMMIT, 4); + UT_ASSERT_NE((void *)cluster_tt_status_source_dispatch, NULL); } UT_TEST(test_t10_flush_all_linkable) { @@ -331,7 +316,8 @@ UT_TEST(test_t20_shmem_helpers_linkable) /* ===== T21: self-consumer hit bump linkable (v0.4 N7) ===== */ UT_TEST(test_t21_self_consumer_hit_linkable) { - UT_ASSERT_NE((void *)cluster_tt_status_bump_self_consumer_hit, NULL); + UT_ASSERT_EQ((int)CLUSTER_TT_SOURCE_BUMP_SELF_CONSUMER_HIT, 5); + UT_ASSERT_NE((void *)cluster_tt_status_source_dispatch, NULL); } /* ===== T22: enum distinctness ===== */ diff --git a/src/test/cluster_unit/test_cluster_tt_status_hint.c b/src/test/cluster_unit/test_cluster_tt_status_hint.c index b8cb01a213e..19c79b1d5e2 100644 --- a/src/test/cluster_unit/test_cluster_tt_status_hint.c +++ b/src/test/cluster_unit/test_cluster_tt_status_hint.c @@ -78,18 +78,13 @@ ExceptionalCondition(const char *conditionName pg_attribute_unused(), int cluster_tt_status_hint_outbound_capacity = 256; int cluster_tt_status_hint_emit_mode = CLUSTER_TT_STATUS_HINT_EMIT_ALL_STATUS; -void -cluster_tt_status_hint_emit(const ClusterTTStatusKey *key pg_attribute_unused(), - ClusterTTStatus status pg_attribute_unused(), - SCN commit_scn pg_attribute_unused()) -{} -void -cluster_tt_status_hint_handle_envelope(const ClusterICEnvelope *env pg_attribute_unused(), - const void *payload pg_attribute_unused()) -{} -void -cluster_tt_status_hint_drain_outbound(void) -{} +ClusterSemanticAdmissionResult +cluster_tt_status_hint_source_dispatch( + ClusterTTStatusHintSourceOp op pg_attribute_unused(), + const ClusterTTStatusHintSourceRequest *request pg_attribute_unused()) +{ + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; +} void cluster_tt_status_hint_register_msg_type(void) {} @@ -162,9 +157,10 @@ UT_TEST(test_t6_producer_mask_lmon) /* ===== T7: API prototypes linkable ===== */ UT_TEST(test_t7_api_linkable) { - UT_ASSERT_NE((void *)cluster_tt_status_hint_emit, NULL); - UT_ASSERT_NE((void *)cluster_tt_status_hint_handle_envelope, NULL); - UT_ASSERT_NE((void *)cluster_tt_status_hint_drain_outbound, NULL); + UT_ASSERT_EQ((int)CLUSTER_TT_HINT_SOURCE_EMIT, 0); + UT_ASSERT_EQ((int)CLUSTER_TT_HINT_SOURCE_HANDLE_ENVELOPE, 3); + UT_ASSERT_EQ((int)CLUSTER_TT_HINT_SOURCE_DRAIN_OUTBOUND, 4); + UT_ASSERT_NE((void *)cluster_tt_status_hint_source_dispatch, NULL); UT_ASSERT_NE((void *)cluster_tt_status_hint_register_msg_type, NULL); } diff --git a/src/test/cluster_unit/test_cluster_visibility_inject.c b/src/test/cluster_unit/test_cluster_visibility_inject.c index 8e0fa4eb875..c7226e56d19 100644 --- a/src/test/cluster_unit/test_cluster_visibility_inject.c +++ b/src/test/cluster_unit/test_cluster_visibility_inject.c @@ -7,9 +7,9 @@ * 16 tests covering: * T1 cluster_test_lookup_visibility_inject API linkable * T2 cluster_visibility_inject_shmem_* APIs linkable - * T3 cluster_tt_status_lookup_exact API linkable - * T4 cluster_tt_status_install_local API linkable - * T5 cluster_tt_status_delete_exact API linkable (spec-3.4c D6 / F4) + * T3 typed TT lookup dispatch API linkable + * T4 typed TT install dispatch API linkable + * T5 typed TT delete dispatch API linkable (spec-3.4c D6 / F4) * T6 ClusterUndoTTSlotRef.cached_commit_scn at offset 16 (D9 stash sink) * T7 ClusterUndoTTSlotRef.has_cached_status at offset 24 (D9 toggle) * T8 ClusterUndoTTSlotRef sizeof 32 (regression) @@ -97,23 +97,12 @@ void cluster_visibility_inject_shmem_register(void) {} -bool -cluster_tt_status_lookup_exact(const ClusterTTStatusKey *key pg_attribute_unused(), - ClusterTTStatusResult *res pg_attribute_unused()) -{ - return false; -} -bool -cluster_tt_status_install_local(const ClusterTTStatusKey *key pg_attribute_unused(), - ClusterTTStatus status pg_attribute_unused(), - SCN commit_scn pg_attribute_unused()) +ClusterSemanticAdmissionResult +cluster_tt_status_source_dispatch(ClusterTTStatusSourceOp op pg_attribute_unused(), + const ClusterTTStatusSourceRequest *request pg_attribute_unused(), + ClusterTTStatusSourceResult *result pg_attribute_unused()) { - return false; -} -bool -cluster_tt_status_delete_exact(const ClusterTTStatusKey *key pg_attribute_unused()) -{ - return false; + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; } @@ -145,17 +134,20 @@ UT_TEST(t2_inject_shmem_helpers_linkable) } UT_TEST(t3_tt_status_lookup_exact_linkable) { - UT_ASSERT_NE((void *)cluster_tt_status_lookup_exact, NULL); + UT_ASSERT_EQ((int)CLUSTER_TT_SOURCE_LOOKUP, 0); + UT_ASSERT_NE((void *)cluster_tt_status_source_dispatch, NULL); } UT_TEST(t4_tt_status_install_local_linkable) { - UT_ASSERT_NE((void *)cluster_tt_status_install_local, NULL); + UT_ASSERT_EQ((int)CLUSTER_TT_SOURCE_INSTALL_LOCAL, 1); + UT_ASSERT_NE((void *)cluster_tt_status_source_dispatch, NULL); } UT_TEST(t5_tt_status_delete_exact_linkable) { /* spec-3.4c D6 / F4: per-key delete companion required so D5b * clear UDF does not fake-clear via ABORTED install. */ - UT_ASSERT_NE((void *)cluster_tt_status_delete_exact, NULL); + UT_ASSERT_EQ((int)CLUSTER_TT_SOURCE_DELETE_EXACT, 3); + UT_ASSERT_NE((void *)cluster_tt_status_source_dispatch, NULL); } From fb60a6a75123dd4c9cc9d5a44cf6114b49713e5a Mon Sep 17 00:00:00 2001 From: SqlRush Date: Mon, 10 Aug 2026 11:25:52 +0800 Subject: [PATCH 7/8] fix(cluster): close D10 admission review findings --- src/backend/cluster/cluster_cr_server.c | 94 ++++++++------ src/backend/cluster/cluster_gcs_block.c | 98 ++++++++------- .../cluster/cluster_semantic_activation.c | 75 ++++++------ src/backend/cluster/cluster_tx_resolve.c | 71 ++++++----- .../test_cluster_r4_activation_fsm.c | 96 ++++++++++++++- .../test_cluster_r4_static_model.c | 115 +++++++++++++++++- .../cluster_unit/test_cluster_r4_tx_locator.c | 45 ++++++- 7 files changed, 429 insertions(+), 165 deletions(-) diff --git a/src/backend/cluster/cluster_cr_server.c b/src/backend/cluster/cluster_cr_server.c index 7aa041ceec9..8a73cf15f81 100644 --- a/src/backend/cluster/cluster_cr_server.c +++ b/src/backend/cluster/cluster_cr_server.c @@ -150,6 +150,8 @@ cluster_cr_build_on_holder(const BufferTag *tag, SCN read_scn, char dst[BLCKSZ], ClusterSemanticAdmissionToken admission; ClusterSemanticAdmissionResult admission_result; ClusterBufmgrGcsCopyRefusal refusal = CLUSTER_BUFMGR_GCS_COPY_REFUSAL_NONE; + ClusterCrBuildReason reason = CLUSTER_CR_BUILD_NONE; + ClusterCrBuildResult result = CLUSTER_CR_BUILD_FAIL_CLOSED; PGAlignedBlock current_copy; XLogRecPtr page_lsn = InvalidXLogRecPtr; SCN page_scn = InvalidScn; @@ -174,61 +176,73 @@ cluster_cr_build_on_holder(const BufferTag *tag, SCN read_scn, char dst[BLCKSZ], return CLUSTER_CR_BUILD_RETRYABLE; } - if (tag == NULL || !SCN_VALID(read_scn)) { - *reason_out = CLUSTER_CR_BUILD_PROTOCOL; - cluster_semantic_activation_leave(&admission); - return CLUSTER_CR_BUILD_FAIL_CLOSED; - } + PG_TRY(); + { + if (tag == NULL || !SCN_VALID(read_scn)) { + reason = CLUSTER_CR_BUILD_PROTOCOL; + goto admitted_done; + } - if (!cluster_bufmgr_copy_block_for_r4_cr(*tag, InvalidScn, &page_lsn, &page_scn, + if (!cluster_bufmgr_copy_block_for_r4_cr(*tag, InvalidScn, &page_lsn, &page_scn, current_copy.data, &refusal)) { - cluster_semantic_activation_leave(&admission); - if (refusal == CLUSTER_BUFMGR_GCS_COPY_REFUSAL_INVALID_ARGUMENT) { - *reason_out = CLUSTER_CR_BUILD_PROTOCOL; - return CLUSTER_CR_BUILD_FAIL_CLOSED; - } - switch (refusal) { + if (refusal == CLUSTER_BUFMGR_GCS_COPY_REFUSAL_INVALID_ARGUMENT) { + reason = CLUSTER_CR_BUILD_PROTOCOL; + goto admitted_done; + } + switch (refusal) { case CLUSTER_BUFMGR_GCS_COPY_REFUSAL_NOT_RESIDENT: case CLUSTER_BUFMGR_GCS_COPY_REFUSAL_CURRENT_INVALID: case CLUSTER_BUFMGR_GCS_COPY_REFUSAL_CONTENT_LOCK_FIRST: case CLUSTER_BUFMGR_GCS_COPY_REFUSAL_CONTENT_LOCK_SECOND: case CLUSTER_BUFMGR_GCS_COPY_REFUSAL_OWNERSHIP_REVOKE_BUSY: - *reason_out = CLUSTER_CR_BUILD_HOLDER_MOVED; - return CLUSTER_CR_BUILD_RETRYABLE; + reason = CLUSTER_CR_BUILD_HOLDER_MOVED; + result = CLUSTER_CR_BUILD_RETRYABLE; + goto admitted_done; default: - *reason_out = CLUSTER_CR_BUILD_PROTOCOL; - return CLUSTER_CR_BUILD_FAIL_CLOSED; + reason = CLUSTER_CR_BUILD_PROTOCOL; + goto admitted_done; + } } - } - PG_TRY(); - { - cluster_cr_construct_page_for_server(current_copy.data, read_scn, *tag, dst, &partial); - constructed = true; + PG_TRY(); + { + cluster_cr_construct_page_for_server(current_copy.data, read_scn, *tag, dst, &partial); + constructed = true; + } + PG_CATCH(); + { + constructed = false; + FlushErrorState(); + } + PG_END_TRY(); + + if (!cluster_semantic_activation_recheck(&admission)) { + memset(dst, 0, BLCKSZ); + reason = CLUSTER_CR_BUILD_RF_DEFERRED; + result = CLUSTER_CR_BUILD_RETRYABLE; + goto admitted_done; + } + + if (!constructed || partial) { + memset(dst, 0, BLCKSZ); + reason = CLUSTER_CR_BUILD_BAD_UNDO; + goto admitted_done; + } + + reason = CLUSTER_CR_BUILD_NONE; + result = CLUSTER_CR_BUILD_FULL; + +admitted_done: + ; } - PG_CATCH(); + PG_FINALLY(); { - constructed = false; - FlushErrorState(); - } - PG_END_TRY(); - - if (!cluster_semantic_activation_recheck(&admission)) { - memset(dst, 0, BLCKSZ); - *reason_out = CLUSTER_CR_BUILD_RF_DEFERRED; cluster_semantic_activation_leave(&admission); - return CLUSTER_CR_BUILD_RETRYABLE; - } - cluster_semantic_activation_leave(&admission); - - if (!constructed || partial) { - memset(dst, 0, BLCKSZ); - *reason_out = CLUSTER_CR_BUILD_BAD_UNDO; - return CLUSTER_CR_BUILD_FAIL_CLOSED; } + PG_END_TRY(); - *reason_out = CLUSTER_CR_BUILD_NONE; - return CLUSTER_CR_BUILD_FULL; + *reason_out = reason; + return result; } static ClusterCrServerShared *CrServerShared = NULL; diff --git a/src/backend/cluster/cluster_gcs_block.c b/src/backend/cluster/cluster_gcs_block.c index 992ca5365bd..b6e64b9d91b 100644 --- a/src/backend/cluster/cluster_gcs_block.c +++ b/src/backend/cluster/cluster_gcs_block.c @@ -2001,54 +2001,62 @@ cluster_gcs_block_r4_route_cr(const BufferTag *tag, SCN read_scn, uint64 request return CLUSTER_CR_BUILD_RETRYABLE; } - if (tag == NULL || !SCN_VALID(read_scn) || request_id == 0 || requester_backend_id <= 0 - || requester_backend_id > MaxBackends) { - reason = CLUSTER_CR_BUILD_PROTOCOL; - goto done; - } - - current_epoch = cluster_epoch_get_current(); - real_master_node = cluster_gcs_lookup_master(*tag); - if (real_master_node < 0 || real_master_node >= PCM_X_PROTOCOL_NODE_LIMIT - || cluster_node_id != real_master_node) { - reason = CLUSTER_CR_BUILD_WRONG_MASTER; - goto done; - } - if (cluster_gcs_block_phase_for_tag(*tag) == GCS_BLOCK_RECOVERING) { - reason = CLUSTER_CR_BUILD_RECOVERING; - goto done; - } + PG_TRY(); + { + if (tag == NULL || !SCN_VALID(read_scn) || request_id == 0 || requester_backend_id <= 0 + || requester_backend_id > MaxBackends) { + reason = CLUSTER_CR_BUILD_PROTOCOL; + goto done; + } - if (!cluster_pcm_lock_r4_route_snapshot(*tag, &authority, - &master_authority_generation, - &expected_page_scn)) { - reason = CLUSTER_CR_BUILD_NO_HOLDER; - goto done; - } - reason = cluster_r4_route_policy_classify(&authority, current_epoch, - master_authority_generation, - ¤t_holder_node); - if (reason != CLUSTER_CR_BUILD_NONE) - goto done; + current_epoch = cluster_epoch_get_current(); + real_master_node = cluster_gcs_lookup_master(*tag); + if (real_master_node < 0 || real_master_node >= PCM_X_PROTOCOL_NODE_LIMIT + || cluster_node_id != real_master_node) { + reason = CLUSTER_CR_BUILD_WRONG_MASTER; + goto done; + } + if (cluster_gcs_block_phase_for_tag(*tag) == GCS_BLOCK_RECOVERING) { + reason = CLUSTER_CR_BUILD_RECOVERING; + goto done; + } - if (!cluster_semantic_activation_recheck(&admission)) { - reason = CLUSTER_CR_BUILD_RF_DEFERRED; - goto done; - } + if (!cluster_pcm_lock_r4_route_snapshot(*tag, &authority, + &master_authority_generation, + &expected_page_scn)) { + reason = CLUSTER_CR_BUILD_NO_HOLDER; + goto done; + } + reason = cluster_r4_route_policy_classify(&authority, current_epoch, + master_authority_generation, + ¤t_holder_node); + if (reason != CLUSTER_CR_BUILD_NONE) + goto done; + + if (!cluster_semantic_activation_recheck(&admission)) { + reason = CLUSTER_CR_BUILD_RF_DEFERRED; + goto done; + } - out->tag = *tag; - out->read_scn = read_scn; - out->formation_epoch = current_epoch; - out->activation_generation = admission.record_generation; - out->master_authority_generation = master_authority_generation; - out->master_resource_transition_count = authority.transition_count; - out->expected_page_scn = expected_page_scn; - out->real_master_node = real_master_node; - out->selected_holder_node = current_holder_node; + out->tag = *tag; + out->read_scn = read_scn; + out->formation_epoch = current_epoch; + out->activation_generation = admission.record_generation; + out->master_authority_generation = master_authority_generation; + out->master_resource_transition_count = authority.transition_count; + out->expected_page_scn = expected_page_scn; + out->real_master_node = real_master_node; + out->selected_holder_node = current_holder_node; done: - result = cluster_cr_build_result_for_reason(reason); - cluster_semantic_activation_leave(&admission); + result = cluster_cr_build_result_for_reason(reason); + } + PG_FINALLY(); + { + cluster_semantic_activation_leave(&admission); + } + PG_END_TRY(); + *reason_out = reason; return result; } @@ -4180,14 +4188,12 @@ cluster_r4_source_cr_dispatch(ClusterR4SourceCrOp op, const ClusterR4SourceCrReq && !cluster_semantic_activation_recheck(&token)) admission = CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED; } - PG_CATCH(); + PG_FINALLY(); { cluster_semantic_activation_leave(&token); - PG_RE_THROW(); } PG_END_TRY(); - cluster_semantic_activation_leave(&token); if (admission == CLUSTER_SEMANTIC_ADMISSION_OK) *result = local_result; return admission; diff --git a/src/backend/cluster/cluster_semantic_activation.c b/src/backend/cluster/cluster_semantic_activation.c index b0bce2ed6c3..936e9d4049a 100644 --- a/src/backend/cluster/cluster_semantic_activation.c +++ b/src/backend/cluster/cluster_semantic_activation.c @@ -700,17 +700,19 @@ cluster_semantic_activation_enter(uint64 feature_bit, ClusterSemanticAdmissionSi if (!incremented) return CLUSTER_SEMANTIC_ADMISSION_CLOSED; - epoch_after = cluster_epoch_get_current(); if (!semantic_activation_snapshot(&after)) result = CLUSTER_SEMANTIC_ADMISSION_CLOSED; - else if (before.seq != after.seq || before.record_generation != after.record_generation - || before.formation_epoch != after.formation_epoch || epoch_before != epoch_after - || after.formation_epoch != epoch_after) - result = CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED; - else - result = semantic_activation_admission_policy( - feature_bit, after.active_bits, after.transition_closed, side, before.record_generation, - after.record_generation); + else { + epoch_after = cluster_epoch_get_current(); + if (before.seq != after.seq || before.record_generation != after.record_generation + || before.formation_epoch != after.formation_epoch || epoch_before != epoch_after + || after.formation_epoch != epoch_after) + result = CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED; + else + result = semantic_activation_admission_policy( + feature_bit, after.active_bits, after.transition_closed, side, + before.record_generation, after.record_generation); + } if (result != CLUSTER_SEMANTIC_ADMISSION_OK) { HOLD_INTERRUPTS(); semantic_activation_release_debt(side, feature_index); @@ -739,9 +741,10 @@ cluster_semantic_activation_recheck(const ClusterSemanticAdmissionToken *token) || token->side > CLUSTER_SEMANTIC_TARGET_SIDE || SemanticActivationShmem == NULL) return false; (void)feature_index; + if (!semantic_activation_snapshot(&snapshot)) + return false; current_epoch = cluster_epoch_get_current(); - if (!semantic_activation_snapshot(&snapshot) || snapshot.formation_epoch != current_epoch - || token->formation_epoch != current_epoch) + if (snapshot.formation_epoch != current_epoch || token->formation_epoch != current_epoch) return false; return semantic_activation_admission_policy( @@ -1006,48 +1009,38 @@ cluster_semantic_activation_record_decode(const uint8 bytes[512], void cluster_semantic_activation_lmon_tick(void) { - uint64 seq; - uint64 active_bits; - uint64 generation; - uint64 formation_epoch; + SemanticActivationAdmissionSnapshot snapshot; + uint64 current_epoch; + uint64 expected_seq; if (SemanticActivationShmem == NULL) return; - seq = pg_atomic_read_u64(&SemanticActivationShmem->admission_seq); - if ((seq & UINT64_C(1)) != 0) { - if (seq == UINT64_MAX) - ereport(PANIC, (errcode(ERRCODE_INTERNAL_ERROR), - errmsg("semantic activation admission sequence exhausted"), - errhint("Retain the shared-memory image and restart the cluster."))); - pg_atomic_write_u64(&SemanticActivationShmem->active_bits, 0); - pg_atomic_write_u64(&SemanticActivationShmem->record_generation, 0); - pg_atomic_write_u64(&SemanticActivationShmem->formation_epoch, cluster_epoch_get_current()); - pg_atomic_write_u32(&SemanticActivationShmem->transition_closed, 1); - pg_write_barrier(); - pg_atomic_write_u64(&SemanticActivationShmem->admission_seq, seq + 1); + /* + * D13 owns validated majority-zero/durable-OPEN publication. Until that + * proof is available, odd or unreadable state remains fail-closed. + */ + if (!semantic_activation_snapshot(&snapshot)) return; - } - active_bits = pg_atomic_read_u64(&SemanticActivationShmem->active_bits); - generation = pg_atomic_read_u64(&SemanticActivationShmem->record_generation); - formation_epoch = cluster_epoch_get_current(); - if (active_bits != 0 || generation != 0 - || (pg_atomic_read_u32(&SemanticActivationShmem->transition_closed) == 0 - && pg_atomic_read_u64(&SemanticActivationShmem->formation_epoch) == formation_epoch)) + current_epoch = cluster_epoch_get_current(); + if (snapshot.formation_epoch == current_epoch) return; - if (seq > UINT64_MAX - 2) + if (snapshot.seq > UINT64_MAX - 2) ereport(PANIC, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("semantic activation admission sequence exhausted"), errhint("Retain the shared-memory image and restart the cluster."))); - pg_atomic_write_u64(&SemanticActivationShmem->admission_seq, seq + 1); + expected_seq = snapshot.seq; + if (!pg_atomic_compare_exchange_u64(&SemanticActivationShmem->admission_seq, &expected_seq, + snapshot.seq + 1)) + return; pg_write_barrier(); - pg_atomic_write_u64(&SemanticActivationShmem->active_bits, 0); - pg_atomic_write_u64(&SemanticActivationShmem->record_generation, 0); - pg_atomic_write_u64(&SemanticActivationShmem->formation_epoch, formation_epoch); - pg_atomic_write_u32(&SemanticActivationShmem->transition_closed, 0); + pg_atomic_write_u64(&SemanticActivationShmem->active_bits, snapshot.active_bits); + pg_atomic_write_u64(&SemanticActivationShmem->record_generation, snapshot.record_generation); + pg_atomic_write_u64(&SemanticActivationShmem->formation_epoch, current_epoch); + pg_atomic_write_u32(&SemanticActivationShmem->transition_closed, 1); pg_write_barrier(); - pg_atomic_write_u64(&SemanticActivationShmem->admission_seq, seq + 2); + pg_atomic_write_u64(&SemanticActivationShmem->admission_seq, snapshot.seq + 2); } ClusterSemanticActivationResult diff --git a/src/backend/cluster/cluster_tx_resolve.c b/src/backend/cluster/cluster_tx_resolve.c index 3fd0b8b1b41..2aa006a71c4 100644 --- a/src/backend/cluster/cluster_tx_resolve.c +++ b/src/backend/cluster/cluster_tx_resolve.c @@ -195,41 +195,50 @@ cluster_tx_resolve_exact(const ClusterTxLocator *locator, ClusterTxResolveMode m goto done; } - if (out == NULL || (unsigned int)mode > (unsigned int)CLUSTER_TX_RESOLVE_CLEANOUT_HINT) { - reason = CLUSTER_TX_RESOLVE_PROTOCOL; - goto admitted_done; - } - if (!cluster_tx_locator_is_well_formed(locator, &reason)) - goto admitted_done; + PG_TRY(); + { + if (out == NULL || (unsigned int)mode > (unsigned int)CLUSTER_TX_RESOLVE_CLEANOUT_HINT) { + reason = CLUSTER_TX_RESOLVE_PROTOCOL; + goto admitted_done; + } + if (!cluster_tx_locator_is_well_formed(locator, &reason)) + goto admitted_done; - formation_epoch = cluster_epoch_get_current(); - outcome = cluster_runtime_visibility_resolve_exact_origin( - locator, mode, formation_epoch, &candidate, &provider_reason); - if (outcome == CLUSTER_TX_UNKNOWN) { - reason = provider_reason == CLUSTER_TX_RESOLVE_NONE - || !cluster_tx_resolve_reason_is_known(provider_reason) - ? CLUSTER_TX_RESOLVE_PROTOCOL - : provider_reason; - goto admitted_done; - } - if (!cluster_tx_resolution_is_publishable(locator, mode, formation_epoch, outcome, &candidate, - provider_reason)) { - outcome = CLUSTER_TX_UNKNOWN; - reason = CLUSTER_TX_RESOLVE_PROTOCOL; - goto admitted_done; - } - if (cluster_epoch_get_current() != formation_epoch - || !cluster_semantic_activation_recheck(&admission)) { - outcome = CLUSTER_TX_UNKNOWN; - reason = CLUSTER_TX_RESOLVE_RF_DEFERRED; - goto admitted_done; - } + formation_epoch = cluster_epoch_get_current(); + outcome = cluster_runtime_visibility_resolve_exact_origin( + locator, mode, formation_epoch, &candidate, &provider_reason); + if (outcome == CLUSTER_TX_UNKNOWN) { + reason = provider_reason == CLUSTER_TX_RESOLVE_NONE + || !cluster_tx_resolve_reason_is_known(provider_reason) + ? CLUSTER_TX_RESOLVE_PROTOCOL + : provider_reason; + goto admitted_done; + } + if (!cluster_tx_resolution_is_publishable(locator, mode, formation_epoch, outcome, + &candidate, provider_reason)) { + outcome = CLUSTER_TX_UNKNOWN; + reason = CLUSTER_TX_RESOLVE_PROTOCOL; + goto admitted_done; + } + if (cluster_epoch_get_current() != formation_epoch + || !cluster_semantic_activation_recheck(&admission)) { + outcome = CLUSTER_TX_UNKNOWN; + reason = CLUSTER_TX_RESOLVE_RF_DEFERRED; + goto admitted_done; + } - *out = candidate; - reason = CLUSTER_TX_RESOLVE_NONE; + *out = candidate; + reason = CLUSTER_TX_RESOLVE_NONE; admitted_done: - cluster_semantic_activation_leave(&admission); + ; + } + PG_FINALLY(); + { + cluster_semantic_activation_leave(&admission); + } + PG_END_TRY(); + done: if (reason_out != NULL) *reason_out = reason; diff --git a/src/test/cluster_unit/test_cluster_r4_activation_fsm.c b/src/test/cluster_unit/test_cluster_r4_activation_fsm.c index 5de0071cc60..043baf64987 100644 --- a/src/test/cluster_unit/test_cluster_r4_activation_fsm.c +++ b/src/test/cluster_unit/test_cluster_r4_activation_fsm.c @@ -33,6 +33,8 @@ static pg_on_exit_callback test_exit_callback; static Datum test_exit_callback_arg; static int test_exit_registration_count; static uint64 test_current_epoch = 7; +static int test_read_barrier_count; +static int test_advance_epoch_on_read_barrier; int MyProcPid = 101; volatile sig_atomic_t InterruptPending = false; @@ -68,6 +70,18 @@ void ProcessInterrupts(void) {} +static void +test_read_barrier(void) +{ + pg_read_barrier_impl(); + test_read_barrier_count++; + if (test_advance_epoch_on_read_barrier == test_read_barrier_count) + test_current_epoch++; +} + +#undef pg_read_barrier +#define pg_read_barrier() test_read_barrier() + bool errstart(int elevel pg_attribute_unused(), const char *domain pg_attribute_unused()) { @@ -151,6 +165,8 @@ test_gate_reset(void) test_exit_callback_arg = (Datum)0; test_exit_registration_count = 0; test_current_epoch = 7; + test_read_barrier_count = 0; + test_advance_epoch_on_read_barrier = 0; MyProcPid = 101; SemanticActivationShmem = NULL; memset(semantic_activation_local_inflight, 0, sizeof(semantic_activation_local_inflight)); @@ -938,7 +954,7 @@ UT_TEST(test_108_nonregistered_feature_is_closed_without_debt) UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 7)), 0); } -UT_TEST(test_109_lmon_legacy_zero_publish_opens_source_atomically) +UT_TEST(test_109_lmon_without_validated_majority_remains_closed) { ClusterSemanticAdmissionToken token; @@ -947,17 +963,83 @@ UT_TEST(test_109_lmon_legacy_zero_publish_opens_source_atomically) UT_ASSERT_EQ(pg_atomic_read_u64(test_gate_u64(TEST_GATE_SEQ_OFFSET)), 2); UT_ASSERT_EQ(pg_atomic_read_u64(test_gate_u64(TEST_GATE_FORMATION_EPOCH_OFFSET)), test_current_epoch); - UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_u32(TEST_GATE_CLOSED_OFFSET)), 0); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_u32(TEST_GATE_CLOSED_OFFSET)), 1); UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, - CLUSTER_SEMANTIC_SOURCE_SIDE, &token), + CLUSTER_SEMANTIC_SOURCE_SIDE, &token), + CLUSTER_SEMANTIC_ADMISSION_CLOSED); + UT_ASSERT(!token.entered); +} + +UT_TEST(test_110_lmon_odd_writer_remains_fail_closed) +{ + test_gate_reset(); + test_gate_publish(3, 0, 0, test_current_epoch, true); + cluster_semantic_activation_lmon_tick(); + UT_ASSERT_EQ(pg_atomic_read_u64(test_gate_u64(TEST_GATE_SEQ_OFFSET)), 3); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_u32(TEST_GATE_CLOSED_OFFSET)), 1); +} + +UT_TEST(test_111_formation_change_closes_before_debt_drain) +{ + ClusterSemanticAdmissionToken old_token; + ClusterSemanticAdmissionToken new_token; + + test_gate_reset(); + test_gate_publish(2, 0, 0, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &old_token), + CLUSTER_SEMANTIC_ADMISSION_OK); + test_current_epoch++; + cluster_semantic_activation_lmon_tick(); + UT_ASSERT_EQ(pg_atomic_read_u64(test_gate_u64(TEST_GATE_FORMATION_EPOCH_OFFSET)), + test_current_epoch); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_u32(TEST_GATE_CLOSED_OFFSET)), 1); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 1); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &new_token), + CLUSTER_SEMANTIC_ADMISSION_CLOSED); + if (new_token.entered) + cluster_semantic_activation_leave(&new_token); + cluster_semantic_activation_leave(&old_token); +} + +UT_TEST(test_112_enter_samples_second_snapshot_before_epoch) +{ + ClusterSemanticAdmissionToken token; + ClusterSemanticAdmissionResult result; + + test_gate_reset(); + test_gate_publish(2, 0, 21, test_current_epoch, false); + test_advance_epoch_on_read_barrier = 4; + result = cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &token); + UT_ASSERT_EQ(result, CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED); + UT_ASSERT(!token.entered); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 0); + if (token.entered) + cluster_semantic_activation_leave(&token); +} + +UT_TEST(test_113_recheck_samples_snapshot_before_epoch) +{ + ClusterSemanticAdmissionToken token; + + test_gate_reset(); + test_gate_publish(2, 0, 22, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &token), CLUSTER_SEMANTIC_ADMISSION_OK); + test_read_barrier_count = 0; + test_advance_epoch_on_read_barrier = 2; + UT_ASSERT(!cluster_semantic_activation_recheck(&token)); + UT_ASSERT_EQ(pg_atomic_read_u32(test_gate_inflight(CLUSTER_SEMANTIC_SOURCE_SIDE, 0)), 1); cluster_semantic_activation_leave(&token); } int main(void) { - UT_PLAN(109); + UT_PLAN(113); UT_RUN(test_01_feature_bit_is_one); UT_RUN(test_02_required_hello_caps_are_frozen); UT_RUN(test_03_action_values_are_frozen); @@ -1066,7 +1148,11 @@ main(void) UT_RUN(test_106_exit_hook_drains_both_side_ledgers); UT_RUN(test_107_odd_snapshot_is_bounded_closed_without_debt); UT_RUN(test_108_nonregistered_feature_is_closed_without_debt); - UT_RUN(test_109_lmon_legacy_zero_publish_opens_source_atomically); + UT_RUN(test_109_lmon_without_validated_majority_remains_closed); + UT_RUN(test_110_lmon_odd_writer_remains_fail_closed); + UT_RUN(test_111_formation_change_closes_before_debt_drain); + UT_RUN(test_112_enter_samples_second_snapshot_before_epoch); + UT_RUN(test_113_recheck_samples_snapshot_before_epoch); UT_DONE(); return ut_failed_count == 0 ? 0 : 1; } diff --git a/src/test/cluster_unit/test_cluster_r4_static_model.c b/src/test/cluster_unit/test_cluster_r4_static_model.c index e53ee7b7da2..9a86a8ba411 100644 --- a/src/test/cluster_unit/test_cluster_r4_static_model.c +++ b/src/test/cluster_unit/test_cluster_r4_static_model.c @@ -310,6 +310,72 @@ source_has_ordered(const char *source, const char *const *needles, size_t count) return true; } +static bool +function_region_has_ordered(const char *source, const char *symbol, const char *next_symbol, + const char *const *needles, size_t count) +{ + char start_marker[128]; + char end_marker[128]; + const char *start; + const char *end; + const char *cursor; + int n; + + n = snprintf(start_marker, sizeof(start_marker), "\n%s(", symbol); + if (n <= 0 || n >= (int)sizeof(start_marker) || source == NULL) + return false; + n = snprintf(end_marker, sizeof(end_marker), "\n%s(", next_symbol); + if (n <= 0 || n >= (int)sizeof(end_marker)) + return false; + start = strstr(source, start_marker); + end = start != NULL ? strstr(start + strlen(start_marker), end_marker) : NULL; + if (start == NULL || end == NULL) + return false; + + cursor = start; + for (size_t i = 0; i < count; i++) { + cursor = strstr(cursor, needles[i]); + if (cursor == NULL || cursor >= end) + return false; + cursor += strlen(needles[i]); + } + return true; +} + +static bool +function_region_has_single_finally_leave(const char *source, const char *symbol, + const char *next_symbol) +{ + const char *const ordered[] = { "PG_FINALLY();", "cluster_semantic_activation_leave" }; + char start_marker[128]; + char end_marker[128]; + const char *start; + const char *end; + const char *cursor; + int finally_count = 0; + int leave_count = 0; + int n; + + if (!function_region_has_ordered(source, symbol, next_symbol, ordered, lengthof(ordered))) + return false; + n = snprintf(start_marker, sizeof(start_marker), "\n%s(", symbol); + if (n <= 0 || n >= (int)sizeof(start_marker)) + return false; + n = snprintf(end_marker, sizeof(end_marker), "\n%s(", next_symbol); + if (n <= 0 || n >= (int)sizeof(end_marker)) + return false; + start = strstr(source, start_marker); + end = strstr(start + strlen(start_marker), end_marker); + for (cursor = start; (cursor = strstr(cursor, "PG_FINALLY();")) != NULL && cursor < end; + cursor += strlen("PG_FINALLY();")) + finally_count++; + for (cursor = start; (cursor = strstr(cursor, "cluster_semantic_activation_leave")) != NULL + && cursor < end; + cursor += strlen("cluster_semantic_activation_leave")) + leave_count++; + return finally_count == 1 && leave_count == 1; +} + static bool r4_sources_have(const char *needle) { @@ -413,6 +479,41 @@ all_four_d10_dispatches_gate_before_body(void) "cluster_multixact_source_dispatch_body"); } +static bool +d10_admitted_wrappers_have_single_finally_leave(void) +{ + return function_region_has_single_finally_leave( + sources.gcs_source, "cluster_r4_source_cr_dispatch", + "cluster_gcs_block_undo_tt_fetch_and_wait") + && function_region_has_single_finally_leave( + sources.gcs_source, "cluster_gcs_block_r4_route_cr", + "cluster_gcs_block_redo_lsn_covered") + && function_region_has_single_finally_leave( + sources.cr_server_source, "cluster_cr_build_on_holder", + "cluster_cr_server_shmem_size") + && function_region_has_single_finally_leave( + sources.tx_resolve_source, "cluster_tx_resolve_exact", + "cluster_tx_resolve_multixact"); +} + +static bool +d10_epoch_sampling_order_is_exact(void) +{ + const char *const enter_order[] + = { "pg_write_barrier();", "semantic_activation_snapshot(&after)", + "epoch_after = cluster_epoch_get_current()" }; + const char *const recheck_order[] + = { "semantic_activation_snapshot(&snapshot)", + "current_epoch = cluster_epoch_get_current()" }; + + return function_region_has_ordered( + sources.semantic_source, "cluster_semantic_activation_enter", + "cluster_semantic_activation_recheck", enter_order, lengthof(enter_order)) + && function_region_has_ordered( + sources.semantic_source, "cluster_semantic_activation_recheck", + "cluster_semantic_activation_leave", recheck_order, lengthof(recheck_order)); +} + static bool legacy_d6_xmin_route_present(void) { @@ -955,6 +1056,16 @@ UT_TEST(test_source_edges_match_required_model) assert_contract_matches(16); } +UT_TEST(test_d10_admitted_wrappers_have_single_finally_leave) +{ + UT_ASSERT(d10_admitted_wrappers_have_single_finally_leave()); +} + +UT_TEST(test_d10_epoch_sampling_order_is_exact) +{ + UT_ASSERT(d10_epoch_sampling_order_is_exact()); +} + UT_TEST(test_d6_live_scratch_edges_match_required_model) { assert_contract_matches(17); @@ -994,7 +1105,7 @@ main(int argc pg_attribute_unused(), char **const argv pg_attribute_unused()) model_hash_ok = digest_model_rows(model_rows, model_row_count, model_hash); emitted = emit_model(); - UT_PLAN(11); + UT_PLAN(13); UT_RUN(test_model_shape_and_key_order_controls); UT_RUN(test_matrix_closed_keyspace_control); UT_RUN(test_canonical_hash_and_mutation_controls); @@ -1003,6 +1114,8 @@ main(int argc pg_attribute_unused(), char **const argv pg_attribute_unused()) UT_RUN(test_full_only_matches_required_model); UT_RUN(test_locator_matches_required_model); UT_RUN(test_source_edges_match_required_model); + UT_RUN(test_d10_admitted_wrappers_have_single_finally_leave); + UT_RUN(test_d10_epoch_sampling_order_is_exact); UT_RUN(test_d6_live_scratch_edges_match_required_model); UT_RUN(test_matrix_actions_match_required_model); UT_RUN(test_remaining_contracts_match_required_model); diff --git a/src/test/cluster_unit/test_cluster_r4_tx_locator.c b/src/test/cluster_unit/test_cluster_r4_tx_locator.c index 431adb860cd..8763c039c03 100644 --- a/src/test/cluster_unit/test_cluster_r4_tx_locator.c +++ b/src/test/cluster_unit/test_cluster_r4_tx_locator.c @@ -46,6 +46,18 @@ static int test_enter_calls; static int test_recheck_calls; static int test_leave_calls; static int test_provider_calls; +static bool test_provider_raise; + +sigjmp_buf *PG_exception_stack = NULL; +ErrorContextCallback *error_context_stack = NULL; + +void +pg_re_throw(void) +{ + if (PG_exception_stack != NULL) + siglongjmp(*PG_exception_stack, 1); + abort(); +} ClusterSemanticAdmissionResult cluster_semantic_activation_enter(uint64 feature_bit, ClusterSemanticAdmissionSide side, @@ -94,6 +106,8 @@ cluster_runtime_visibility_resolve_exact_origin(const ClusterTxLocator *locator, ClusterTxResolveReason *reason_out) { test_provider_calls++; + if (test_provider_raise) + siglongjmp(*PG_exception_stack, 1); test_provider_locator = *locator; test_provider_mode = mode; test_provider_epoch = formation_epoch; @@ -187,6 +201,7 @@ reset_exact_resolver_fixture(void) test_recheck_calls = 0; test_leave_calls = 0; test_provider_calls = 0; + test_provider_raise = false; } static ClusterItlSlotData * @@ -357,6 +372,33 @@ UT_TEST(test_exact_cleanout_consumer_rejects_live_provider_outcome) UT_ASSERT(bytes_are_zero(&resolution, sizeof(resolution))); } +UT_TEST(test_exact_resolver_error_releases_target_admission_once) +{ + ClusterTxLocator locator = exact_locator(); + ClusterTxResolution resolution; + ClusterTxResolveReason reason = CLUSTER_TX_RESOLVE_NONE; + volatile bool caught = false; + + reset_exact_resolver_fixture(); + test_provider_raise = true; + PG_TRY(); + { + (void)cluster_tx_resolve_exact(&locator, CLUSTER_TX_RESOLVE_VISIBILITY, &resolution, + &reason); + } + PG_CATCH(); + { + caught = true; + } + PG_END_TRY(); + + UT_ASSERT(caught); + UT_ASSERT_EQ(test_enter_calls, 1); + UT_ASSERT_EQ(test_provider_calls, 1); + UT_ASSERT_EQ(test_recheck_calls, 0); + UT_ASSERT_EQ(test_leave_calls, 1); +} + UT_TEST(test_multixact_resolver_is_dormant_and_zeroes_output) { ClusterMultiResolution resolution; @@ -665,7 +707,7 @@ UT_TEST(test_epoch_is_absent_from_locator_value) int main(void) { - UT_PLAN(37); + UT_PLAN(38); UT_RUN(test_frozen_identity_layout); UT_RUN(test_frozen_closed_domains); UT_RUN(test_reason_names_are_stable); @@ -674,6 +716,7 @@ main(void) UT_RUN(test_exact_resolver_rejects_mismatched_locator_echo); UT_RUN(test_exact_resolver_discards_provider_result_when_activation_generation_moves); UT_RUN(test_exact_cleanout_consumer_rejects_live_provider_outcome); + UT_RUN(test_exact_resolver_error_releases_target_admission_once); UT_RUN(test_multixact_resolver_is_dormant_and_zeroes_output); UT_RUN(test_bad_locator_scaffold_fails_closed_and_zeroes_output); UT_RUN(test_valid_caller_selected_data_slot_forms_exact_locator); From d0e32b7c3dc27f6a75a1a91b6491f48d0b78897e Mon Sep 17 00:00:00 2001 From: SqlRush Date: Mon, 10 Aug 2026 19:14:16 +0800 Subject: [PATCH 8/8] feat(cluster): add R4 route dedup and typed refusal --- src/backend/cluster/cluster_cr_server.c | 17 + src/backend/cluster/cluster_gcs_block.c | 472 ++++++- src/backend/cluster/cluster_gcs_block_dedup.c | 463 ++++++- src/backend/cluster/cluster_gcs_block_shard.c | 6 +- src/backend/cluster/cluster_lms_outbound.c | 81 +- .../cluster/cluster_semantic_activation.c | 22 + src/backend/cluster/cluster_sf_dep.c | 23 + src/include/cluster/cluster_cr_server.h | 8 + src/include/cluster/cluster_gcs_block.h | 83 +- src/include/cluster/cluster_gcs_block_dedup.h | 61 +- src/include/cluster/cluster_lms.h | 7 +- .../cluster/cluster_semantic_activation.h | 3 + src/include/cluster/cluster_sf_dep.h | 3 + src/test/cluster_unit/Makefile | 57 +- .../cluster_r4_activation_test_stubs.h | 10 + .../test_cluster_gcs_block_dedup_r4_route.c | 948 ++++++++++++++ .../test_cluster_gcs_block_shard.c | 97 +- .../cluster_unit/test_cluster_lms_outbound.c | 107 +- src/test/cluster_unit/test_cluster_qvotec.c | 9 + .../test_cluster_r4_activation_fsm.c | 95 +- .../test_cluster_r4_route_policy.c | 1158 ++++++++++++++++- .../cluster_unit/test_cluster_r4_wire_codec.c | 33 +- src/test/cluster_unit/test_cluster_sf_dep.c | 172 +++ 23 files changed, 3847 insertions(+), 88 deletions(-) create mode 100644 src/test/cluster_unit/test_cluster_gcs_block_dedup_r4_route.c diff --git a/src/backend/cluster/cluster_cr_server.c b/src/backend/cluster/cluster_cr_server.c index be3f3b133df..2edba4c2805 100644 --- a/src/backend/cluster/cluster_cr_server.c +++ b/src/backend/cluster/cluster_cr_server.c @@ -347,6 +347,23 @@ cluster_lms_cr_submit(const GcsBlockForwardPayload *fwd) return false; /* all slots busy — fail closed, requester retries/refuses */ } +/* + * cluster_lms_cr_submit_r4 — typed R4 FORWARD96 holder-submit boundary. + * + * D3 deliberately does not reinterpret the 96-byte route proof as the + * legacy 64-byte payload: that would discard the proof. D4 owns the future + * stable-copy and slot-proof positive integration, so this pre-D4 boundary + * remains fail closed without allocating a slot or mutating shared state. + */ +bool +cluster_lms_cr_submit_r4(const ClusterR4CrForwardPayload *forward) +{ + if (forward == NULL) + return false; + + return false; +} + /* * cluster_lms_undo_fetch_submit — CONTROL-plane park (spec-6.12i D-i1). * diff --git a/src/backend/cluster/cluster_gcs_block.c b/src/backend/cluster/cluster_gcs_block.c index 26d3fb28e1c..bd151186178 100644 --- a/src/backend/cluster/cluster_gcs_block.c +++ b/src/backend/cluster/cluster_gcs_block.c @@ -1967,62 +1967,257 @@ cluster_gcs_block_phase_for_tag(BufferTag tag) return GCS_BLOCK_NORMAL; } -/* Stage 8 R4 D3: read one canonical master/current-holder route proof. */ +#define R4_CR_REQUIRED_HELLO_CAPS \ + (PGRAC_IC_HELLO_CAP_SEMANTIC_ACTIVATION_V1 | PGRAC_IC_HELLO_CAP_R4_SYNC_CR_V1) + +typedef struct GcsBlockR4ReplyExpectation { + uint64 request_id; + uint64 epoch; + int32 requester_backend_id; + uint8 transition_id; + int32 sender_node; +} GcsBlockR4ReplyExpectation; + +static bool gcs_block_decode_r4_reply_payload( + const ClusterICEnvelope *env, const void *payload, + const GcsBlockR4ReplyExpectation *expected) pg_attribute_unused(); + +/* Return true when D3 must publish an immediate refusal and false only for + * the proved admitted FORWARD96 success. Invalid result/reason pairs close + * as status 26; they never inherit retry polarity from one member alone. */ +static bool +gcs_block_r4_refusal_status_for_build(ClusterCrBuildResult result, + ClusterCrBuildReason reason, bool admitted_forward, + GcsBlockReplyStatus *status_out) +{ + if (status_out == NULL) + return true; + *status_out = GCS_BLOCK_REPLY_R4_DENIED; + if (result == CLUSTER_CR_BUILD_FULL && reason == CLUSTER_CR_BUILD_NONE + && admitted_forward) + return false; + if (result == CLUSTER_CR_BUILD_RETRYABLE) { + switch (reason) { + case CLUSTER_CR_BUILD_TARGET_DISABLED: + case CLUSTER_CR_BUILD_RF_DEFERRED: + case CLUSTER_CR_BUILD_WRONG_MASTER: + case CLUSTER_CR_BUILD_NO_HOLDER: + case CLUSTER_CR_BUILD_HOLDER_AMBIGUOUS: + case CLUSTER_CR_BUILD_HOLDER_MOVED: + case CLUSTER_CR_BUILD_RECOVERING: + case CLUSTER_CR_BUILD_GENERATION_MISMATCH: + case CLUSTER_CR_BUILD_CAPACITY: + case CLUSTER_CR_BUILD_EPOCH_MISMATCH: + *status_out = GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED; + return true; + default: + break; + } + } + if (result == CLUSTER_CR_BUILD_FAIL_CLOSED) { + switch (reason) { + case CLUSTER_CR_BUILD_BAD_LOCATOR: + case CLUSTER_CR_BUILD_BAD_UNDO: + case CLUSTER_CR_BUILD_CHAIN_LIMIT: + case CLUSTER_CR_BUILD_SNAPSHOT_TOO_OLD: + case CLUSTER_CR_BUILD_CANCELLED: + case CLUSTER_CR_BUILD_IO_ERROR: + case CLUSTER_CR_BUILD_PROTOCOL: + return true; + default: + break; + } + } + return true; +} + +/* Exact D3 refusal decoder. The independent expectation is supplied by the + * R4 request slot owner; D3 exposes it to the focused unit seam while the R4 + * source slot remains a later deliverable. Legacy reply mutation never calls + * this path and rejects the entire 21..26 domain below. */ +static bool +gcs_block_decode_r4_reply_payload(const ClusterICEnvelope *env, const void *payload, + const GcsBlockR4ReplyExpectation *expected) +{ + const GcsBlockReplyHeader *header; + const char *block_data; + int i; + + if (env == NULL || payload == NULL || expected == NULL + || env->msg_type != PGRAC_IC_MSG_GCS_BLOCK_REPLY + || env->payload_length != GCS_BLOCK_REPLY_PAYLOAD_TOTAL_SIZE + || env->source_node_id != (uint32)expected->sender_node + || env->dest_node_id != (uint32)cluster_node_id) + return false; + header = (const GcsBlockReplyHeader *)payload; + block_data = ((const char *)payload) + sizeof(*header); + if (!GcsBlockReplyStatusIsR4Refusal((GcsBlockReplyStatus)header->status) + || header->request_id != expected->request_id || header->epoch != expected->epoch + || header->requester_backend_id != expected->requester_backend_id + || header->transition_id != expected->transition_id + || expected->transition_id != (uint8)PCM_TRANS_N_TO_S + || header->sender_node != expected->sender_node || expected->sender_node < 0 + || expected->sender_node >= CLUSTER_MAX_NODES + || GcsBlockReplyHeaderGetForwardingMasterNode(header) + != GCS_BLOCK_REPLY_NO_FORWARDING_MASTER + || header->checksum != gcs_block_compute_checksum(block_data)) + return false; + for (i = 0; i < (int)sizeof(header->reserved_0); i++) + if (header->reserved_0[i] != 0) + return false; + for (i = 0; i < GCS_BLOCK_DATA_SIZE; i++) + if (block_data[i] != 0) + return false; + if (header->status == (uint8)GCS_BLOCK_REPLY_R4_DENIED) + return header->page_lsn == 0; + return header->page_lsn <= (uint64)CLUSTER_MAX_NODES; +} + +static bool +gcs_block_r4_publish_refusal(int worker_id, const ClusterICEnvelope *env, + const ClusterR4CrRequestPayload *request, + uint32 requester_capability_generation, + ClusterCrBuildResult result, ClusterCrBuildReason reason, + bool admitted_forward, int32 current_master_node) +{ + GcsBlockReplyHeader header; + GcsBlockReplyStatus status; + + if (!gcs_block_r4_refusal_status_for_build(result, reason, admitted_forward, &status)) + return true; + memset(&header, 0, sizeof(header)); + header.request_id = request->base.request_id; + header.epoch = request->base.epoch; + header.sender_node = cluster_node_id; + header.requester_backend_id = request->base.requester_backend_id; + header.transition_id = request->base.transition_id; + header.status = (uint8)status; + if (status == GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED + && reason == CLUSTER_CR_BUILD_WRONG_MASTER && current_master_node >= 0 + && current_master_node < CLUSTER_MAX_NODES) + header.page_lsn = (uint64)(uint32)(current_master_node + 1); + GcsBlockReplyHeaderSetForwardingMasterNode( + &header, GCS_BLOCK_REPLY_NO_FORWARDING_MASTER); + return cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + worker_id, env->source_node_id, &header, R4_CR_REQUIRED_HELLO_CAPS, + requester_capability_generation); +} + +static bool +gcs_block_r4_request_base_valid(const ClusterICEnvelope *env, + const ClusterR4CrRequestPayload *request, + uint64 current_epoch, SCN *read_scn_out) +{ + int i; + + if (read_scn_out != NULL) + *read_scn_out = InvalidScn; + if (env == NULL || request == NULL || read_scn_out == NULL + || env->msg_type != PGRAC_IC_MSG_GCS_BLOCK_REQUEST + || env->payload_length != sizeof(*request) + || env->source_node_id >= PCM_X_PROTOCOL_NODE_LIMIT + || env->dest_node_id != (uint32)cluster_node_id + || request->base.sender_node != (int32)env->source_node_id + || request->base.request_id == 0 || request->base.requester_backend_id <= 0 + || request->base.requester_backend_id > MaxBackends + || request->base.transition_id != (uint8)PCM_TRANS_N_TO_S + || request->base.epoch != env->epoch || request->base.epoch != current_epoch + || request->base.reserved_0[0] != 0 || request->base.reserved_0[1] != 0 + || !ClusterR4RequestExtensionGetCr(&request->extension, read_scn_out)) + return false; + for (i = 6; i < (int)sizeof(request->base.reserved_0); i++) + if (request->base.reserved_0[i] != 0) + return false; + return true; +} + +/* Stage 8 R4 D3 request-level TARGET wrapper. One admission token dominates + * the same-OPEN join, sole PCM sample, typed route arm, cap-bound enqueue, + * send publication and final recheck. */ ClusterCrBuildResult -cluster_gcs_block_r4_route_cr(const BufferTag *tag, SCN read_scn, uint64 request_id, - int32 requester_backend_id, ClusterR4CrRouteProof *out, +cluster_gcs_block_r4_route_cr(const ClusterICEnvelope *env, + const ClusterR4CrRequestPayload *request, ClusterCrBuildReason *reason_out) { ClusterSemanticAdmissionToken admission; ClusterSemanticAdmissionResult admission_result; PcmAuthoritySnapshot authority; + GcsBlockR4RouteIdentity identity; + ClusterR4CrRouteProof fresh_proof; + GcsBlockR4RouteRecord stored_record; + ClusterR4CrForwardPayload forward; uint64 current_epoch; uint64 master_authority_generation; + SCN read_scn; SCN expected_page_scn; - int32 real_master_node; + uint32 requester_capability_generation = 0; + uint32 holder_capability_generation = 0; + bool requester_done_capable = false; + bool holder_optional = false; + bool outbound_admitted = false; + bool final_recheck_ok; + bool admitted_forward; + int32 real_master_node = -1; int32 current_holder_node = -1; - ClusterCrBuildReason reason = CLUSTER_CR_BUILD_NONE; - ClusterCrBuildResult result; + int dedup_worker_id; + GcsBlockR4RouteArmResult arm_result; + GcsBlockR4RouteSendResult send_result = GCS_BLOCK_R4_ROUTE_SEND_INVALID; + ClusterCrBuildReason reason = CLUSTER_CR_BUILD_PROTOCOL; + ClusterCrBuildResult result = CLUSTER_CR_BUILD_FAIL_CLOSED; - if (out != NULL) - memset(out, 0, sizeof(*out)); if (reason_out != NULL) - *reason_out = CLUSTER_CR_BUILD_NONE; + *reason_out = CLUSTER_CR_BUILD_PROTOCOL; memset(&admission, 0, sizeof(admission)); - - if (out == NULL || reason_out == NULL) + if (env == NULL || request == NULL || reason_out == NULL) return CLUSTER_CR_BUILD_FAIL_CLOSED; + dedup_worker_id = cluster_ic_tier1_my_data_channel(); + if (!cluster_sf_peer_capability_family_sample( + (int32)env->source_node_id, R4_CR_REQUIRED_HELLO_CAPS, + PGRAC_IC_HELLO_CAP_GCS_DONE_V1, &requester_done_capable, + &requester_capability_generation)) { + *reason_out = CLUSTER_CR_BUILD_TARGET_DISABLED; + return CLUSTER_CR_BUILD_RETRYABLE; + } admission_result = cluster_semantic_activation_enter( CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, CLUSTER_SEMANTIC_TARGET_SIDE, &admission); if (admission_result != CLUSTER_SEMANTIC_ADMISSION_OK) { - *reason_out = admission_result == CLUSTER_SEMANTIC_ADMISSION_TARGET_DISABLED - ? CLUSTER_CR_BUILD_TARGET_DISABLED - : CLUSTER_CR_BUILD_RF_DEFERRED; - return CLUSTER_CR_BUILD_RETRYABLE; + reason = admission_result == CLUSTER_SEMANTIC_ADMISSION_TARGET_DISABLED + ? CLUSTER_CR_BUILD_TARGET_DISABLED + : CLUSTER_CR_BUILD_RF_DEFERRED; + result = cluster_cr_build_result_for_reason(reason); + (void)gcs_block_r4_publish_refusal( + dedup_worker_id, env, request, requester_capability_generation, result, reason, + false, -1); + *reason_out = reason; + return result; } PG_TRY(); { - if (tag == NULL || !SCN_VALID(read_scn) || request_id == 0 || requester_backend_id <= 0 - || requester_backend_id > MaxBackends) { + if (!cluster_semantic_activation_peer_open_matches( + &admission, (int32)env->source_node_id, R4_CR_REQUIRED_HELLO_CAPS, + requester_capability_generation)) { + reason = CLUSTER_CR_BUILD_RF_DEFERRED; + goto done; + } + current_epoch = cluster_epoch_get_current(); + if (!gcs_block_r4_request_base_valid(env, request, current_epoch, &read_scn)) { reason = CLUSTER_CR_BUILD_PROTOCOL; goto done; } - current_epoch = cluster_epoch_get_current(); - real_master_node = cluster_gcs_lookup_master(*tag); + real_master_node = cluster_gcs_lookup_master(request->base.tag); if (real_master_node < 0 || real_master_node >= PCM_X_PROTOCOL_NODE_LIMIT || cluster_node_id != real_master_node) { reason = CLUSTER_CR_BUILD_WRONG_MASTER; goto done; } - if (cluster_gcs_block_phase_for_tag(*tag) == GCS_BLOCK_RECOVERING) { + if (cluster_gcs_block_phase_for_tag(request->base.tag) == GCS_BLOCK_RECOVERING) { reason = CLUSTER_CR_BUILD_RECOVERING; goto done; } - - if (!cluster_pcm_lock_r4_route_snapshot(*tag, &authority, + if (!cluster_pcm_lock_r4_route_snapshot(request->base.tag, &authority, &master_authority_generation, &expected_page_scn)) { reason = CLUSTER_CR_BUILD_NO_HOLDER; @@ -2034,23 +2229,96 @@ cluster_gcs_block_r4_route_cr(const BufferTag *tag, SCN read_scn, uint64 request if (reason != CLUSTER_CR_BUILD_NONE) goto done; - if (!cluster_semantic_activation_recheck(&admission)) { + if (!cluster_sf_peer_capability_family_sample( + current_holder_node, R4_CR_REQUIRED_HELLO_CAPS, 0, &holder_optional, + &holder_capability_generation) + || !cluster_semantic_activation_peer_open_matches( + &admission, current_holder_node, R4_CR_REQUIRED_HELLO_CAPS, + holder_capability_generation)) { reason = CLUSTER_CR_BUILD_RF_DEFERRED; goto done; } - out->tag = *tag; - out->read_scn = read_scn; - out->formation_epoch = current_epoch; - out->activation_generation = admission.record_generation; - out->master_authority_generation = master_authority_generation; - out->master_resource_transition_count = authority.transition_count; - out->expected_page_scn = expected_page_scn; - out->real_master_node = real_master_node; - out->selected_holder_node = current_holder_node; + memset(&identity, 0, sizeof(identity)); + identity.legacy_key.origin_node_id = env->source_node_id; + identity.legacy_key.requester_backend_id = request->base.requester_backend_id; + identity.legacy_key.request_id = request->base.request_id; + identity.legacy_key.cluster_epoch = current_epoch; + identity.tag = request->base.tag; + identity.read_scn = read_scn; + identity.activation_generation = admission.record_generation; + + memset(&fresh_proof, 0, sizeof(fresh_proof)); + fresh_proof.tag = request->base.tag; + fresh_proof.read_scn = read_scn; + fresh_proof.formation_epoch = current_epoch; + fresh_proof.activation_generation = admission.record_generation; + fresh_proof.master_authority_generation = master_authority_generation; + fresh_proof.master_resource_transition_count = authority.transition_count; + fresh_proof.expected_page_scn = expected_page_scn; + fresh_proof.real_master_node = real_master_node; + fresh_proof.selected_holder_node = current_holder_node; + + if (dedup_worker_id < 0 || dedup_worker_id >= cluster_lms_workers + || cluster_lms_shard_for_tag(&identity.tag, cluster_lms_workers) + != dedup_worker_id) { + reason = CLUSTER_CR_BUILD_PROTOCOL; + goto done; + } + arm_result = cluster_gcs_block_dedup_r4_route_arm_or_match( + dedup_worker_id, &identity, (uint8)PCM_TRANS_N_TO_S, &fresh_proof, + GcsBlockRequestPayloadGetLifetimeHintMs(&request->base), requester_done_capable, + &stored_record); + if (arm_result != GCS_BLOCK_R4_ROUTE_ARM_NEW + && arm_result != GCS_BLOCK_R4_ROUTE_ARM_REPLAY) { + reason = arm_result == GCS_BLOCK_R4_ROUTE_ARM_FULL ? CLUSTER_CR_BUILD_CAPACITY + : arm_result == GCS_BLOCK_R4_ROUTE_ARM_INVALID + ? CLUSTER_CR_BUILD_PROTOCOL + : CLUSTER_CR_BUILD_HOLDER_MOVED; + goto done; + } + + memset(&forward, 0, sizeof(forward)); + forward.base.request_id = identity.legacy_key.request_id; + forward.base.epoch = stored_record.proof.formation_epoch; + forward.base.tag = stored_record.proof.tag; + forward.base.original_requester_node = (int32)identity.legacy_key.origin_node_id; + forward.base.requester_backend_id = identity.legacy_key.requester_backend_id; + forward.base.master_node = stored_record.proof.real_master_node; + forward.base.transition_id = (uint8)PCM_TRANS_N_TO_S; + GcsBlockForwardPayloadSetExpectedPiWatermarkScn(&forward.base, + stored_record.proof.read_scn); + GcsBlockForwardPayloadSetCrRequest(&forward.base, true); + ClusterR4ForwardExtensionSetCrProof( + &forward.extension, stored_record.proof.master_authority_generation, + stored_record.proof.master_resource_transition_count, + stored_record.proof.expected_page_scn); + outbound_admitted = cluster_lms_outbound_enqueue_cap_bound( + dedup_worker_id, PGRAC_IC_MSG_GCS_BLOCK_FORWARD, + (uint32)stored_record.proof.selected_holder_node, &forward, sizeof(forward), + R4_CR_REQUIRED_HELLO_CAPS, holder_capability_generation); + send_result = cluster_gcs_block_dedup_r4_route_finish_send( + dedup_worker_id, &identity, (uint8)PCM_TRANS_N_TO_S, &stored_record.proof, + outbound_admitted); + if (!outbound_admitted || send_result != GCS_BLOCK_R4_ROUTE_SEND_FORWARDED) { + reason = send_result == GCS_BLOCK_R4_ROUTE_SEND_INVALID + ? CLUSTER_CR_BUILD_PROTOCOL + : CLUSTER_CR_BUILD_HOLDER_MOVED; + goto done; + } + reason = CLUSTER_CR_BUILD_NONE; done: + final_recheck_ok = cluster_semantic_activation_recheck(&admission); + if (!final_recheck_ok && reason == CLUSTER_CR_BUILD_NONE) + reason = CLUSTER_CR_BUILD_RF_DEFERRED; result = cluster_cr_build_result_for_reason(reason); + admitted_forward = final_recheck_ok && reason == CLUSTER_CR_BUILD_NONE + && outbound_admitted + && send_result == GCS_BLOCK_R4_ROUTE_SEND_FORWARDED; + (void)gcs_block_r4_publish_refusal( + dedup_worker_id, env, request, requester_capability_generation, result, reason, + admitted_forward, real_master_node); } PG_FINALLY(); { @@ -2062,6 +2330,130 @@ cluster_gcs_block_r4_route_cr(const BufferTag *tag, SCN read_scn, uint64 request return result; } +static bool +gcs_block_try_r4_request80(const ClusterICEnvelope *env, const void *payload) +{ + ClusterCrBuildReason reason; + + if (env == NULL || payload == NULL || env->msg_type != PGRAC_IC_MSG_GCS_BLOCK_REQUEST + || env->payload_length != sizeof(ClusterR4CrRequestPayload)) + return false; + (void)cluster_gcs_block_r4_route_cr( + env, (const ClusterR4CrRequestPayload *)payload, &reason); + return true; +} + +static bool +gcs_block_try_r4_forward96(const ClusterICEnvelope *env, const void *payload) +{ + const ClusterR4CrForwardPayload *forward = (const ClusterR4CrForwardPayload *)payload; + ClusterSemanticAdmissionToken admission; + ClusterSemanticAdmissionResult admission_result; + uint32 master_capability_generation = 0; + uint64 master_authority_generation; + uint64 master_resource_transition_count; + SCN expected_page_scn; + bool optional_supported = false; + bool accepted = false; + + if (env == NULL || payload == NULL || env->msg_type != PGRAC_IC_MSG_GCS_BLOCK_FORWARD + || env->payload_length != sizeof(ClusterR4CrForwardPayload)) + return false; + if (!cluster_sf_peer_capability_family_sample( + (int32)env->source_node_id, R4_CR_REQUIRED_HELLO_CAPS, 0, &optional_supported, + &master_capability_generation)) + return true; + + memset(&admission, 0, sizeof(admission)); + admission_result = cluster_semantic_activation_enter( + CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, CLUSTER_SEMANTIC_TARGET_SIDE, &admission); + if (admission_result != CLUSTER_SEMANTIC_ADMISSION_OK) + return true; + + PG_TRY(); + { + if (!cluster_semantic_activation_peer_open_matches( + &admission, (int32)env->source_node_id, R4_CR_REQUIRED_HELLO_CAPS, + master_capability_generation)) + goto done; + if (env->source_node_id >= PCM_X_PROTOCOL_NODE_LIMIT + || forward->base.master_node != (int32)env->source_node_id + || forward->base.original_requester_node < 0 + || forward->base.original_requester_node >= PCM_X_PROTOCOL_NODE_LIMIT + || forward->base.requester_backend_id <= 0 + || forward->base.requester_backend_id > MaxBackends + || forward->base.request_id == 0 + || forward->base.transition_id != (uint8)PCM_TRANS_N_TO_S + || forward->base.epoch != env->epoch + || forward->base.epoch != cluster_epoch_get_current() + || cluster_gcs_lookup_master(forward->base.tag) != forward->base.master_node + || !GcsBlockForwardPayloadIsCrRequest(&forward->base) + || forward->base.reserved_0[0] != 0 || forward->base.reserved_0[1] != 0 + || forward->base.reserved_0[2] != 0 || forward->base.reserved_0[3] != 0 + || forward->base.reserved_0[4] != 1 || forward->base.reserved_0[5] != 0 + || forward->base.reserved_0[6] != 0 + || !SCN_VALID(GcsBlockForwardPayloadGetExpectedPiWatermarkScn(&forward->base)) + || !ClusterR4ForwardExtensionGetCrProof( + &forward->extension, forward->base.epoch, &master_authority_generation, + &master_resource_transition_count, &expected_page_scn)) + goto done; + + accepted = cluster_lms_cr_submit_r4(forward); + if (!cluster_semantic_activation_recheck(&admission)) + accepted = false; + +done: + (void)accepted; + } + PG_FINALLY(); + { + cluster_semantic_activation_leave(&admission); + } + PG_END_TRY(); + return true; +} + +#ifdef USE_CLUSTER_UNIT +bool +cluster_gcs_block_test_r4_request80(const ClusterICEnvelope *env, const void *payload) +{ + return gcs_block_try_r4_request80(env, payload); +} + +bool +cluster_gcs_block_test_r4_forward96(const ClusterICEnvelope *env, const void *payload) +{ + return gcs_block_try_r4_forward96(env, payload); +} + +bool +cluster_gcs_block_test_r4_refusal_status(ClusterCrBuildResult result, + ClusterCrBuildReason reason, bool admitted_forward, + GcsBlockReplyStatus *status_out) +{ + return gcs_block_r4_refusal_status_for_build(result, reason, admitted_forward, status_out); +} + +bool +cluster_gcs_block_test_decode_r4_reply( + const ClusterICEnvelope *env, const void *payload, uint64 expected_request_id, + uint64 expected_epoch, int32 expected_requester_backend_id, uint8 expected_transition_id, + int32 expected_sender_node) +{ + GcsBlockR4ReplyExpectation expected; + + memset(&expected, 0, sizeof(expected)); + expected.request_id = expected_request_id; + expected.epoch = expected_epoch; + expected.requester_backend_id = expected_requester_backend_id; + expected.transition_id = expected_transition_id; + expected.sender_node = expected_sender_node; + return gcs_block_decode_r4_reply_payload(env, payload, &expected); +} +#endif + +#undef R4_CR_REQUIRED_HELLO_CAPS + /* * cluster_gcs_block_redo_lsn_covered -- spec-4.7 D5 redo-before-unfreeze gate * (Q5, the core safety门). @@ -5938,8 +6330,9 @@ cluster_gcs_handle_block_request_envelope(const ClusterICEnvelope *env, const vo bool queue_pending_x_before = false; uint8 request_flags = 0; - (void)env; cluster_sf_dep_vec_reset(&sf_dep_vec); + if (gcs_block_try_r4_request80(env, payload)) + return; if (env == NULL || payload == NULL || env->payload_length != sizeof(GcsBlockRequestPayload)) return; @@ -7865,8 +8258,12 @@ gcs_block_decode_reply_payload(const ClusterICEnvelope *env, const void *payload return false; if (env->payload_length == v1_size) { + const GcsBlockReplyHeader *h = (const GcsBlockReplyHeader *)payload; + + if (!GcsBlockReplyStatusIsLegacy((GcsBlockReplyStatus)h->status)) + return false; if (out_hdr != NULL) - *out_hdr = (const GcsBlockReplyHeader *)payload; + *out_hdr = h; if (out_block_data != NULL) *out_block_data = ((const char *)payload) + sizeof(GcsBlockReplyHeader); return true; @@ -7899,7 +8296,9 @@ gcs_block_decode_reply_payload(const ClusterICEnvelope *env, const void *payload ClusterSfDepVec dep_vec; cluster_sf_dep_vec_reset(&dep_vec); - if (!cluster_smart_fusion || !cluster_gcs_block_reply_v2_extract_dep_vec(hdrv2, &dep_vec)) { + if (!GcsBlockReplyStatusIsLegacy((GcsBlockReplyStatus)hdrv2->v1.status) + || !cluster_smart_fusion + || !cluster_gcs_block_reply_v2_extract_dep_vec(hdrv2, &dep_vec)) { cluster_sf_dep_note_lost_failclosed(); return false; } @@ -8133,6 +8532,8 @@ cluster_gcs_handle_block_forward_envelope(const ClusterICEnvelope *env, const vo * only by the read-image branch below; inactive otherwise). */ ClusterXpScope xp_fwd_ship = { .active = false }; + if (gcs_block_try_r4_forward96(env, payload)) + return; if (env == NULL || payload == NULL || env->payload_length != sizeof(GcsBlockForwardPayload)) return; @@ -17159,6 +17560,7 @@ cluster_gcs_block_on_epoch_advance(uint64 new_epoch) int b; int j; + (void)cluster_gcs_block_dedup_r4_route_sweep_epoch(new_epoch); if (gcs_block_backend_blocks == NULL || ClusterGcsBlock == NULL) return; /* not initialized — nothing to invalidate */ diff --git a/src/backend/cluster/cluster_gcs_block_dedup.c b/src/backend/cluster/cluster_gcs_block_dedup.c index 6600794a455..42209f18b62 100644 --- a/src/backend/cluster/cluster_gcs_block_dedup.c +++ b/src/backend/cluster/cluster_gcs_block_dedup.c @@ -56,6 +56,7 @@ #include "miscadmin.h" #include "port/atomics.h" #include "port/pg_crc32c.h" +#include "portability/instr_time.h" #include "storage/bufpage.h" #include "storage/ipc.h" #include "storage/lwlock.h" @@ -142,6 +143,9 @@ static int64 dedup_expiry_threshold_us(void); static int dedup_reclaim_reclaimable_locked(ClusterGcsBlockDedupShard *shard, HTAB *htab, TimestampTz now, int want); +StaticAssertDecl(sizeof(instr_time) == sizeof(TimestampTz), + "R4 route monotonic anchor must fit the existing 8-byte TTL slot"); + static int cluster_gcs_block_dedup_effective_entries(void) @@ -575,6 +579,144 @@ dedup_pcm_x_entry_drained_valid(const GcsBlockDedupKey *key, const BufferTag *ta && dedup_pcm_x_entry_payload_valid(key, tag, entry); } +static bool +dedup_entry_kind_is_pcm_x(uint8 entry_kind) +{ + return entry_kind == GCS_BLOCK_DEDUP_ENTRY_PCM_X_RESERVED + || entry_kind == GCS_BLOCK_DEDUP_ENTRY_PCM_X_IMAGE + || entry_kind == GCS_BLOCK_DEDUP_ENTRY_PCM_X_MATERIALIZED_UNCOMMITTED + || entry_kind == GCS_BLOCK_DEDUP_ENTRY_PCM_X_DRAINED; +} + +static void +dedup_pcm_x_note_wrong_kind(ClusterGcsBlockDedupShard *shard, + const GcsBlockDedupEntry *entry) +{ + if (entry == NULL || entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE) + dedup_pcm_x_note_failclosed(shard); +} + +static bool +dedup_r4_route_proof_equal(const ClusterR4CrRouteProof *left, + const ClusterR4CrRouteProof *right) +{ + return left != NULL && right != NULL + && memcmp(&left->tag, &right->tag, sizeof(BufferTag)) == 0 + && left->read_scn == right->read_scn + && left->formation_epoch == right->formation_epoch + && left->activation_generation == right->activation_generation + && left->master_authority_generation == right->master_authority_generation + && left->master_resource_transition_count + == right->master_resource_transition_count + && left->expected_page_scn == right->expected_page_scn + && left->real_master_node == right->real_master_node + && left->selected_holder_node == right->selected_holder_node; +} + +static bool +dedup_r4_route_identity_equal(const GcsBlockDedupEntry *entry, + const GcsBlockR4RouteIdentity *identity, + uint8 transition_id) +{ + const ClusterR4CrRouteProof *stored = &entry->payload_meta.r4_route.proof; + + return entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE + && entry->transition_id == transition_id + && memcmp(&entry->tag, &identity->tag, sizeof(BufferTag)) == 0 + && stored->read_scn == identity->read_scn + && stored->activation_generation == identity->activation_generation; +} + +static bool +dedup_r4_route_input_valid(const GcsBlockR4RouteIdentity *identity, uint8 transition_id, + const ClusterR4CrRouteProof *proof) +{ + return identity != NULL && proof != NULL + && identity->legacy_key.request_id != 0 + && identity->legacy_key.requester_backend_id > 0 + && identity->legacy_key.origin_node_id < PCM_X_PROTOCOL_NODE_LIMIT + && identity->activation_generation != 0 + && SCN_VALID(identity->read_scn) + && memcmp(&identity->tag, &proof->tag, sizeof(BufferTag)) == 0 + && proof->read_scn == identity->read_scn + && proof->formation_epoch == identity->legacy_key.cluster_epoch + && proof->activation_generation == identity->activation_generation + && (uint32)proof->master_authority_generation != 0 + && (uint32)(proof->master_authority_generation >> 32) + == (uint32)proof->formation_epoch + && proof->master_resource_transition_count != 0 + && proof->master_resource_transition_count != UINT64_MAX + && proof->real_master_node >= 0 + && proof->real_master_node < PCM_X_PROTOCOL_NODE_LIMIT + && proof->selected_holder_node >= 0 + && proof->selected_holder_node < PCM_X_PROTOCOL_NODE_LIMIT; +} + +static bool +dedup_r4_route_anchor_load(const TimestampTz *slot, instr_time *anchor_out) +{ + Assert(slot != NULL); + Assert(anchor_out != NULL); + memcpy(anchor_out, slot, sizeof(*anchor_out)); + return !INSTR_TIME_IS_ZERO(*anchor_out); +} + +static void +dedup_r4_route_anchor_now(TimestampTz *slot) +{ + instr_time now; + + Assert(slot != NULL); + INSTR_TIME_SET_CURRENT(now); + memcpy(slot, &now, sizeof(now)); +} + +static bool +dedup_r4_route_reclaim_safe(const GcsBlockDedupEntry *entry, const instr_time *now, + int64 fallback_out_of_window_us) +{ + const GcsBlockR4RouteRecord *record = &entry->payload_meta.r4_route; + const TimestampTz *anchor_slot; + instr_time anchor; + instr_time elapsed; + int64 deadline_us; + + if (entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE) + return false; + if (record->state == GCS_BLOCK_R4_ROUTE_ROUTING) + anchor_slot = &entry->registered_at_ts; + else if (record->state == GCS_BLOCK_R4_ROUTE_FORWARDED + || record->state == GCS_BLOCK_R4_ROUTE_RETRYABLE) + anchor_slot = &entry->completed_at_ts; + else + return false; + if (now == NULL || !dedup_r4_route_anchor_load(anchor_slot, &anchor)) + return false; + + deadline_us = entry->pinned_lifetime_us > 0 ? entry->pinned_lifetime_us + : fallback_out_of_window_us; + elapsed = *now; + INSTR_TIME_SUBTRACT(elapsed, anchor); + if (INSTR_TIME_GET_NANOSEC(elapsed) < 0) + return false; + return deadline_us > 0 + && INSTR_TIME_GET_MICROSEC(elapsed) > (uint64)deadline_us; +} + +static bool +dedup_entry_reclaim_safe(const GcsBlockDedupEntry *entry, TimestampTz wall_now, + const instr_time *route_now, + int64 fallback_out_of_window_us) +{ + if (entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_GENERIC) + return GcsBlockDedupEntryIsReclaimSafe(entry, wall_now, + fallback_out_of_window_us); + if (entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE) + return dedup_r4_route_reclaim_safe(entry, route_now, + fallback_out_of_window_us); + return false; +} + /* ============================================================ * Public API. @@ -611,7 +753,7 @@ cluster_gcs_block_dedup_lookup_or_register(int worker_id, const GcsBlockDedupKey if (found) { if (entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_GENERIC) { - dedup_pcm_x_note_failclosed(shard); + dedup_pcm_x_note_wrong_kind(shard, entry); LWLockRelease(&shard->lock.lock); return GCS_BLOCK_DEDUP_VALIDATION_FAIL; } @@ -743,6 +885,228 @@ cluster_gcs_block_dedup_lookup_or_register(int worker_id, const GcsBlockDedupKey return result; } +GcsBlockR4RouteArmResult +cluster_gcs_block_dedup_r4_route_arm_or_match( + int worker_id, const GcsBlockR4RouteIdentity *identity, uint8 transition_id, + const ClusterR4CrRouteProof *fresh_proof, uint32 requester_lifetime_hint_ms, + bool lifetime_hint_trusted, GcsBlockR4RouteRecord *record_out) +{ + ClusterGcsBlockDedupShard *shard; + HTAB *htab = NULL; + GcsBlockDedupEntry *entry; + bool found = false; + int64 pinned_lifetime_ms; + GcsBlockR4RouteArmResult result; + + if (record_out != NULL) + memset(record_out, 0, sizeof(*record_out)); + if (record_out == NULL + || !dedup_r4_route_input_valid(identity, transition_id, fresh_proof)) + return GCS_BLOCK_R4_ROUTE_ARM_INVALID; + if (lifetime_hint_trusted + && (requester_lifetime_hint_ms == 0 + || (int64)requester_lifetime_hint_ms > GCS_BLOCK_DEDUP_MAX_PROTOCOL_LIFETIME_MS)) + return GCS_BLOCK_R4_ROUTE_ARM_INVALID; + + shard = cluster_gcs_block_dedup_resolve_shard(worker_id, &htab); + if (shard == NULL) + return GCS_BLOCK_R4_ROUTE_ARM_FULL; + + LWLockAcquire(&shard->lock.lock, LW_EXCLUSIVE); + entry = (GcsBlockDedupEntry *)hash_search(htab, &identity->legacy_key, HASH_FIND, &found); + if (found) { + if (entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE + || !dedup_r4_route_identity_equal(entry, identity, transition_id)) { + result = GCS_BLOCK_R4_ROUTE_ARM_IDENTITY_COLLISION; + goto out; + } + *record_out = entry->payload_meta.r4_route; + if (!dedup_r4_route_proof_equal(&record_out->proof, fresh_proof)) { + if (entry->payload_meta.r4_route.state != GCS_BLOCK_R4_ROUTE_RETRYABLE) { + entry->payload_meta.r4_route.state = GCS_BLOCK_R4_ROUTE_RETRYABLE; + dedup_r4_route_anchor_now(&entry->completed_at_ts); + } + *record_out = entry->payload_meta.r4_route; + result = GCS_BLOCK_R4_ROUTE_ARM_HOLDER_MOVED; + goto out; + } + if (record_out->state == GCS_BLOCK_R4_ROUTE_RETRYABLE) + result = GCS_BLOCK_R4_ROUTE_ARM_RETRYABLE; + else if (record_out->state == GCS_BLOCK_R4_ROUTE_ROUTING + || record_out->state == GCS_BLOCK_R4_ROUTE_FORWARDED) + result = GCS_BLOCK_R4_ROUTE_ARM_REPLAY; + else + result = GCS_BLOCK_R4_ROUTE_ARM_INVALID; + goto out; + } + + if (transition_id != (uint8)PCM_TRANS_N_TO_S) { + result = GCS_BLOCK_R4_ROUTE_ARM_INVALID; + goto out; + } + entry = (GcsBlockDedupEntry *)hash_search(htab, &identity->legacy_key, HASH_ENTER_NULL, + &found); + if (entry == NULL + && dedup_reclaim_reclaimable_locked(shard, htab, GetCurrentTimestamp(), 1) > 0) + entry = (GcsBlockDedupEntry *)hash_search(htab, &identity->legacy_key, + HASH_ENTER_NULL, &found); + if (entry == NULL) { + result = GCS_BLOCK_R4_ROUTE_ARM_FULL; + goto out; + } + memset(((char *)entry) + sizeof(GcsBlockDedupKey), 0, + sizeof(GcsBlockDedupEntry) - sizeof(GcsBlockDedupKey)); + entry->tag = identity->tag; + entry->transition_id = transition_id; + entry->entry_kind = GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE; + entry->payload_meta.r4_route.proof = *fresh_proof; + entry->payload_meta.r4_route.state = GCS_BLOCK_R4_ROUTE_ROUTING; + dedup_r4_route_anchor_now(&entry->registered_at_ts); + pinned_lifetime_ms = lifetime_hint_trusted ? (int64)requester_lifetime_hint_ms + : GCS_BLOCK_DEDUP_MAX_PROTOCOL_LIFETIME_MS; + entry->pinned_lifetime_us = pinned_lifetime_ms * 1000 * 2; + pg_atomic_fetch_add_u32(&shard->entry_count, 1); + *record_out = entry->payload_meta.r4_route; + result = GCS_BLOCK_R4_ROUTE_ARM_NEW; + +out: + LWLockRelease(&shard->lock.lock); + return result; +} + +GcsBlockR4RouteSendResult +cluster_gcs_block_dedup_r4_route_finish_send( + int worker_id, const GcsBlockR4RouteIdentity *identity, uint8 transition_id, + const ClusterR4CrRouteProof *armed_proof, bool outbound_admitted) +{ + ClusterGcsBlockDedupShard *shard; + HTAB *htab = NULL; + GcsBlockDedupEntry *entry; + bool found = false; + GcsBlockR4RouteSendResult result; + + if (!dedup_r4_route_input_valid(identity, transition_id, armed_proof)) + return GCS_BLOCK_R4_ROUTE_SEND_INVALID; + shard = cluster_gcs_block_dedup_resolve_shard(worker_id, &htab); + if (shard == NULL) + return GCS_BLOCK_R4_ROUTE_SEND_STALE; + + LWLockAcquire(&shard->lock.lock, LW_EXCLUSIVE); + entry = (GcsBlockDedupEntry *)hash_search(htab, &identity->legacy_key, HASH_FIND, &found); + if (!found) { + result = GCS_BLOCK_R4_ROUTE_SEND_STALE; + goto out; + } + if (!dedup_r4_route_identity_equal(entry, identity, transition_id) + || !dedup_r4_route_proof_equal(&entry->payload_meta.r4_route.proof, armed_proof)) { + result = GCS_BLOCK_R4_ROUTE_SEND_COLLISION; + goto out; + } + + if (outbound_admitted) { + if (entry->payload_meta.r4_route.state != GCS_BLOCK_R4_ROUTE_FORWARDED) { + entry->payload_meta.r4_route.state = GCS_BLOCK_R4_ROUTE_FORWARDED; + dedup_r4_route_anchor_now(&entry->completed_at_ts); + } + result = GCS_BLOCK_R4_ROUTE_SEND_FORWARDED; + } else { + if (entry->payload_meta.r4_route.state != GCS_BLOCK_R4_ROUTE_FORWARDED) { + entry->payload_meta.r4_route.state = GCS_BLOCK_R4_ROUTE_RETRYABLE; + dedup_r4_route_anchor_now(&entry->completed_at_ts); + } + result = GCS_BLOCK_R4_ROUTE_SEND_RETRYABLE; + } + +out: + LWLockRelease(&shard->lock.lock); + return result; +} + +static uint64 +dedup_r4_route_remove_if(bool (*predicate)(const GcsBlockDedupEntry *, uint64), uint64 arg) +{ + uint64 removed_total = 0; + int s; + + if (cluster_gcs_block_dedup_shards == NULL) + return 0; + for (s = 0; s < cluster_gcs_block_dedup_n_shards; s++) { + ClusterGcsBlockDedupShard *shard = &cluster_gcs_block_dedup_shards[s]; + HTAB *htab = cluster_gcs_block_dedup_htabs[s]; + HASH_SEQ_STATUS scan; + GcsBlockDedupEntry *entry; + uint32 removed = 0; + + if (htab == NULL) + continue; + LWLockAcquire(&shard->lock.lock, LW_EXCLUSIVE); + hash_seq_init(&scan, htab); + while ((entry = (GcsBlockDedupEntry *)hash_seq_search(&scan)) != NULL) { + if (entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE + || !predicate(entry, arg)) + continue; + (void)hash_search(htab, &entry->key, HASH_REMOVE, NULL); + removed++; + } + if (removed > 0) + pg_atomic_fetch_sub_u32(&shard->entry_count, removed); + LWLockRelease(&shard->lock.lock); + removed_total += removed; + } + return removed_total; +} + +static bool +dedup_r4_route_stale_epoch(const GcsBlockDedupEntry *entry, uint64 current_epoch) +{ + return entry->payload_meta.r4_route.proof.formation_epoch != current_epoch; +} + +static bool +dedup_r4_route_any(const GcsBlockDedupEntry *entry pg_attribute_unused(), + uint64 arg pg_attribute_unused()) +{ + return true; +} + +uint64 +cluster_gcs_block_dedup_r4_route_sweep_epoch(uint64 current_epoch) +{ + return dedup_r4_route_remove_if(dedup_r4_route_stale_epoch, current_epoch); +} + +uint64 +cluster_gcs_block_dedup_r4_route_count(void) +{ + uint64 total = 0; + int s; + + if (cluster_gcs_block_dedup_shards == NULL) + return 0; + for (s = 0; s < cluster_gcs_block_dedup_n_shards; s++) { + ClusterGcsBlockDedupShard *shard = &cluster_gcs_block_dedup_shards[s]; + HTAB *htab = cluster_gcs_block_dedup_htabs[s]; + HASH_SEQ_STATUS scan; + GcsBlockDedupEntry *entry; + + if (htab == NULL) + continue; + LWLockAcquire(&shard->lock.lock, LW_SHARED); + hash_seq_init(&scan, htab); + while ((entry = (GcsBlockDedupEntry *)hash_seq_search(&scan)) != NULL) + if (entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE) + total++; + LWLockRelease(&shard->lock.lock); + } + return total; +} + +uint64 +cluster_gcs_block_dedup_r4_route_purge_closed(void) +{ + return dedup_r4_route_remove_if(dedup_r4_route_any, 0); +} + static bool dedup_pending_x_denial_is_exact(const GcsBlockDedupEntry *entry) { @@ -889,7 +1253,7 @@ cluster_gcs_block_dedup_pending_x_deny_exact(int worker_id, const GcsBlockDedupK if (!found || entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_GENERIC || memcmp(&entry->tag, tag, sizeof(*tag)) != 0 || entry->transition_id != transition_id) { if (found && entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_GENERIC) - dedup_pcm_x_note_failclosed(shard); + dedup_pcm_x_note_wrong_kind(shard, entry); LWLockRelease(&shard->lock.lock); return GCS_BLOCK_PENDING_X_DENY_INVALID; } @@ -938,7 +1302,7 @@ cluster_gcs_block_dedup_set_request_flags_exact(int worker_id, const GcsBlockDed entry->request_flags = pinned_flags; updated = entry->request_flags == pinned_flags; } else if (found && entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_GENERIC) - dedup_pcm_x_note_failclosed(shard); + dedup_pcm_x_note_wrong_kind(shard, entry); LWLockRelease(&shard->lock.lock); return updated; } @@ -964,6 +1328,11 @@ cluster_gcs_block_dedup_pcm_x_reserve(int worker_id, const GcsBlockDedupKey *key LWLockAcquire(&shard->lock.lock, LW_EXCLUSIVE); entry = (GcsBlockDedupEntry *)hash_search(htab, key, HASH_FIND, &found); if (found) { + if (!dedup_entry_kind_is_pcm_x(entry->entry_kind)) { + dedup_pcm_x_note_wrong_kind(shard, entry); + LWLockRelease(&shard->lock.lock); + return GCS_BLOCK_PCM_X_IMAGE_STALE; + } if (entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_PCM_X_DRAINED && !dedup_pcm_x_entry_drained_valid(key, tag, entry)) { dedup_pcm_x_note_failclosed(shard); @@ -1042,6 +1411,11 @@ cluster_gcs_block_dedup_pcm_x_materialize(int worker_id, const GcsBlockDedupKey LWLockRelease(&shard->lock.lock); return GCS_BLOCK_PCM_X_IMAGE_NOT_FOUND; } + if (!dedup_entry_kind_is_pcm_x(entry->entry_kind)) { + dedup_pcm_x_note_wrong_kind(shard, entry); + LWLockRelease(&shard->lock.lock); + return GCS_BLOCK_PCM_X_IMAGE_STALE; + } if (entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_PCM_X_IMAGE || entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_PCM_X_MATERIALIZED_UNCOMMITTED) { GcsBlockPcmXImageBinding stored_binding; @@ -1132,6 +1506,11 @@ cluster_gcs_block_dedup_pcm_x_publish_ready_exact(int worker_id, const GcsBlockD LWLockRelease(&shard->lock.lock); return GCS_BLOCK_PCM_X_IMAGE_NOT_FOUND; } + if (!dedup_entry_kind_is_pcm_x(entry->entry_kind)) { + dedup_pcm_x_note_wrong_kind(shard, entry); + LWLockRelease(&shard->lock.lock); + return GCS_BLOCK_PCM_X_IMAGE_STALE; + } dedup_pcm_x_binding_from_entry(entry, &stored_binding); if (entry->transition_id != (uint8)PCM_TRANS_N_TO_S || memcmp(&entry->tag, tag, sizeof(*tag)) != 0 @@ -1192,6 +1571,11 @@ cluster_gcs_block_dedup_pcm_x_lookup(int worker_id, const GcsBlockDedupKey *key, LWLockRelease(&shard->lock.lock); return GCS_BLOCK_PCM_X_IMAGE_NOT_FOUND; } + if (!dedup_entry_kind_is_pcm_x(entry->entry_kind)) { + dedup_pcm_x_note_wrong_kind(shard, entry); + LWLockRelease(&shard->lock.lock); + return GCS_BLOCK_PCM_X_IMAGE_STALE; + } if (entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_PCM_X_DRAINED) { dedup_pcm_x_binding_from_entry(entry, &stored_binding); if (entry->transition_id != (uint8)PCM_TRANS_N_TO_S @@ -1273,6 +1657,11 @@ cluster_gcs_block_dedup_pcm_x_drain_status_exact(int worker_id, const GcsBlockDe LWLockRelease(&shard->lock.lock); return GCS_BLOCK_PCM_X_IMAGE_NOT_FOUND; } + if (!dedup_entry_kind_is_pcm_x(entry->entry_kind)) { + dedup_pcm_x_note_wrong_kind(shard, entry); + LWLockRelease(&shard->lock.lock); + return GCS_BLOCK_PCM_X_IMAGE_STALE; + } dedup_pcm_x_binding_from_entry(entry, &stored_binding); if (entry->transition_id != (uint8)PCM_TRANS_N_TO_S || memcmp(&entry->tag, tag, sizeof(*tag)) != 0 @@ -1330,6 +1719,11 @@ cluster_gcs_block_dedup_pcm_x_release_exact(int worker_id, const GcsBlockDedupKe LWLockRelease(&shard->lock.lock); return GCS_BLOCK_PCM_X_IMAGE_NOT_FOUND; } + if (!dedup_entry_kind_is_pcm_x(entry->entry_kind)) { + dedup_pcm_x_note_wrong_kind(shard, entry); + LWLockRelease(&shard->lock.lock); + return GCS_BLOCK_PCM_X_IMAGE_STALE; + } dedup_pcm_x_binding_from_entry(entry, &stored_binding); if (entry->transition_id != (uint8)PCM_TRANS_N_TO_S || memcmp(&entry->tag, tag, sizeof(*tag)) != 0 @@ -1471,6 +1865,11 @@ cluster_gcs_block_dedup_pcm_x_preserve_finish_error_exact( LWLockRelease(&shard->lock.lock); return GCS_BLOCK_PCM_X_IMAGE_NOT_FOUND; } + if (!dedup_entry_kind_is_pcm_x(entry->entry_kind)) { + dedup_pcm_x_note_wrong_kind(shard, entry); + LWLockRelease(&shard->lock.lock); + return GCS_BLOCK_PCM_X_IMAGE_STALE; + } dedup_pcm_x_binding_from_entry(entry, &stored_binding); if (entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_PCM_X_MATERIALIZED_UNCOMMITTED || entry->transition_id != (uint8)PCM_TRANS_N_TO_S @@ -1665,6 +2064,11 @@ cluster_gcs_block_dedup_pcm_x_mark_staged_exact(int worker_id, const GcsBlockDed LWLockRelease(&shard->lock.lock); return GCS_BLOCK_PCM_X_IMAGE_NOT_FOUND; } + if (!dedup_entry_kind_is_pcm_x(entry->entry_kind)) { + dedup_pcm_x_note_wrong_kind(shard, entry); + LWLockRelease(&shard->lock.lock); + return GCS_BLOCK_PCM_X_IMAGE_STALE; + } dedup_pcm_x_binding_from_entry(entry, &stored_binding); if (entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_PCM_X_IMAGE || entry->transition_id != (uint8)PCM_TRANS_N_TO_S @@ -1714,6 +2118,11 @@ cluster_gcs_block_dedup_pcm_x_unmark_staged_exact(int worker_id, const GcsBlockD LWLockRelease(&shard->lock.lock); return GCS_BLOCK_PCM_X_IMAGE_NOT_FOUND; } + if (!dedup_entry_kind_is_pcm_x(entry->entry_kind)) { + dedup_pcm_x_note_wrong_kind(shard, entry); + LWLockRelease(&shard->lock.lock); + return GCS_BLOCK_PCM_X_IMAGE_STALE; + } dedup_pcm_x_binding_from_entry(entry, &stored_binding); if (entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_PCM_X_IMAGE || entry->transition_id != (uint8)PCM_TRANS_N_TO_S @@ -1762,6 +2171,11 @@ cluster_gcs_block_dedup_pcm_x_rearm_exact(int worker_id, const GcsBlockDedupKey LWLockRelease(&shard->lock.lock); return GCS_BLOCK_PCM_X_IMAGE_NOT_FOUND; } + if (!dedup_entry_kind_is_pcm_x(entry->entry_kind)) { + dedup_pcm_x_note_wrong_kind(shard, entry); + LWLockRelease(&shard->lock.lock); + return GCS_BLOCK_PCM_X_IMAGE_STALE; + } if ((entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_PCM_X_RESERVED && entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_PCM_X_IMAGE && entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_PCM_X_DRAINED) @@ -1822,7 +2236,7 @@ cluster_gcs_block_dedup_pcm_x_restart_audit(int worker_id) LWLockAcquire(&shard->lock.lock, LW_SHARED); hash_seq_init(&scan, htab); while ((entry = (GcsBlockDedupEntry *)hash_seq_search(&scan)) != NULL) { - if (entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_GENERIC) { + if (dedup_entry_kind_is_pcm_x(entry->entry_kind)) { evidence_found = true; hash_seq_term(&scan); break; @@ -1866,9 +2280,9 @@ cluster_gcs_block_dedup_mark_done(int worker_id, const GcsBlockDedupKey *key, co entry->done_at_ts = GetCurrentTimestamp(); stamped = true; /* duplicate DONE re-stamps nothing: idempotent */ pg_atomic_fetch_add_u64(&shard->done_marked_count, 1); - } else { + } else if (!found || entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE) { if (found && entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_GENERIC) - dedup_pcm_x_note_failclosed(shard); + dedup_pcm_x_note_wrong_kind(shard, entry); pg_atomic_fetch_add_u64(&shard->done_mismatch_count, 1); } LWLockRelease(&shard->lock.lock); @@ -1949,7 +2363,7 @@ cluster_gcs_block_dedup_install_reply_ex(int worker_id, const GcsBlockDedupKey * return; } if (entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_GENERIC) { - dedup_pcm_x_note_failclosed(shard); + dedup_pcm_x_note_wrong_kind(shard, entry); LWLockRelease(&shard->lock.lock); return; } @@ -2021,7 +2435,7 @@ cluster_gcs_block_dedup_remove(int worker_id, const GcsBlockDedupKey *key) Assert(found); pg_atomic_fetch_sub_u32(&shard->entry_count, 1); } else if (found) - dedup_pcm_x_note_failclosed(shard); + dedup_pcm_x_note_wrong_kind(shard, entry); LWLockRelease(&shard->lock.lock); } @@ -2095,18 +2509,23 @@ dedup_reclaim_reclaimable_locked(ClusterGcsBlockDedupShard *shard, HTAB *htab, T { HASH_SEQ_STATUS seq; GcsBlockDedupEntry *entry; + instr_time route_now; int64 out_of_window_us; int reclaimed = 0; + int generic_reclaimed = 0; int probed = 0; if (want <= 0) return 0; out_of_window_us = dedup_expiry_threshold_us(); + INSTR_TIME_SET_CURRENT(route_now); hash_seq_init(&seq, htab); while ((entry = (GcsBlockDedupEntry *)hash_seq_search(&seq)) != NULL) { - if (GcsBlockDedupEntryIsReclaimSafe(entry, now, out_of_window_us)) { + if (dedup_entry_reclaim_safe(entry, now, &route_now, out_of_window_us)) { + if (entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_GENERIC) + generic_reclaimed++; (void)hash_search(htab, &entry->key, HASH_REMOVE, NULL); reclaimed++; } @@ -2119,7 +2538,8 @@ dedup_reclaim_reclaimable_locked(ClusterGcsBlockDedupShard *shard, HTAB *htab, T if (reclaimed > 0) { pg_atomic_fetch_sub_u32(&shard->entry_count, (uint32)reclaimed); - pg_atomic_fetch_add_u64(&shard->evict_count, (uint64)reclaimed); + if (generic_reclaimed > 0) + pg_atomic_fetch_add_u64(&shard->evict_count, (uint64)generic_reclaimed); } return reclaimed; } @@ -2127,6 +2547,7 @@ dedup_reclaim_reclaimable_locked(ClusterGcsBlockDedupShard *shard, HTAB *htab, T void cluster_gcs_block_dedup_sweep_expired(TimestampTz now) { + instr_time route_now; int64 threshold_us; int s; @@ -2134,6 +2555,7 @@ cluster_gcs_block_dedup_sweep_expired(TimestampTz now) return; threshold_us = dedup_expiry_threshold_us(); + INSTR_TIME_SET_CURRENT(route_now); /* * spec-7.2a D5: saturation LOG-once. When DENIED_DEDUP_FULL keeps @@ -2167,6 +2589,7 @@ cluster_gcs_block_dedup_sweep_expired(TimestampTz now) HASH_SEQ_STATUS seq; GcsBlockDedupEntry *entry; int removed = 0; + int generic_removed = 0; if (htab == NULL) continue; @@ -2179,8 +2602,16 @@ cluster_gcs_block_dedup_sweep_expired(TimestampTz now) int64 age_us; int64 deadline_us; - if (entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_GENERIC) + if (entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_GENERIC + && entry->entry_kind != GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE) + continue; + if (entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE) { + if (!dedup_r4_route_reclaim_safe(entry, &route_now, threshold_us)) + continue; + (void)hash_search(htab, &entry->key, HASH_REMOVE, NULL); + removed++; continue; + } /* * GCS-race round-2 RC-F: per-entry pinned deadlines. A @@ -2208,13 +2639,15 @@ cluster_gcs_block_dedup_sweep_expired(TimestampTz now) if (age_us > deadline_us) { (void)hash_search(htab, &entry->key, HASH_REMOVE, NULL); removed++; + generic_removed++; } } if (removed > 0) { pg_atomic_fetch_sub_u32(&shard->entry_count, (uint32)removed); /* spec-7.2a D5: evict_count aggregates eager reclaim + TTL sweep. */ - pg_atomic_fetch_add_u64(&shard->evict_count, (uint64)removed); + if (generic_removed > 0) + pg_atomic_fetch_add_u64(&shard->evict_count, (uint64)generic_removed); } LWLockRelease(&shard->lock.lock); @@ -2242,7 +2675,8 @@ cluster_gcs_block_dedup_cleanup_on_backend_exit(uint32 origin_node_id, int32 bac LWLockAcquire(&shard->lock.lock, LW_EXCLUSIVE); hash_seq_init(&seq, htab); while ((entry = (GcsBlockDedupEntry *)hash_seq_search(&seq)) != NULL) { - if (entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_GENERIC + if ((entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_GENERIC + || entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE) && entry->key.origin_node_id == origin_node_id && entry->key.requester_backend_id == backend_id) { (void)hash_search(htab, &entry->key, HASH_REMOVE, NULL); @@ -2276,7 +2710,8 @@ cluster_gcs_block_dedup_cleanup_on_node_dead(uint32 node_id) LWLockAcquire(&shard->lock.lock, LW_EXCLUSIVE); hash_seq_init(&seq, htab); while ((entry = (GcsBlockDedupEntry *)hash_seq_search(&seq)) != NULL) { - if (entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_GENERIC + if ((entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_GENERIC + || entry->entry_kind == GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE) && entry->key.origin_node_id == node_id) { (void)hash_search(htab, &entry->key, HASH_REMOVE, NULL); removed++; diff --git a/src/backend/cluster/cluster_gcs_block_shard.c b/src/backend/cluster/cluster_gcs_block_shard.c index e957e8df4cb..77e1a60c0b8 100644 --- a/src/backend/cluster/cluster_gcs_block_shard.c +++ b/src/backend/cluster/cluster_gcs_block_shard.c @@ -87,12 +87,14 @@ cluster_gcs_block_payload_shard(uint8 msg_type, const void *payload, uint16 payl switch (msg_type) { case PGRAC_IC_MSG_GCS_BLOCK_REQUEST: - if (payload_len != sizeof(GcsBlockRequestPayload)) + if (payload_len != sizeof(GcsBlockRequestPayload) + && payload_len != sizeof(ClusterR4CrRequestPayload)) return -1; tag = &((const GcsBlockRequestPayload *)payload)->tag; break; case PGRAC_IC_MSG_GCS_BLOCK_FORWARD: - if (payload_len != sizeof(GcsBlockForwardPayload)) + if (payload_len != sizeof(GcsBlockForwardPayload) + && payload_len != sizeof(ClusterR4CrForwardPayload)) return -1; tag = &((const GcsBlockForwardPayload *)payload)->tag; break; diff --git a/src/backend/cluster/cluster_lms_outbound.c b/src/backend/cluster/cluster_lms_outbound.c index a6bff9ad085..e168e5453d0 100644 --- a/src/backend/cluster/cluster_lms_outbound.c +++ b/src/backend/cluster/cluster_lms_outbound.c @@ -266,24 +266,46 @@ cluster_lms_outbound_enqueue_cap_bound(int worker_id, uint8 msg_type, uint32 des required_capability, connection_generation); } -/* - * Stage a header-only GCS denial from a CONTROL-plane producer. The DATA - * owner expands the ABI-mandated zero block immediately before transport - * admission. Keep this surface narrow: only the Shape-B pending-X denial - * may use it, and callers must choose worker[shard(tag)] so it stays on the - * same per-tag stream as the request it terminates. - */ -bool -cluster_lms_outbound_enqueue_zero_block_reply(int worker_id, uint32 dest_node_id, - const GcsBlockReplyHeader *header, bool direct_land) +static bool +lms_outbound_r4_refusal_header_valid(const GcsBlockReplyHeader *header) +{ + int i; + + if (header == NULL || !GcsBlockReplyStatusIsR4Refusal((GcsBlockReplyStatus)header->status) + || header->request_id == 0 || header->checksum != 0 || header->sender_node < 0 + || header->sender_node >= CLUSTER_MAX_NODES || header->requester_backend_id <= 0 + || header->transition_id != (uint8)PCM_TRANS_N_TO_S + || GcsBlockReplyHeaderGetForwardingMasterNode(header) + != GCS_BLOCK_REPLY_NO_FORWARDING_MASTER) + return false; + for (i = 0; i < (int)sizeof(header->reserved_0); i++) + if (header->reserved_0[i] != 0) + return false; + if (header->status == (uint8)GCS_BLOCK_REPLY_R4_DENIED) + return header->page_lsn == 0; + /* Status 25 optionally carries WRONG_MASTER as node+1. The encoded + * value is therefore either zero or in [1, CLUSTER_MAX_NODES]. */ + return header->page_lsn <= (uint64)CLUSTER_MAX_NODES; +} + +static bool +lms_outbound_enqueue_zero_block_reply_internal(int worker_id, uint32 dest_node_id, + const GcsBlockReplyHeader *header, + bool direct_land, uint32 required_capability, + uint32 connection_generation) { ClusterLmsOutboundState *ring; LWLock *lock; ClusterLmsOutboundSlot *slot; + bool r4_cap_bound = required_capability != 0; if (worker_id < 0 || worker_id >= CLUSTER_LMS_MAX_WORKERS || dest_node_id >= CLUSTER_MAX_NODES - || header == NULL || (direct_land && (int32)dest_node_id == cluster_node_id) - || header->status != (uint8)GCS_BLOCK_REPLY_DENIED_PENDING_X) + || header == NULL || (direct_land && (int32)dest_node_id == cluster_node_id)) + return false; + if (r4_cap_bound) { + if (direct_land || !lms_outbound_r4_refusal_header_valid(header)) + return false; + } else if (header->status != (uint8)GCS_BLOCK_REPLY_DENIED_PENDING_X) return false; if (cluster_lms_outbound_rings == NULL || OB_LOCK(worker_id) == NULL) return false; @@ -301,8 +323,8 @@ cluster_lms_outbound_enqueue_zero_block_reply(int worker_id, uint32 dest_node_id slot->kind = (uint8)(direct_land ? CLUSTER_LMS_OUTBOUND_DIRECT_ZERO_BLOCK_REPLY : CLUSTER_LMS_OUTBOUND_ZERO_BLOCK_REPLY); slot->payload_len = sizeof(*header); - slot->required_capability = 0; - slot->connection_generation = 0; + slot->required_capability = required_capability; + slot->connection_generation = connection_generation; memcpy(slot->payload, header, sizeof(*header)); ring->head = (ring->head + 1) % PGRAC_LMS_OUTBOUND_CAPACITY; ring->count++; @@ -312,6 +334,29 @@ cluster_lms_outbound_enqueue_zero_block_reply(int worker_id, uint32 dest_node_id return true; } +/* Stage the legacy Shape-B pending-X denial. */ +bool +cluster_lms_outbound_enqueue_zero_block_reply(int worker_id, uint32 dest_node_id, + const GcsBlockReplyHeader *header, bool direct_land) +{ + return lms_outbound_enqueue_zero_block_reply_internal( + worker_id, dest_node_id, header, direct_land, 0, 0); +} + +/* PGRAC R4 adaptation: stage a typed 25/26 refusal on the requester's exact + * HELLO-authenticated connection. The existing DATA slot remains 144 bytes; + * only the 48-byte header is retained until drain expands the zero page. */ +bool +cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + int worker_id, uint32 dest_node_id, const GcsBlockReplyHeader *header, + uint32 required_capability, uint32 connection_generation) +{ + if (required_capability == 0) + return false; + return lms_outbound_enqueue_zero_block_reply_internal( + worker_id, dest_node_id, header, false, required_capability, connection_generation); +} + /* * cluster_lms_outbound_drain_send — one worker drains + sends its own ring. * @@ -405,7 +450,13 @@ cluster_lms_outbound_drain_send(int worker_id) } memset(&zero_reply, 0, sizeof(zero_reply)); memcpy(&zero_reply.header, slot.payload, sizeof(zero_reply.header)); - if (zero_reply.header.status != (uint8)GCS_BLOCK_REPLY_DENIED_PENDING_X) { + if (slot.required_capability == 0) { + if (zero_reply.header.status != (uint8)GCS_BLOCK_REPLY_DENIED_PENDING_X) { + rc = CLUSTER_IC_SEND_HARD_ERROR; + goto handle_send_result; + } + } else if (slot.kind == (uint8)CLUSTER_LMS_OUTBOUND_DIRECT_ZERO_BLOCK_REPLY + || !lms_outbound_r4_refusal_header_valid(&zero_reply.header)) { rc = CLUSTER_IC_SEND_HARD_ERROR; goto handle_send_result; } diff --git a/src/backend/cluster/cluster_semantic_activation.c b/src/backend/cluster/cluster_semantic_activation.c index 936e9d4049a..2c1a62398e4 100644 --- a/src/backend/cluster/cluster_semantic_activation.c +++ b/src/backend/cluster/cluster_semantic_activation.c @@ -17,8 +17,10 @@ #include "postgres.h" #include "miscadmin.h" +#include "cluster/cluster_conf.h" #include "cluster/cluster_epoch.h" #include "cluster/cluster_semantic_activation.h" +#include "cluster/cluster_sf_dep.h" #include "cluster/storage/cluster_undo_block0.h" #include "port/atomics.h" #include "port/pg_crc32c.h" @@ -754,6 +756,26 @@ cluster_semantic_activation_recheck(const ClusterSemanticAdmissionToken *token) == CLUSTER_SEMANTIC_ADMISSION_OK; } +bool +cluster_semantic_activation_peer_open_matches( + const ClusterSemanticAdmissionToken *token, int32 authenticated_peer_node_id, + uint32 required_hello_caps, uint32 sampled_capability_generation) +{ + if (token == NULL || !token->entered + || token->feature_bit != CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1 + || token->side != CLUSTER_SEMANTIC_TARGET_SIDE || authenticated_peer_node_id < 0 + || authenticated_peer_node_id >= CLUSTER_MAX_NODES || required_hello_caps == 0) + return false; + if (!cluster_semantic_activation_recheck(token)) + return false; + if (!cluster_sf_peer_capability_generation_matches( + authenticated_peer_node_id, required_hello_caps, sampled_capability_generation)) + return false; + + /* D13 owns positive results after installing its frozen ACK table. */ + return false; +} + void cluster_semantic_activation_leave(ClusterSemanticAdmissionToken *token) { diff --git a/src/backend/cluster/cluster_sf_dep.c b/src/backend/cluster/cluster_sf_dep.c index f1193388f36..1578962a233 100644 --- a/src/backend/cluster/cluster_sf_dep.c +++ b/src/backend/cluster/cluster_sf_dep.c @@ -426,6 +426,29 @@ cluster_sf_peer_pcm_x_source_floor_sample(int32 peer_id, bool *source_floor_out, return supported; } +/* Generic, record-coherent required/optional capability sample. */ +bool +cluster_sf_peer_capability_family_sample(int32 peer_id, uint32 required_capabilities, + uint32 optional_capabilities, + bool *optional_supported_out, uint32 *generation_out) +{ + bool supported; + + if (optional_supported_out != NULL) + *optional_supported_out = false; + if (generation_out != NULL) + *generation_out = 0; + if (ClusterSfDep == NULL || peer_id < 0 || peer_id >= CLUSTER_MAX_NODES) + return false; + + LWLockAcquire(&ClusterSfDep->lock, LW_SHARED); + supported = cluster_sf_peer_cap_family_sample( + &ClusterSfDep->peer_capabilities[peer_id], required_capabilities, optional_capabilities, + optional_supported_out, generation_out); + LWLockRelease(&ClusterSfDep->lock); + return supported; +} + /* Drain-side exact fence for a capability-bound LMS slot. */ bool cluster_sf_peer_capability_generation_matches(int32 peer_id, uint32 required_capabilities, diff --git a/src/include/cluster/cluster_cr_server.h b/src/include/cluster/cluster_cr_server.h index 89e78c2c4e0..21c433068ae 100644 --- a/src/include/cluster/cluster_cr_server.h +++ b/src/include/cluster/cluster_cr_server.h @@ -216,6 +216,14 @@ extern void cluster_cr_server_publish_lms_latch(struct Latch *latch); * data plane off (caller replies the fail-closed DENIED immediately). */ extern bool cluster_lms_cr_submit(const GcsBlockForwardPayload *fwd); +/* + * R4 FORWARD96 holder-submit boundary. D3 supplies the typed handoff only; + * D4 owns every positive stable-copy/slot submission path. Until that D4 + * integration exists, this boundary refuses without narrowing FORWARD96 to + * the legacy 64-byte submit ABI. + */ +extern bool cluster_lms_cr_submit_r4(const ClusterR4CrForwardPayload *forward); + /* LMON dispatch side (spec-6.12i D-i1): park a validated undo-TT fetch * request; false = wave GUC off on this node / malformed synthetic tag / no * capacity (caller replies the fail-closed DENIED immediately — the diff --git a/src/include/cluster/cluster_gcs_block.h b/src/include/cluster/cluster_gcs_block.h index 14ad9e71f2b..34b197ff2fc 100644 --- a/src/include/cluster/cluster_gcs_block.h +++ b/src/include/cluster/cluster_gcs_block.h @@ -1510,8 +1510,8 @@ typedef enum GcsBlockReplyStatus { * reply instead — the requester * keeps 53R97 (Rule 8.A). */ , - GCS_BLOCK_REPLY_UNDO_MULTI_VERDICT_RESULT = 20 /* PGRAC: spec-7.1 D3-b NEW; the - * origin's LMS enumerated a foreign + GCS_BLOCK_REPLY_UNDO_MULTI_VERDICT_RESULT = 20, /* PGRAC: spec-7.1 D3-b NEW; the + * origin's LMS enumerated a foreign * multixact's members and served a * per-updater-member batch verdict * (ClusterGcsUndoMultiVerdictPage in @@ -1520,8 +1520,14 @@ typedef enum GcsBlockReplyStatus { * statuses 18/19. Shipped ONLY when * every updater member is proven * (status SERVED); any unprovable - * multi is a DENIED reply — the - * requester keeps 53R97 (Rule 8.A). */ + * multi is a DENIED reply — the + * requester keeps 53R97 (Rule 8.A). */ + GCS_BLOCK_REPLY_R4_CR_FULL = 21, + GCS_BLOCK_REPLY_R4_TX_RESOLVE_RESULT = 22, + GCS_BLOCK_REPLY_R4_MULTI_RESOLVE_RESULT = 23, + GCS_BLOCK_REPLY_R4_UNDO_DATA_RESULT = 24, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED = 25, + GCS_BLOCK_REPLY_R4_DENIED = 26 } GcsBlockReplyStatus; /* @@ -1565,7 +1571,34 @@ StaticAssertDecl(GCS_BLOCK_REPLY_UNDO_VERDICT_RESULT == GCS_BLOCK_REPLY_UNDO_TT_ "spec-6.12i undo-verdict status must follow the undo-TT fetch status"); StaticAssertDecl(GCS_BLOCK_REPLY_UNDO_MULTI_VERDICT_RESULT == GCS_BLOCK_REPLY_UNDO_VERDICT_RESULT + 1, - "spec-7.1 D3-b undo-multi-verdict status must be the tail enum value"); + "spec-7.1 D3-b undo-multi-verdict status must follow undo-verdict"); +StaticAssertDecl(GCS_BLOCK_REPLY_R4_CR_FULL == 21, + "R4 reply status ABI must begin at 21"); +StaticAssertDecl(GCS_BLOCK_REPLY_R4_DENIED == 26, + "R4 reply status ABI must end at 26"); + +/* PGRAC adaptation: R4 owns one closed status suffix. Keep the domain + * predicates numeric so legacy and R4 decoders cannot accept each other's + * frames merely because both use the 48+BLCKSZ envelope shape. */ +static inline bool +GcsBlockReplyStatusIsLegacy(GcsBlockReplyStatus status) +{ + return status >= GCS_BLOCK_REPLY_GRANTED + && status <= GCS_BLOCK_REPLY_UNDO_MULTI_VERDICT_RESULT; +} + +static inline bool +GcsBlockReplyStatusIsR4(GcsBlockReplyStatus status) +{ + return status >= GCS_BLOCK_REPLY_R4_CR_FULL && status <= GCS_BLOCK_REPLY_R4_DENIED; +} + +static inline bool +GcsBlockReplyStatusIsR4Refusal(GcsBlockReplyStatus status) +{ + return status == GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED + || status == GCS_BLOCK_REPLY_R4_DENIED; +} /* PGRAC: spec-6.12i / spec-7.1 — every undo-plane reply kind (TT-header fetch, * single-xid verdict, batched multi-member verdict) ships the BLCKSZ page plus @@ -2404,6 +2437,9 @@ typedef struct ClusterR4CrRouteProof { int32 selected_holder_node; } ClusterR4CrRouteProof; +StaticAssertDecl(sizeof(ClusterR4CrRouteProof) == 80, + "R4 CR route proof must remain 80 bytes"); + /* * R4 D3 master-side policy over one coherent PCM snapshot. NONE means one * canonical current holder was selected. This helper neither queries nor @@ -2812,9 +2848,6 @@ cluster_r4_transition_lookup(ClusterR4OperationState state, ClusterR4OperationEv #undef CLUSTER_R4_ROUTE_OWNERS extern const char *cluster_cr_build_reason_name(ClusterCrBuildReason reason); -extern ClusterCrBuildResult cluster_gcs_block_r4_route_cr( - const BufferTag *tag, SCN read_scn, uint64 request_id, int32 requester_backend_id, - ClusterR4CrRouteProof *out, ClusterCrBuildReason *reason_out); typedef enum ClusterR4WireKind { CLUSTER_R4_WIRE_CR_BUILD = 1, @@ -2859,6 +2892,40 @@ StaticAssertDecl(offsetof(ClusterR4ForwardExtension, == 12, "R4 FORWARD96 transition count must occupy absolute bytes 76..83"); +typedef struct ClusterR4CrRequestPayload { + GcsBlockRequestPayload base; + ClusterR4RequestExtension extension; +} ClusterR4CrRequestPayload; + +typedef struct ClusterR4CrForwardPayload { + GcsBlockForwardPayload base; + ClusterR4ForwardExtension extension; +} ClusterR4CrForwardPayload; + +StaticAssertDecl(sizeof(ClusterR4CrRequestPayload) == 80, + "R4 CR request payload must remain 80 bytes"); +StaticAssertDecl(sizeof(ClusterR4CrForwardPayload) == 96, + "R4 CR forward payload must remain 96 bytes"); + +struct ClusterICEnvelope; +extern ClusterCrBuildResult cluster_gcs_block_r4_route_cr( + const struct ClusterICEnvelope *env, const ClusterR4CrRequestPayload *request, + ClusterCrBuildReason *reason_out); +#ifdef USE_CLUSTER_UNIT +extern bool cluster_gcs_block_test_r4_request80(const struct ClusterICEnvelope *env, + const void *payload); +extern bool cluster_gcs_block_test_r4_forward96(const struct ClusterICEnvelope *env, + const void *payload); +extern bool cluster_gcs_block_test_r4_refusal_status(ClusterCrBuildResult result, + ClusterCrBuildReason reason, + bool admitted_forward, + GcsBlockReplyStatus *status_out); +extern bool cluster_gcs_block_test_decode_r4_reply( + const struct ClusterICEnvelope *env, const void *payload, uint64 expected_request_id, + uint64 expected_epoch, int32 expected_requester_backend_id, uint8 expected_transition_id, + int32 expected_sender_node); +#endif + static inline void ClusterR4WireWriteU16(uint8 out[2], uint16 value) { diff --git a/src/include/cluster/cluster_gcs_block_dedup.h b/src/include/cluster/cluster_gcs_block_dedup.h index 92624eefd38..05339783db8 100644 --- a/src/include/cluster/cluster_gcs_block_dedup.h +++ b/src/include/cluster/cluster_gcs_block_dedup.h @@ -156,12 +156,56 @@ typedef enum GcsBlockDedupEntryKind { GCS_BLOCK_DEDUP_ENTRY_PCM_X_MATERIALIZED_UNCOMMITTED = 3, /* Exact descriptor/byte cleanup completed after local TERMINAL_DRAINED. * Keep the binding as an ACK replay tombstone until exact RETIRE. */ - GCS_BLOCK_DEDUP_ENTRY_PCM_X_DRAINED = 4 + GCS_BLOCK_DEDUP_ENTRY_PCM_X_DRAINED = 4, + GCS_BLOCK_DEDUP_ENTRY_R4_CR_ROUTE = 5 } GcsBlockDedupEntryKind; +typedef enum GcsBlockR4RouteState { + GCS_BLOCK_R4_ROUTE_ROUTING = 1, + GCS_BLOCK_R4_ROUTE_FORWARDED = 2, + GCS_BLOCK_R4_ROUTE_RETRYABLE = 3 +} GcsBlockR4RouteState; + +typedef struct GcsBlockR4RouteIdentity { + GcsBlockDedupKey legacy_key; + BufferTag tag; + SCN read_scn; + uint64 activation_generation; +} GcsBlockR4RouteIdentity; + +typedef struct GcsBlockR4RouteRecord { + ClusterR4CrRouteProof proof; + uint8 state; + uint8 reserved[47]; +} GcsBlockR4RouteRecord; + +typedef enum GcsBlockR4RouteArmResult { + GCS_BLOCK_R4_ROUTE_ARM_INVALID = -1, + GCS_BLOCK_R4_ROUTE_ARM_NEW = 0, + GCS_BLOCK_R4_ROUTE_ARM_REPLAY = 1, + GCS_BLOCK_R4_ROUTE_ARM_RETRYABLE = 2, + GCS_BLOCK_R4_ROUTE_ARM_IDENTITY_COLLISION = 3, + GCS_BLOCK_R4_ROUTE_ARM_HOLDER_MOVED = 4, + GCS_BLOCK_R4_ROUTE_ARM_FULL = 5 +} GcsBlockR4RouteArmResult; + +typedef enum GcsBlockR4RouteSendResult { + GCS_BLOCK_R4_ROUTE_SEND_INVALID = -1, + GCS_BLOCK_R4_ROUTE_SEND_FORWARDED = 0, + GCS_BLOCK_R4_ROUTE_SEND_RETRYABLE = 1, + GCS_BLOCK_R4_ROUTE_SEND_STALE = 2, + GCS_BLOCK_R4_ROUTE_SEND_COLLISION = 3 +} GcsBlockR4RouteSendResult; + +StaticAssertDecl(sizeof(GcsBlockR4RouteIdentity) == 64, + "R4 CR route identity must remain 64 bytes"); +StaticAssertDecl(sizeof(GcsBlockR4RouteRecord) == 128, + "R4 CR route record must remain 128 bytes"); + typedef union GcsBlockDedupPayloadMeta { ClusterSfDepVec sf_dep_vec; GcsBlockPcmXImageIdentity pcm_x_identity; + GcsBlockR4RouteRecord r4_route; } GcsBlockDedupPayloadMeta; StaticAssertDecl(sizeof(GcsBlockDedupPayloadMeta) == 128, @@ -183,8 +227,8 @@ typedef struct GcsBlockDedupEntry { uint8 _pad1[5]; /* 5B — dep_vec @ 112 */ GcsBlockDedupPayloadMeta payload_meta; /* 128B — deps or exact PCM-X identity */ char block_data[GCS_BLOCK_DATA_SIZE]; /* 8192B — full page payload */ - TimestampTz completed_at_ts; /* 8B — TTL sweep replied */ - TimestampTz registered_at_ts; /* 8B — TTL sweep in-flight */ + TimestampTz completed_at_ts; /* 8B — generic wall / R4 monotonic replied */ + TimestampTz registered_at_ts; /* 8B — generic wall / R4 monotonic in-flight */ TimestampTz done_at_ts; /* 8B — round-2: DONE proof consumed */ int64 pinned_lifetime_us; /* 8B — round-2: TTL pinned at register */ int64 pinned_done_linger_us; /* 8B — round-2: quarantine pinned */ @@ -478,6 +522,17 @@ extern GcsBlockDedupResult cluster_gcs_block_dedup_lookup_or_register( uint32 requester_lifetime_hint_ms, bool requester_done_capable, GcsBlockDedupEntry *cached_reply_out); +extern GcsBlockR4RouteArmResult cluster_gcs_block_dedup_r4_route_arm_or_match( + int worker_id, const GcsBlockR4RouteIdentity *identity, uint8 transition_id, + const ClusterR4CrRouteProof *fresh_proof, uint32 requester_lifetime_hint_ms, + bool lifetime_hint_trusted, GcsBlockR4RouteRecord *record_out); +extern GcsBlockR4RouteSendResult cluster_gcs_block_dedup_r4_route_finish_send( + int worker_id, const GcsBlockR4RouteIdentity *identity, uint8 transition_id, + const ClusterR4CrRouteProof *armed_proof, bool outbound_admitted); +extern uint64 cluster_gcs_block_dedup_r4_route_sweep_epoch(uint64 current_epoch); +extern uint64 cluster_gcs_block_dedup_r4_route_count(void); +extern uint64 cluster_gcs_block_dedup_r4_route_purge_closed(void); + /* Under the routed shard lock, terminate one same-tag, still-live legacy * N->S grant/forward identity after PCM-X publishes its queue-kind claim. * A by-value cached denial is returned for initial send or periodic replay; diff --git a/src/include/cluster/cluster_lms.h b/src/include/cluster/cluster_lms.h index ba694f242d9..0b72bc042ca 100644 --- a/src/include/cluster/cluster_lms.h +++ b/src/include/cluster/cluster_lms.h @@ -410,8 +410,11 @@ extern bool cluster_lms_outbound_enqueue_cap_bound(int worker_id, uint8 msg_type uint32 connection_generation); struct GcsBlockReplyHeader; extern bool cluster_lms_outbound_enqueue_zero_block_reply(int worker_id, uint32 dest_node_id, - const struct GcsBlockReplyHeader *header, - bool direct_land); + const struct GcsBlockReplyHeader *header, + bool direct_land); +extern bool cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + int worker_id, uint32 dest_node_id, const struct GcsBlockReplyHeader *header, + uint32 required_capability, uint32 connection_generation); extern int cluster_lms_outbound_drain_send(int worker_id); extern uint32 cluster_lms_outbound_depth(int worker_id); extern void cluster_lms_note_pcm_x_image_ready_boundary(uint8 msg_type, const char *boundary, diff --git a/src/include/cluster/cluster_semantic_activation.h b/src/include/cluster/cluster_semantic_activation.h index caa9f610837..b9e18eadf11 100644 --- a/src/include/cluster/cluster_semantic_activation.h +++ b/src/include/cluster/cluster_semantic_activation.h @@ -136,6 +136,9 @@ extern ClusterSemanticAdmissionResult cluster_semantic_activation_enter(uint64 feature_bit, ClusterSemanticAdmissionSide side, ClusterSemanticAdmissionToken *token); extern bool cluster_semantic_activation_recheck(const ClusterSemanticAdmissionToken *token); +extern bool cluster_semantic_activation_peer_open_matches( + const ClusterSemanticAdmissionToken *token, int32 authenticated_peer_node_id, + uint32 required_hello_caps, uint32 sampled_capability_generation); extern void cluster_semantic_activation_leave(ClusterSemanticAdmissionToken *token); extern Size cluster_semantic_activation_shmem_size(void); extern void cluster_semantic_activation_shmem_init(void); diff --git a/src/include/cluster/cluster_sf_dep.h b/src/include/cluster/cluster_sf_dep.h index 24acea633d2..b25e9670006 100644 --- a/src/include/cluster/cluster_sf_dep.h +++ b/src/include/cluster/cluster_sf_dep.h @@ -274,6 +274,9 @@ extern bool cluster_sf_peer_supports_pcm_x_rebase(int32 peer_id); extern bool cluster_sf_peer_supports_pcm_x_source_floor(int32 peer_id); extern bool cluster_sf_peer_pcm_x_source_floor_sample(int32 peer_id, bool *source_floor_out, uint32 *generation_out); +extern bool cluster_sf_peer_capability_family_sample( + int32 peer_id, uint32 required_capabilities, uint32 optional_capabilities, + bool *optional_supported_out, uint32 *generation_out); extern bool cluster_sf_peer_capability_generation_matches(int32 peer_id, uint32 required_capabilities, uint32 expected_generation); diff --git a/src/test/cluster_unit/Makefile b/src/test/cluster_unit/Makefile index fba47c62d96..a1c7f27043a 100644 --- a/src/test/cluster_unit/Makefile +++ b/src/test/cluster_unit/Makefile @@ -106,7 +106,7 @@ TESTS = test_cluster_basic test_cluster_version test_cluster_backend_types \ test_cluster_vis_undo_verdict_map \ test_cluster_undo_horizon \ test_cluster_lms_shard test_cluster_pcm_x_image_fetch \ - test_cluster_gcs_block_dedup \ + test_cluster_gcs_block_dedup test_cluster_gcs_block_dedup_r4_route \ test_cluster_gcs_block_shard # Path to the cluster_version object (no PG deps, safe to link standalone). @@ -249,7 +249,8 @@ test_cluster_backup: test_cluster_backup.c unit_test.h $(CLUSTER_VERSION_O) \ # objects reference). SIMPLE_TESTS = $(filter-out test_cluster_ic_tier1_partial test_cluster_lms_outbound 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_scn_frontier test_cluster_adg test_cluster_epoch test_cluster_fence test_cluster_reconfig test_cluster_ges test_cluster_grd_outbound 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_writer_chain 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_mxid_stripe test_cluster_share_barrier test_cluster_heap_barrier 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_block_dedup_reclaim test_cluster_gcs_block_dedup_htab 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_active_itl_transfer test_cluster_itl_wal test_cluster_multixact test_cluster_multixact_served test_cluster_pcm_lock test_cluster_pcm_own test_cluster_pcm_direct_init test_cluster_pcm_x_convert test_cluster_pcm_x_image_fetch 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_undo_block0 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_xid_authority test_cluster_recovery_anchor test_cluster_relmap_authority test_cluster_lms_shard test_cluster_gcs_block_dedup test_cluster_gcs_block_shard test_cluster_undo_resid test_cluster_undo_authority test_cluster_undo_gcs test_cluster_undo_verdict test_cluster_vis_undo_verdict_map test_cluster_undo_horizon test_cluster_r4_static_model test_cluster_r4_tx_locator test_cluster_r4_tx_outcome test_cluster_r4_cr_walk test_cluster_r4_activation_record test_cluster_r4_activation_fsm test_cluster_r4_lock_order,$(TESTS)) SIMPLE_TESTS := $(filter-out test_cluster_r4_d10_hint_source test_cluster_r4_d10_tt_source \ - test_cluster_r4_d10_multi_source,$(SIMPLE_TESTS)) + test_cluster_r4_d10_multi_source test_cluster_sf_dep \ + test_cluster_gcs_block_dedup_r4_route test_cluster_r4_route_policy,$(SIMPLE_TESTS)) # spec-2.4 D16: test_cluster_epoch links cluster_epoch.o standalone. # cluster_epoch.c references ShmemInitStruct + cluster_shmem_register_region @@ -1099,7 +1100,8 @@ check: all # ---------- clean distclean maintainer-clean: rm -f $(TESTS) $(CLUSTER_UNDO_RECORD_TEST_O) $(CLUSTER_UNDO_ALLOC_TEST_O) \ - $(CLUSTER_QVOTEC_PGSA_TEST_O) $(CLUSTER_R4_RUNTIME_VIS_TEST_O) + $(CLUSTER_QVOTEC_PGSA_TEST_O) $(CLUSTER_R4_RUNTIME_VIS_TEST_O) \ + $(CLUSTER_R4_ROUTE_DEDUP_TEST_O) install installdirs uninstall installcheck: @: @@ -1465,6 +1467,37 @@ test_cluster_r4_tx_outcome: test_cluster_r4_tx_outcome.c unit_test.h \ $(top_builddir)/src/common/libpgcommon_srv.a \ $(top_builddir)/src/port/libpgport_srv.a -o $@ +# Stage 8 R4 D3: compile the real capability store with function sections so +# this focused target executes its shared-lock sampler and existing generation +# fence without pulling unrelated Smart Fusion runtime paths into the fixture. +CLUSTER_R4_SF_DEP_TEST_O = test_cluster_r4_sf_dep_product.o +$(CLUSTER_R4_SF_DEP_TEST_O): $(top_srcdir)/src/backend/cluster/cluster_sf_dep.c \ + $(top_srcdir)/src/include/cluster/cluster_sf_dep.h + $(CC) $(CFLAGS) $(CPPFLAGS) -ffunction-sections -fdata-sections -c $< -o $@ + +test_cluster_sf_dep: test_cluster_sf_dep.c unit_test.h $(CLUSTER_R4_SF_DEP_TEST_O) + $(CC) $(CFLAGS) $(CPPFLAGS) $< \ + $(CLUSTER_R4_SF_DEP_TEST_O) $(R4_RUNTIME_VIS_TEST_DEAD_STRIP) \ + $(top_builddir)/src/common/libpgcommon_srv.a \ + $(top_builddir)/src/port/libpgport_srv.a -o $@ + +# Stage 8 R4 D3: exact REQUEST80/FORWARD96 production decoder seam. The +# USE_CLUSTER_UNIT symbols call the same static branches as the registered +# handlers; function sections keep the fixture independent of legacy GCS code. +CLUSTER_R4_GCS_BLOCK_TEST_O = test_cluster_r4_gcs_block_product.o +$(CLUSTER_R4_GCS_BLOCK_TEST_O): $(top_srcdir)/src/backend/cluster/cluster_gcs_block.c \ + $(top_srcdir)/src/include/cluster/cluster_gcs_block.h + $(CC) $(CFLAGS) $(CPPFLAGS) -DUSE_CLUSTER_UNIT -ffunction-sections -fdata-sections \ + -c $< -o $@ + +test_cluster_r4_route_policy: test_cluster_r4_route_policy.c unit_test.h \ + $(CLUSTER_VERSION_O) $(CLUSTER_R4_GCS_BLOCK_TEST_O) + $(CC) $(CFLAGS) $(CPPFLAGS) $< \ + $(CLUSTER_VERSION_O) $(CLUSTER_R4_GCS_BLOCK_TEST_O) \ + $(R4_RUNTIME_VIS_TEST_DEAD_STRIP) \ + $(top_builddir)/src/common/libpgcommon_srv.a \ + $(top_builddir)/src/port/libpgport_srv.a -o $@ + # spec-8.4 B1/D2: transaction-head and exact previous-edge identity subset. # The production validators are pure header-inline helpers; cluster_uba.o # supplies only the existing origin derivation used by the fixture. @@ -1593,6 +1626,24 @@ test_cluster_gcs_block_dedup_htab: test_cluster_gcs_block_dedup_htab.c unit_test $(top_builddir)/src/common/libpgcommon_srv.a \ $(top_builddir)/src/port/libpgport_srv.a -o $@ +# Stage 8 R4 D3 P1-B: compile the real dedup source into a target-local object +# whose instr_time clock_gettime call is redirected to the independent fixture +# monotonic clock. Other dedup targets continue to link the normal product +# object and observe the normal system clock. +CLUSTER_R4_ROUTE_DEDUP_TEST_O = test_cluster_r4_route_dedup_product.o +$(CLUSTER_R4_ROUTE_DEDUP_TEST_O): \ + $(top_srcdir)/src/backend/cluster/cluster_gcs_block_dedup.c \ + $(top_srcdir)/src/include/cluster/cluster_gcs_block_dedup.h + $(CC) $(CFLAGS) $(CPPFLAGS) \ + -Dclock_gettime=cluster_test_clock_gettime -c $< -o $@ + +test_cluster_gcs_block_dedup_r4_route: test_cluster_gcs_block_dedup_r4_route.c unit_test.h \ + $(CLUSTER_R4_ROUTE_DEDUP_TEST_O) + $(CC) $(CFLAGS) $(CPPFLAGS) $< \ + $(CLUSTER_R4_ROUTE_DEDUP_TEST_O) \ + $(top_builddir)/src/common/libpgcommon_srv.a \ + $(top_builddir)/src/port/libpgport_srv.a -o $@ + # spec-2.35 D17: test_cluster_gcs_block_2way verifies CF 2-way protocol # compile-time invariants (msg_type 16, status 8, GcsBlockForwardPayload # 64B, reply header reserved 重解读 sizeof 不变, dedup FORWARDED_DUPLICATE diff --git a/src/test/cluster_unit/cluster_r4_activation_test_stubs.h b/src/test/cluster_unit/cluster_r4_activation_test_stubs.h index a9fa9b60f57..f5f907c69ae 100644 --- a/src/test/cluster_unit/cluster_r4_activation_test_stubs.h +++ b/src/test/cluster_unit/cluster_r4_activation_test_stubs.h @@ -37,6 +37,16 @@ cluster_epoch_get_current(void) return 0; } +bool cluster_sf_peer_capability_generation_matches(int32 peer_id, uint32 required_capabilities, + uint32 expected_generation); +bool +cluster_sf_peer_capability_generation_matches(int32 peer_id pg_attribute_unused(), + uint32 required_capabilities pg_attribute_unused(), + uint32 expected_generation pg_attribute_unused()) +{ + return false; +} + void on_shmem_exit(pg_on_exit_callback function pg_attribute_unused(), Datum arg pg_attribute_unused()) {} diff --git a/src/test/cluster_unit/test_cluster_gcs_block_dedup_r4_route.c b/src/test/cluster_unit/test_cluster_gcs_block_dedup_r4_route.c new file mode 100644 index 00000000000..2c6eca8110e --- /dev/null +++ b/src/test/cluster_unit/test_cluster_gcs_block_dedup_r4_route.c @@ -0,0 +1,948 @@ +/*------------------------------------------------------------------------- + * + * test_cluster_gcs_block_dedup_r4_route.c + * Behavioral tests for the R4 D3 typed master-route record. This + * binary links the real cluster_gcs_block_dedup.o against a bounded + * fake shared-memory HTAB; only PostgreSQL runtime dependencies are + * stubbed. + * + * 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 + * + * IDENTIFICATION + * src/test/cluster_unit/test_cluster_gcs_block_dedup_r4_route.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include +#include +#include +#include + +#include "cluster/cluster_conf.h" +#include "cluster/cluster_gcs_block.h" +#include "cluster/cluster_gcs_block_dedup.h" +#include "cluster/cluster_guc.h" +#include "cluster/cluster_shmem.h" +#include "miscadmin.h" +#include "storage/backendid.h" +#include "storage/ipc.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" +#include "utils/hsearch.h" +#include "utils/timestamp.h" +#include "unit_test.h" + +UT_DEFINE_GLOBALS(); + +/* Globals normally supplied by cluster_guc.c / globals.c. */ +bool cluster_enabled = true; +int cluster_node_id = 0; +int cluster_lms_workers = 1; +int cluster_gcs_block_dedup_max_entries = 8; +int cluster_gcs_block_retransmit_initial_backoff_ms = 100; +int cluster_gcs_block_retransmit_max_retries = 4; +int cluster_gcs_reply_timeout_ms = 5000; +int MaxConnections = 1; +bool IsUnderPostmaster = false; +BackendId MyBackendId = InvalidBackendId; + +/* 26.5 seconds, pinned with the established 2x discipline. */ +#define TEST_LIFETIME_HINT_MS UINT32_C(26500) +#define TEST_PINNED_LIFETIME_US INT64CONST(53000000) +#define TEST_ROUTE_TRANSITION ((uint8)PCM_TRANS_N_TO_S) +#define TEST_WALL_BASE_US INT64CONST(1000000000) +#define TEST_MONOTONIC_BASE_US INT64CONST(2000000000) + +static TimestampTz fake_now = TEST_WALL_BASE_US; +static struct timespec fake_monotonic_now; +static int fake_declared_nodes = 1; +static int fake_lock_depth = 0; + +int cluster_test_clock_gettime(clockid_t clock_id, struct timespec *tp); + +/* ------------------------------------------------------------------------- + * PostgreSQL runtime stubs required by cluster_gcs_block_dedup.o. + * ------------------------------------------------------------------------- + */ + +void +ExceptionalCondition(const char *conditionName pg_attribute_unused(), + const char *fileName pg_attribute_unused(), + int lineNumber pg_attribute_unused()) +{ + abort(); +} + +bool +errstart(int elevel pg_attribute_unused(), const char *domain pg_attribute_unused()) +{ + return false; +} + +bool +errstart_cold(int elevel pg_attribute_unused(), const char *domain pg_attribute_unused()) +{ + return false; +} + +void +errfinish(const char *filename pg_attribute_unused(), int lineno pg_attribute_unused(), + const char *funcname pg_attribute_unused()) +{} + +int +errcode(int sqlerrcode pg_attribute_unused()) +{ + return 0; +} + +int +errmsg(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +int +errmsg_internal(const char *fmt pg_attribute_unused(), ...) +{ + return 0; +} + +bool +message_level_is_interesting(int elevel pg_attribute_unused()) +{ + return false; +} + +TimestampTz +GetCurrentTimestamp(void) +{ + return fake_now; +} + +int +cluster_test_clock_gettime(clockid_t clock_id pg_attribute_unused(), struct timespec *tp) +{ + Assert(tp != NULL); + *tp = fake_monotonic_now; + return 0; +} + +int +cluster_conf_declared_node_count_early(void) +{ + return fake_declared_nodes; +} + +void +cluster_shmem_register_region(const ClusterShmemRegion *region pg_attribute_unused()) +{} + +void +before_shmem_exit(pg_on_exit_callback function pg_attribute_unused(), + Datum arg pg_attribute_unused()) +{} + +Size +add_size(Size s1, Size s2) +{ + return s1 + s2; +} + +Size +mul_size(Size s1, Size s2) +{ + return s1 * s2; +} + +Size +hash_estimate_size(long num_entries, Size entrysize) +{ + return (Size)num_entries * entrysize; +} + +void +LWLockInitialize(LWLock *lock pg_attribute_unused(), int tranche_id pg_attribute_unused()) +{} + +bool +LWLockAcquire(LWLock *lock pg_attribute_unused(), LWLockMode mode pg_attribute_unused()) +{ + Assert(fake_lock_depth == 0); + fake_lock_depth++; + return true; +} + +void +LWLockRelease(LWLock *lock pg_attribute_unused()) +{ + Assert(fake_lock_depth == 1); + fake_lock_depth--; +} + +/* ------------------------------------------------------------------------- + * Bounded fake shared-memory HTAB. HASH_REMOVE and hole reuse are required + * by route TTL/epoch/requester/closed cleanup. + * ------------------------------------------------------------------------- + */ + +#define FAKE_ROUTE_MAX_SLOTS 32 + +static union { + uint64 force_align; + char data[4096]; +} fake_dedup_header; + +static union { + uint64 force_align; + char data[FAKE_ROUTE_MAX_SLOTS][sizeof(GcsBlockDedupEntry)]; +} fake_dedup_slots; + +static bool fake_slot_used[FAKE_ROUTE_MAX_SLOTS]; +static char fake_dedup_htab_token; +static bool fake_dedup_header_found = false; +static long fake_dedup_entry_max = 0; +static Size fake_dedup_keysize = 0; + +void * +ShmemInitStruct(const char *name pg_attribute_unused(), Size size, bool *foundPtr) +{ + Assert(size <= sizeof(fake_dedup_header.data)); + *foundPtr = fake_dedup_header_found; + fake_dedup_header_found = true; + return fake_dedup_header.data; +} + +HTAB * +ShmemInitHash(const char *name pg_attribute_unused(), long init_size pg_attribute_unused(), + long max_size, HASHCTL *infoP, int hash_flags pg_attribute_unused()) +{ + Assert((hash_flags & HASH_ELEM) != 0); + Assert(infoP->entrysize == sizeof(GcsBlockDedupEntry)); + Assert(max_size <= FAKE_ROUTE_MAX_SLOTS); + fake_dedup_keysize = infoP->keysize; + fake_dedup_entry_max = max_size; + memset(fake_slot_used, 0, sizeof(fake_slot_used)); + return (HTAB *)&fake_dedup_htab_token; +} + +static long +fake_live_count(void) +{ + long i; + long n = 0; + + for (i = 0; i < FAKE_ROUTE_MAX_SLOTS; i++) + if (fake_slot_used[i]) + n++; + return n; +} + +void * +hash_search(HTAB *hashp pg_attribute_unused(), const void *keyPtr, HASHACTION action, + bool *foundPtr) +{ + long i; + + Assert(fake_lock_depth == 1); + Assert(fake_dedup_keysize == sizeof(GcsBlockDedupKey)); + + for (i = 0; i < FAKE_ROUTE_MAX_SLOTS; i++) { + if (!fake_slot_used[i]) + continue; + if (memcmp(fake_dedup_slots.data[i], keyPtr, fake_dedup_keysize) == 0) { + if (foundPtr != NULL) + *foundPtr = true; + if (action == HASH_REMOVE) + fake_slot_used[i] = false; + return fake_dedup_slots.data[i]; + } + } + + if (action != HASH_ENTER && action != HASH_ENTER_NULL) { + if (foundPtr != NULL) + *foundPtr = false; + return NULL; + } + + if (fake_live_count() >= fake_dedup_entry_max) { + if (action == HASH_ENTER_NULL) { + if (foundPtr != NULL) + *foundPtr = false; + return NULL; + } + Assert(false); + } + + for (i = 0; i < FAKE_ROUTE_MAX_SLOTS; i++) { + if (!fake_slot_used[i]) { + memcpy(fake_dedup_slots.data[i], keyPtr, fake_dedup_keysize); + fake_slot_used[i] = true; + if (foundPtr != NULL) + *foundPtr = false; + return fake_dedup_slots.data[i]; + } + } + Assert(false); + return NULL; +} + +void +hash_seq_init(HASH_SEQ_STATUS *status, HTAB *hashp pg_attribute_unused()) +{ + Assert(fake_lock_depth == 1); + status->curBucket = 0; + status->hashp = NULL; +} + +void * +hash_seq_search(HASH_SEQ_STATUS *status) +{ + Assert(fake_lock_depth == 1); + while (status->curBucket < FAKE_ROUTE_MAX_SLOTS) { + uint32 i = status->curBucket++; + + if (fake_slot_used[i]) + return fake_dedup_slots.data[i]; + } + return NULL; +} + +void +hash_seq_term(HASH_SEQ_STATUS *status pg_attribute_unused()) +{} + +/* ------------------------------------------------------------------------- + * Fixture and exact-value assertions. + * ------------------------------------------------------------------------- + */ + +static int64 +fixture_monotonic_us(void) +{ + return (int64)fake_monotonic_now.tv_sec * USECS_PER_SEC + + (int64)fake_monotonic_now.tv_nsec / 1000; +} + +static void +fixture_set_monotonic_us(int64 now_us) +{ + Assert(now_us >= 0); + fake_monotonic_now.tv_sec = (time_t)(now_us / USECS_PER_SEC); + fake_monotonic_now.tv_nsec = (long)((now_us % USECS_PER_SEC) * 1000); +} + +static void +fixture_advance_monotonic_us(int64 delta_us) +{ + fixture_set_monotonic_us(fixture_monotonic_us() + delta_us); +} + +static void +fixture_reset(int cap) +{ + fake_dedup_header_found = false; + fake_dedup_entry_max = 0; + fake_dedup_keysize = 0; + fake_lock_depth = 0; + memset(fake_slot_used, 0, sizeof(fake_slot_used)); + memset(&fake_dedup_header, 0, sizeof(fake_dedup_header)); + memset(&fake_dedup_slots, 0, sizeof(fake_dedup_slots)); + fake_now = TEST_WALL_BASE_US; + fixture_set_monotonic_us(TEST_MONOTONIC_BASE_US); + + cluster_enabled = true; + cluster_node_id = 0; + cluster_lms_workers = 1; + cluster_gcs_block_dedup_max_entries = cap; + MaxConnections = 1; + fake_declared_nodes = 1; + + Assert(cluster_gcs_block_dedup_shmem_size() > 0); + cluster_gcs_block_dedup_shmem_init(); + Assert(fake_lock_depth == 0); +} + +static BufferTag +make_tag(uint32 blockno) +{ + BufferTag tag; + + memset(&tag, 0, sizeof(tag)); + tag.spcOid = 1663; + tag.dbOid = 1; + tag.relNumber = 200; + tag.forkNum = MAIN_FORKNUM; + tag.blockNum = blockno; + return tag; +} + +static GcsBlockR4RouteIdentity +make_identity(uint64 seq, uint64 epoch, uint32 blockno, SCN read_scn, + uint64 activation_generation) +{ + GcsBlockR4RouteIdentity identity; + + memset(&identity, 0, sizeof(identity)); + identity.legacy_key.origin_node_id = 1; + identity.legacy_key.requester_backend_id = 7; + identity.legacy_key.request_id = gcs_reqid_requester(1, 7, seq); + identity.legacy_key.cluster_epoch = epoch; + identity.tag = make_tag(blockno); + identity.read_scn = read_scn; + identity.activation_generation = activation_generation; + return identity; +} + +static ClusterR4CrRouteProof +make_proof(const GcsBlockR4RouteIdentity *identity, int32 holder_node) +{ + ClusterR4CrRouteProof proof; + + memset(&proof, 0, sizeof(proof)); + proof.tag = identity->tag; + proof.read_scn = identity->read_scn; + proof.formation_epoch = identity->legacy_key.cluster_epoch; + proof.activation_generation = identity->activation_generation; + proof.master_authority_generation = (identity->legacy_key.cluster_epoch << 32) | UINT64_C(5); + proof.master_resource_transition_count = UINT64_C(7); + proof.expected_page_scn = (SCN)UINT64_C(11); + proof.real_master_node = 0; + proof.selected_holder_node = holder_node; + return proof; +} + +static void +assert_proof_exact(const ClusterR4CrRouteProof *actual, const ClusterR4CrRouteProof *expected) +{ + UT_ASSERT(memcmp(&actual->tag, &expected->tag, sizeof(BufferTag)) == 0); + UT_ASSERT_EQ(actual->read_scn, expected->read_scn); + UT_ASSERT_EQ(actual->formation_epoch, expected->formation_epoch); + UT_ASSERT_EQ(actual->activation_generation, expected->activation_generation); + UT_ASSERT_EQ(actual->master_authority_generation, expected->master_authority_generation); + UT_ASSERT_EQ(actual->master_resource_transition_count, + expected->master_resource_transition_count); + UT_ASSERT_EQ(actual->expected_page_scn, expected->expected_page_scn); + UT_ASSERT_EQ(actual->real_master_node, expected->real_master_node); + UT_ASSERT_EQ(actual->selected_holder_node, expected->selected_holder_node); +} + +static GcsBlockR4RouteArmResult +arm_route(const GcsBlockR4RouteIdentity *identity, const ClusterR4CrRouteProof *proof, + GcsBlockR4RouteRecord *record_out) +{ + return cluster_gcs_block_dedup_r4_route_arm_or_match( + 0, identity, TEST_ROUTE_TRANSITION, proof, TEST_LIFETIME_HINT_MS, true, record_out); +} + +/* ------------------------------------------------------------------------- + * ABI, arm/match, collision, drift and send publication. + * ------------------------------------------------------------------------- + */ + +UT_TEST(test_route_abi_and_empty_count) +{ + fixture_reset(8); + UT_ASSERT_EQ(64, sizeof(GcsBlockR4RouteIdentity)); + UT_ASSERT_EQ(128, sizeof(GcsBlockR4RouteRecord)); + UT_ASSERT_EQ(128, sizeof(GcsBlockDedupPayloadMeta)); + UT_ASSERT_EQ(8472, sizeof(GcsBlockDedupEntry)); + UT_ASSERT_EQ(0, cluster_gcs_block_dedup_r4_route_count()); +} + +UT_TEST(test_new_then_exact_duplicate_replays_stored_record) +{ + GcsBlockR4RouteIdentity identity = make_identity(1, 3, 41, (SCN)101, 13); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + + fixture_reset(8); + memset(&record, 0xA5, sizeof(record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ROUTING, record.state); + assert_proof_exact(&record.proof, &proof); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(1, fake_live_count()); + + memset(&record, 0x5A, sizeof(record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, arm_route(&identity, &proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ROUTING, record.state); + assert_proof_exact(&record.proof, &proof); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(1, fake_live_count()); +} + +UT_TEST(test_unarmed_expected_page_scn_new_then_exact_replay) +{ + GcsBlockR4RouteIdentity identity = make_identity(21, 3, 62, (SCN)121, 30); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + GcsBlockR4RouteArmResult result; + + proof.expected_page_scn = InvalidScn; + fixture_reset(8); + result = arm_route(&identity, &proof, &record); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, result); + if (result != GCS_BLOCK_R4_ROUTE_ARM_NEW) + return; + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ROUTING, record.state); + UT_ASSERT_EQ(InvalidScn, record.proof.expected_page_scn); + assert_proof_exact(&record.proof, &proof); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + + memset(&record, 0xA5, sizeof(record)); + result = arm_route(&identity, &proof, &record); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, result); + if (result != GCS_BLOCK_R4_ROUTE_ARM_REPLAY) + return; + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ROUTING, record.state); + UT_ASSERT_EQ(InvalidScn, record.proof.expected_page_scn); + assert_proof_exact(&record.proof, &proof); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); +} + +UT_TEST(test_read_scn_collision_preserves_first_record) +{ + GcsBlockR4RouteIdentity first = make_identity(2, 3, 42, (SCN)102, 14); + GcsBlockR4RouteIdentity collision = first; + ClusterR4CrRouteProof first_proof = make_proof(&first, 2); + ClusterR4CrRouteProof collision_proof; + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&first, &first_proof, &record)); + collision.read_scn++; + collision_proof = make_proof(&collision, 2); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_IDENTITY_COLLISION, + arm_route(&collision, &collision_proof, &record)); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, arm_route(&first, &first_proof, &record)); + assert_proof_exact(&record.proof, &first_proof); +} + +UT_TEST(test_activation_collision_preserves_first_record) +{ + GcsBlockR4RouteIdentity first = make_identity(3, 3, 43, (SCN)103, 15); + GcsBlockR4RouteIdentity collision = first; + ClusterR4CrRouteProof first_proof = make_proof(&first, 2); + ClusterR4CrRouteProof collision_proof; + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&first, &first_proof, &record)); + collision.activation_generation++; + collision_proof = make_proof(&collision, 2); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_IDENTITY_COLLISION, + arm_route(&collision, &collision_proof, &record)); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, arm_route(&first, &first_proof, &record)); + assert_proof_exact(&record.proof, &first_proof); +} + +UT_TEST(test_tag_collision_preserves_first_record) +{ + GcsBlockR4RouteIdentity first = make_identity(17, 3, 57, (SCN)117, 26); + GcsBlockR4RouteIdentity collision = first; + ClusterR4CrRouteProof first_proof = make_proof(&first, 2); + ClusterR4CrRouteProof collision_proof; + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&first, &first_proof, &record)); + collision.tag = make_tag(58); + collision_proof = make_proof(&collision, 2); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_IDENTITY_COLLISION, + arm_route(&collision, &collision_proof, &record)); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, arm_route(&first, &first_proof, &record)); + assert_proof_exact(&record.proof, &first_proof); +} + +UT_TEST(test_transition_collision_preserves_first_record) +{ + GcsBlockR4RouteIdentity identity = make_identity(18, 3, 59, (SCN)118, 27); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_IDENTITY_COLLISION, + cluster_gcs_block_dedup_r4_route_arm_or_match( + 0, &identity, (uint8)(TEST_ROUTE_TRANSITION + 1), &proof, + TEST_LIFETIME_HINT_MS, true, &record)); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, arm_route(&identity, &proof, &record)); + assert_proof_exact(&record.proof, &proof); +} + +UT_TEST(test_proof_drift_marks_retryable_without_overwriting_winner) +{ + GcsBlockR4RouteIdentity identity = make_identity(4, 3, 44, (SCN)104, 16); + ClusterR4CrRouteProof first_proof = make_proof(&identity, 2); + ClusterR4CrRouteProof drifted = first_proof; + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &first_proof, &record)); + drifted.selected_holder_node = 3; + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_HOLDER_MOVED, arm_route(&identity, &drifted, &record)); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_RETRYABLE, + arm_route(&identity, &first_proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_SEND_COLLISION, + cluster_gcs_block_dedup_r4_route_finish_send( + 0, &identity, TEST_ROUTE_TRANSITION, &drifted, true)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_SEND_FORWARDED, + cluster_gcs_block_dedup_r4_route_finish_send( + 0, &identity, TEST_ROUTE_TRANSITION, &first_proof, true)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, arm_route(&identity, &first_proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_FORWARDED, record.state); + assert_proof_exact(&record.proof, &first_proof); +} + +UT_TEST(test_send_failure_then_late_admission_becomes_forwarded) +{ + GcsBlockR4RouteIdentity identity = make_identity(5, 3, 45, (SCN)105, 17); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_SEND_RETRYABLE, + cluster_gcs_block_dedup_r4_route_finish_send( + 0, &identity, TEST_ROUTE_TRANSITION, &proof, false)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_RETRYABLE, arm_route(&identity, &proof, &record)); + + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_SEND_FORWARDED, + cluster_gcs_block_dedup_r4_route_finish_send( + 0, &identity, TEST_ROUTE_TRANSITION, &proof, true)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, arm_route(&identity, &proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_FORWARDED, record.state); +} + +UT_TEST(test_send_failure_cannot_downgrade_forwarded) +{ + GcsBlockR4RouteIdentity identity = make_identity(6, 3, 46, (SCN)106, 18); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_SEND_FORWARDED, + cluster_gcs_block_dedup_r4_route_finish_send( + 0, &identity, TEST_ROUTE_TRANSITION, &proof, true)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_SEND_RETRYABLE, + cluster_gcs_block_dedup_r4_route_finish_send( + 0, &identity, TEST_ROUTE_TRANSITION, &proof, false)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, arm_route(&identity, &proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_FORWARDED, record.state); +} + +UT_TEST(test_duplicate_send_admission_does_not_extend_forwarded_ttl) +{ + GcsBlockR4RouteIdentity identity = make_identity(20, 3, 61, (SCN)120, 29); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + TimestampTz first_completion_wall; + int64 first_completion_monotonic; + + fixture_reset(8); + first_completion_wall = fake_now; + first_completion_monotonic = fixture_monotonic_us(); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_SEND_FORWARDED, + cluster_gcs_block_dedup_r4_route_finish_send( + 0, &identity, TEST_ROUTE_TRANSITION, &proof, true)); + + /* A duplicate accepted publication one microsecond later must not refresh + * completed_at_ts. At the original strict TTL + 1 boundary the route + * expires; a refreshed timestamp would leave age == deadline and survive. */ + fake_now = first_completion_wall + 1; + fixture_set_monotonic_us(first_completion_monotonic + 1); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_SEND_FORWARDED, + cluster_gcs_block_dedup_r4_route_finish_send( + 0, &identity, TEST_ROUTE_TRANSITION, &proof, true)); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + + fake_now = first_completion_wall + TEST_PINNED_LIFETIME_US + 1; + fixture_set_monotonic_us(first_completion_monotonic + TEST_PINNED_LIFETIME_US + 1); + cluster_gcs_block_dedup_sweep_expired(fake_now); + UT_ASSERT_EQ(0, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(0, fake_live_count()); +} + +UT_TEST(test_finish_send_after_epoch_cleanup_is_stale) +{ + GcsBlockR4RouteIdentity identity = make_identity(19, 3, 60, (SCN)119, 28); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &proof, &record)); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_sweep_epoch(4)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_SEND_STALE, + cluster_gcs_block_dedup_r4_route_finish_send( + 0, &identity, TEST_ROUTE_TRANSITION, &proof, true)); + UT_ASSERT_EQ(0, cluster_gcs_block_dedup_r4_route_count()); +} + +UT_TEST(test_route_cap_full_preserves_live_record) +{ + GcsBlockR4RouteIdentity first = make_identity(7, 3, 47, (SCN)107, 19); + GcsBlockR4RouteIdentity second = make_identity(8, 3, 48, (SCN)108, 19); + ClusterR4CrRouteProof first_proof = make_proof(&first, 2); + ClusterR4CrRouteProof second_proof = make_proof(&second, 2); + GcsBlockR4RouteRecord record; + + fixture_reset(1); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&first, &first_proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_FULL, arm_route(&second, &second_proof, &record)); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(1, fake_live_count()); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, arm_route(&first, &first_proof, &record)); +} + +/* ------------------------------------------------------------------------- + * Route-specific cleanup and legacy-kind isolation. + * ------------------------------------------------------------------------- + */ + +UT_TEST(test_route_ttl_uses_strict_pinned_deadline) +{ + GcsBlockR4RouteIdentity identity = make_identity(9, 3, 49, (SCN)109, 20); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &proof, &record)); + fake_now += TEST_PINNED_LIFETIME_US; + fixture_advance_monotonic_us(TEST_PINNED_LIFETIME_US); + cluster_gcs_block_dedup_sweep_expired(fake_now); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + + fake_now++; + fixture_advance_monotonic_us(1); + cluster_gcs_block_dedup_sweep_expired(fake_now); + UT_ASSERT_EQ(0, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(0, fake_live_count()); +} + +UT_TEST(test_wall_forward_monotonic_in_window_expires_generic_but_keeps_route) +{ + GcsBlockR4RouteIdentity route = make_identity(22, 3, 63, (SCN)122, 31); + ClusterR4CrRouteProof proof = make_proof(&route, 2); + GcsBlockR4RouteRecord record; + GcsBlockDedupKey generic_key = route.legacy_key; + BufferTag generic_tag = make_tag(64); + + fixture_reset(8); + generic_key.request_id = gcs_reqid_requester(1, 7, 23); + UT_ASSERT_EQ(GCS_BLOCK_DEDUP_MISS_REGISTERED, + cluster_gcs_block_dedup_lookup_or_register( + 0, &generic_key, generic_tag, TEST_ROUTE_TRANSITION, + TEST_LIFETIME_HINT_MS, true, NULL)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&route, &proof, &record)); + + /* Generic remains wall-clock based, while the route is still only one + * monotonic microsecond into its pinned window. */ + fake_now += TEST_PINNED_LIFETIME_US + 1; + fixture_advance_monotonic_us(1); + cluster_gcs_block_dedup_sweep_expired(fake_now); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(1, fake_live_count()); +} + +UT_TEST(test_wall_backward_monotonic_expired_removes_route) +{ + GcsBlockR4RouteIdentity identity = make_identity(24, 3, 65, (SCN)124, 32); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &proof, &record)); + fake_now--; + fixture_advance_monotonic_us(TEST_PINNED_LIFETIME_US + 1); + cluster_gcs_block_dedup_sweep_expired(fake_now); + UT_ASSERT_EQ(0, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(0, fake_live_count()); +} + +UT_TEST(test_cap_one_wall_forward_monotonic_in_window_preserves_route) +{ + GcsBlockR4RouteIdentity first = make_identity(25, 3, 66, (SCN)125, 33); + GcsBlockR4RouteIdentity second = make_identity(26, 3, 67, (SCN)126, 33); + ClusterR4CrRouteProof first_proof = make_proof(&first, 2); + ClusterR4CrRouteProof second_proof = make_proof(&second, 2); + GcsBlockR4RouteRecord record; + GcsBlockR4RouteArmResult result; + + fixture_reset(1); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&first, &first_proof, &record)); + fake_now += TEST_PINNED_LIFETIME_US + 1; + fixture_advance_monotonic_us(1); + result = arm_route(&second, &second_proof, &record); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_FULL, result); + if (result != GCS_BLOCK_R4_ROUTE_ARM_FULL) + return; + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, arm_route(&first, &first_proof, &record)); +} + +UT_TEST(test_cap_one_wall_backward_monotonic_expired_reclaims_route) +{ + GcsBlockR4RouteIdentity first = make_identity(27, 3, 68, (SCN)127, 34); + GcsBlockR4RouteIdentity second = make_identity(28, 3, 69, (SCN)128, 34); + ClusterR4CrRouteProof first_proof = make_proof(&first, 2); + ClusterR4CrRouteProof second_proof = make_proof(&second, 2); + GcsBlockR4RouteRecord record; + GcsBlockR4RouteArmResult result; + + fixture_reset(1); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&first, &first_proof, &record)); + fake_now--; + fixture_advance_monotonic_us(TEST_PINNED_LIFETIME_US + 1); + result = arm_route(&second, &second_proof, &record); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, result); + if (result != GCS_BLOCK_R4_ROUTE_ARM_NEW) + return; + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, + arm_route(&second, &second_proof, &record)); +} + +UT_TEST(test_epoch_zero_is_valid_and_only_stale_formation_is_swept) +{ + GcsBlockR4RouteIdentity identity = make_identity(10, 0, 50, (SCN)110, 21); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &proof, &record)); + UT_ASSERT_EQ(0, cluster_gcs_block_dedup_r4_route_sweep_epoch(0)); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_sweep_epoch(1)); + UT_ASSERT_EQ(0, cluster_gcs_block_dedup_r4_route_count()); +} + +UT_TEST(test_backend_exit_removes_exact_requester_route) +{ + GcsBlockR4RouteIdentity matching = make_identity(11, 3, 51, (SCN)111, 22); + GcsBlockR4RouteIdentity survivor = make_identity(12, 3, 52, (SCN)112, 22); + ClusterR4CrRouteProof matching_proof = make_proof(&matching, 2); + ClusterR4CrRouteProof survivor_proof; + GcsBlockR4RouteRecord record; + + survivor.legacy_key.requester_backend_id = 8; + survivor.legacy_key.request_id = gcs_reqid_requester(1, 8, 12); + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&matching, &matching_proof, &record)); + survivor_proof = make_proof(&survivor, 2); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&survivor, &survivor_proof, &record)); + cluster_gcs_block_dedup_cleanup_on_backend_exit(1, 7); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, + arm_route(&survivor, &survivor_proof, &record)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&matching, &matching_proof, &record)); +} + +UT_TEST(test_node_death_removes_requester_route) +{ + GcsBlockR4RouteIdentity identity = make_identity(13, 3, 53, (SCN)113, 23); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &proof, &record)); + cluster_gcs_block_dedup_cleanup_on_node_dead(1); + UT_ASSERT_EQ(0, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(0, fake_live_count()); +} + +UT_TEST(test_closed_purge_removes_routes_but_preserves_generic_entry) +{ + GcsBlockR4RouteIdentity route = make_identity(14, 3, 54, (SCN)114, 24); + ClusterR4CrRouteProof proof = make_proof(&route, 2); + GcsBlockR4RouteRecord record; + GcsBlockDedupKey generic_key; + BufferTag generic_tag = make_tag(55); + + fixture_reset(8); + generic_key = route.legacy_key; + generic_key.request_id = gcs_reqid_requester(1, 7, 15); + UT_ASSERT_EQ(GCS_BLOCK_DEDUP_MISS_REGISTERED, + cluster_gcs_block_dedup_lookup_or_register( + 0, &generic_key, generic_tag, TEST_ROUTE_TRANSITION, + TEST_LIFETIME_HINT_MS, true, NULL)); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&route, &proof, &record)); + + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_purge_closed()); + UT_ASSERT_EQ(0, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(1, fake_live_count()); + UT_ASSERT_EQ(GCS_BLOCK_DEDUP_IN_FLIGHT_DUPLICATE, + cluster_gcs_block_dedup_lookup_or_register( + 0, &generic_key, generic_tag, TEST_ROUTE_TRANSITION, + TEST_LIFETIME_HINT_MS, true, NULL)); +} + +UT_TEST(test_generic_done_remove_and_pcm_restart_audit_ignore_route) +{ + GcsBlockR4RouteIdentity identity = make_identity(16, 3, 56, (SCN)116, 25); + ClusterR4CrRouteProof proof = make_proof(&identity, 2); + GcsBlockR4RouteRecord record; + uint64 done_mismatch_before; + uint64 pcm_failclosed_before; + + fixture_reset(8); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_NEW, arm_route(&identity, &proof, &record)); + done_mismatch_before = cluster_gcs_block_dedup_get_done_mismatch_count(); + pcm_failclosed_before = cluster_gcs_block_dedup_get_pcm_x_failclosed_count(); + UT_ASSERT(!cluster_gcs_block_dedup_mark_done( + 0, &identity.legacy_key, &identity.tag, TEST_ROUTE_TRANSITION)); + cluster_gcs_block_dedup_remove(0, &identity.legacy_key); + UT_ASSERT(!cluster_gcs_block_dedup_pcm_x_restart_audit(0)); + UT_ASSERT_EQ(GCS_BLOCK_DEDUP_VALIDATION_FAIL, + cluster_gcs_block_dedup_lookup_or_register( + 0, &identity.legacy_key, identity.tag, TEST_ROUTE_TRANSITION, + TEST_LIFETIME_HINT_MS, true, NULL)); + UT_ASSERT_EQ(done_mismatch_before, + cluster_gcs_block_dedup_get_done_mismatch_count()); + UT_ASSERT_EQ(pcm_failclosed_before, + cluster_gcs_block_dedup_get_pcm_x_failclosed_count()); + UT_ASSERT_EQ(1, cluster_gcs_block_dedup_r4_route_count()); + UT_ASSERT_EQ(GCS_BLOCK_R4_ROUTE_ARM_REPLAY, arm_route(&identity, &proof, &record)); +} + +int +main(void) +{ + UT_PLAN(23); + UT_RUN(test_route_abi_and_empty_count); + UT_RUN(test_new_then_exact_duplicate_replays_stored_record); + UT_RUN(test_unarmed_expected_page_scn_new_then_exact_replay); + UT_RUN(test_read_scn_collision_preserves_first_record); + UT_RUN(test_activation_collision_preserves_first_record); + UT_RUN(test_tag_collision_preserves_first_record); + UT_RUN(test_transition_collision_preserves_first_record); + UT_RUN(test_proof_drift_marks_retryable_without_overwriting_winner); + UT_RUN(test_send_failure_then_late_admission_becomes_forwarded); + UT_RUN(test_send_failure_cannot_downgrade_forwarded); + UT_RUN(test_duplicate_send_admission_does_not_extend_forwarded_ttl); + UT_RUN(test_finish_send_after_epoch_cleanup_is_stale); + UT_RUN(test_route_cap_full_preserves_live_record); + UT_RUN(test_route_ttl_uses_strict_pinned_deadline); + UT_RUN(test_wall_forward_monotonic_in_window_expires_generic_but_keeps_route); + UT_RUN(test_wall_backward_monotonic_expired_removes_route); + UT_RUN(test_cap_one_wall_forward_monotonic_in_window_preserves_route); + UT_RUN(test_cap_one_wall_backward_monotonic_expired_reclaims_route); + UT_RUN(test_epoch_zero_is_valid_and_only_stale_formation_is_swept); + UT_RUN(test_backend_exit_removes_exact_requester_route); + UT_RUN(test_node_death_removes_requester_route); + UT_RUN(test_closed_purge_removes_routes_but_preserves_generic_entry); + UT_RUN(test_generic_done_remove_and_pcm_restart_audit_ignore_route); + UT_DONE(); + return ut_failed_count == 0 ? 0 : 1; +} diff --git a/src/test/cluster_unit/test_cluster_gcs_block_shard.c b/src/test/cluster_unit/test_cluster_gcs_block_shard.c index ad9c3e3a068..de546b89be8 100644 --- a/src/test/cluster_unit/test_cluster_gcs_block_shard.c +++ b/src/test/cluster_unit/test_cluster_gcs_block_shard.c @@ -383,6 +383,99 @@ UT_TEST(test_route_ignores_non_tag_fields) } } +/* R4 extended route frames retain the legacy offset-16 BufferTag key. */ +enum { + GCS_BLOCK_ROUTE_TAG_OFFSET = 16, + GCS_BLOCK_LEGACY_ROUTE_LEN = 64, + GCS_BLOCK_R4_REQUEST_ROUTE_LEN = 80, + GCS_BLOCK_R4_FORWARD_ROUTE_LEN = 96, + GCS_BLOCK_R4_ROUTE_PROBE_LEN = 97 +}; + +static void +make_r4_route_frame(uint8 *frame, Size frame_len, BufferTag tag, uint8 fill) +{ + memset(frame, fill, frame_len); + memcpy(frame + GCS_BLOCK_ROUTE_TAG_OFFSET, &tag, sizeof(tag)); +} + +/* ====================================================================== + * U8 -- REQUEST80 and FORWARD96 use the same offset-16 tag shard as their + * legacy 64-byte forms. Changing every byte outside the tag must not + * move either extended frame to another worker. + * ====================================================================== */ +UT_TEST(test_r4_extended_route_exact_lengths_and_tag_affinity) +{ + BufferTag tag = make_tag(1663, 5, 24002, MAIN_FORKNUM, 101); + GcsBlockRequestPayload legacy_req = make_request(tag); + GcsBlockForwardPayload legacy_fwd = make_forward(tag); + union { + uint64 align; + uint8 bytes[GCS_BLOCK_R4_ROUTE_PROBE_LEN]; + } req_a, req_b, fwd_a, fwd_b; + int expected = cluster_lms_shard_for_tag(&tag, CLUSTER_LMS_MAX_WORKERS); + + UT_ASSERT_EQ(sizeof(legacy_req), GCS_BLOCK_LEGACY_ROUTE_LEN); + UT_ASSERT_EQ(sizeof(legacy_fwd), GCS_BLOCK_LEGACY_ROUTE_LEN); + UT_ASSERT_EQ(cluster_gcs_block_payload_shard(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, &legacy_req, + sizeof(legacy_req), CLUSTER_LMS_MAX_WORKERS), + expected); + UT_ASSERT_EQ(cluster_gcs_block_payload_shard(PGRAC_IC_MSG_GCS_BLOCK_FORWARD, &legacy_fwd, + sizeof(legacy_fwd), CLUSTER_LMS_MAX_WORKERS), + expected); + + make_r4_route_frame(req_a.bytes, GCS_BLOCK_R4_REQUEST_ROUTE_LEN, tag, 0x00); + make_r4_route_frame(req_b.bytes, GCS_BLOCK_R4_REQUEST_ROUTE_LEN, tag, 0xA5); + UT_ASSERT_EQ(cluster_gcs_block_payload_shard(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, req_a.bytes, + GCS_BLOCK_R4_REQUEST_ROUTE_LEN, + CLUSTER_LMS_MAX_WORKERS), + expected); + UT_ASSERT_EQ(cluster_gcs_block_payload_shard(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, req_b.bytes, + GCS_BLOCK_R4_REQUEST_ROUTE_LEN, + CLUSTER_LMS_MAX_WORKERS), + expected); + + make_r4_route_frame(fwd_a.bytes, GCS_BLOCK_R4_FORWARD_ROUTE_LEN, tag, 0x00); + make_r4_route_frame(fwd_b.bytes, GCS_BLOCK_R4_FORWARD_ROUTE_LEN, tag, 0x5A); + UT_ASSERT_EQ(cluster_gcs_block_payload_shard(PGRAC_IC_MSG_GCS_BLOCK_FORWARD, fwd_a.bytes, + GCS_BLOCK_R4_FORWARD_ROUTE_LEN, + CLUSTER_LMS_MAX_WORKERS), + expected); + UT_ASSERT_EQ(cluster_gcs_block_payload_shard(PGRAC_IC_MSG_GCS_BLOCK_FORWARD, fwd_b.bytes, + GCS_BLOCK_R4_FORWARD_ROUTE_LEN, + CLUSTER_LMS_MAX_WORKERS), + expected); +} + +/* ====================================================================== + * U9 -- extended route admission is exact, not a minimum-size check: + * REQUEST accepts only 64/80 and FORWARD accepts only 64/96. Adjacent + * lengths and the other frame kind's extended length fail closed. + * ====================================================================== */ +UT_TEST(test_r4_extended_route_length_mismatch_refused) +{ + BufferTag tag = make_tag(1663, 5, 24003, MAIN_FORKNUM, 103); + union { + uint64 align; + uint8 bytes[GCS_BLOCK_R4_ROUTE_PROBE_LEN]; + } payload; + const uint16 request_bad_lengths[] = { 0, 63, 65, 79, 81, 95, 96, 97 }; + const uint16 forward_bad_lengths[] = { 0, 63, 65, 79, 80, 81, 95, 97 }; + Size i; + + make_r4_route_frame(payload.bytes, sizeof(payload.bytes), tag, 0xC3); + for (i = 0; i < lengthof(request_bad_lengths); i++) + UT_ASSERT_EQ(cluster_gcs_block_payload_shard(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, + payload.bytes, request_bad_lengths[i], + CLUSTER_LMS_MAX_WORKERS), + -1); + for (i = 0; i < lengthof(forward_bad_lengths); i++) + UT_ASSERT_EQ(cluster_gcs_block_payload_shard(PGRAC_IC_MSG_GCS_BLOCK_FORWARD, + payload.bytes, forward_bad_lengths[i], + CLUSTER_LMS_MAX_WORKERS), + -1); +} + /* Every staged PCM-X frame is tag-affine. RETIRE/RETIRE_ACK are the only * direct-send members because their compact payload intentionally has no tag. */ UT_TEST(test_pcm_x_route_truth_table) @@ -477,7 +570,7 @@ UT_TEST(test_pi_durable_note_routes_to_exact_tag_worker) int main(void) { - UT_PLAN(9); + UT_PLAN(11); UT_RUN(test_route_matches_shard_for_tag); UT_RUN(test_route_ack_request_interleave_affinity); UT_RUN(test_route_registry_partition); @@ -485,6 +578,8 @@ main(void) UT_RUN(test_route_length_mismatch_refused); UT_RUN(test_route_n1_degenerate_zero); UT_RUN(test_route_ignores_non_tag_fields); + UT_RUN(test_r4_extended_route_exact_lengths_and_tag_affinity); + UT_RUN(test_r4_extended_route_length_mismatch_refused); UT_RUN(test_pcm_x_route_truth_table); UT_RUN(test_pi_durable_note_routes_to_exact_tag_worker); UT_DONE(); diff --git a/src/test/cluster_unit/test_cluster_lms_outbound.c b/src/test/cluster_unit/test_cluster_lms_outbound.c index cec15755377..bbba4cbc2ec 100644 --- a/src/test/cluster_unit/test_cluster_lms_outbound.c +++ b/src/test/cluster_unit/test_cluster_lms_outbound.c @@ -269,6 +269,7 @@ typedef struct UtSentRec { uint8 marker; /* first payload byte identifies the frame */ uint32 payload_len; GcsBlockReplyHeader reply_header; + bool reply_block_zero; } UtSentRec; static UtSentRec ut_sent_log[64]; @@ -278,6 +279,7 @@ static int ut_local_dispatch_count = 0; static uint8 ut_local_dispatch_marker = 0; static int ut_direct_zero_reply_count = 0; static GcsBlockReplyHeader ut_direct_zero_reply_header; +static int ut_checksum_call_count = 0; bool cluster_ic_envelope_build(ClusterICEnvelope *out_env, uint8 msg_type, uint32 source_node_id, @@ -313,8 +315,17 @@ cluster_ic_send_envelope(uint8 msg_type, int32 dest_node_id, const void *payload ut_sent_log[ut_sent_n].dest = dest_node_id; ut_sent_log[ut_sent_n].marker = payload_len > 0 ? *(const uint8 *)payload : 0; ut_sent_log[ut_sent_n].payload_len = payload_len; - if (msg_type == PGRAC_IC_MSG_GCS_BLOCK_REPLY && payload_len >= sizeof(GcsBlockReplyHeader)) + if (msg_type == PGRAC_IC_MSG_GCS_BLOCK_REPLY && payload_len >= sizeof(GcsBlockReplyHeader)) { + const uint8 *block_data = ((const uint8 *)payload) + sizeof(GcsBlockReplyHeader); + uint32 i; + memcpy(&ut_sent_log[ut_sent_n].reply_header, payload, sizeof(GcsBlockReplyHeader)); + ut_sent_log[ut_sent_n].reply_block_zero + = payload_len == GCS_BLOCK_REPLY_PAYLOAD_TOTAL_SIZE; + for (i = 0; ut_sent_log[ut_sent_n].reply_block_zero && i < GCS_BLOCK_DATA_SIZE; i++) + if (block_data[i] != 0) + ut_sent_log[ut_sent_n].reply_block_zero = false; + } } ut_sent_n++; UT_ASSERT(dest_node_id >= 0 && dest_node_id < CLUSTER_MAX_NODES); @@ -325,6 +336,7 @@ uint32 cluster_gcs_block_compute_checksum(const char *block_data) { (void)block_data; + ut_checksum_call_count++; return UINT32_C(0xA55A7E11); } @@ -355,6 +367,7 @@ ut_reset_log(void) ut_local_dispatch_count = 0; ut_local_dispatch_marker = 0; ut_direct_zero_reply_count = 0; + ut_checksum_call_count = 0; memset(&ut_direct_zero_reply_header, 0, sizeof(ut_direct_zero_reply_header)); ut_pcm_x_runtime_state = PCM_X_RUNTIME_ACTIVE; ut_write_fence_enforcing = false; @@ -379,6 +392,23 @@ ut_enqueue_marker(int worker_id, int32 dest, uint8 marker) return ut_enqueue_typed_marker(worker_id, UT_MSG_TYPE, dest, marker); } +static GcsBlockReplyHeader +ut_r4_refusal_header(GcsBlockReplyStatus status, uint64 page_lsn) +{ + GcsBlockReplyHeader header; + + memset(&header, 0, sizeof(header)); + header.request_id = UINT64_C(0x1020304050607080); + header.page_lsn = page_lsn; + header.epoch = UINT64_C(9); + header.sender_node = cluster_node_id; + header.requester_backend_id = 17; + header.transition_id = PCM_TRANS_N_TO_S; + header.status = (uint8)status; + GcsBlockReplyHeaderSetForwardingMasterNode(&header, GCS_BLOCK_REPLY_NO_FORWARDING_MASTER); + return header; +} + /* ============================================================ * Tests. * ============================================================ */ @@ -657,12 +687,80 @@ UT_TEST(test_direct_zero_block_reply_uses_data_owner_direct_lane) UT_ASSERT_EQ(ut_direct_zero_reply_header.checksum, UINT32_C(0xA55A7E11)); } +UT_TEST(test_r4_cap_bound_zero_reply_sends_only_on_exact_generation) +{ + const uint32 cap = PGRAC_IC_HELLO_CAP_SEMANTIC_ACTIVATION_V1 + | PGRAC_IC_HELLO_CAP_R4_SYNC_CR_V1; + GcsBlockReplyHeader hdr + = ut_r4_refusal_header(GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, 1); + + ut_reset_log(); + ut_peer_capabilities[UT_PEER_X] = cap; + ut_peer_cap_generation[UT_PEER_X] = 42; + UT_ASSERT(cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + 2, UT_PEER_X, &hdr, cap, 42)); + UT_ASSERT_EQ(cluster_lms_outbound_drain_send(2), 1); + UT_ASSERT_EQ(ut_sent_n, 1); + UT_ASSERT_EQ(ut_checksum_call_count, 1); + UT_ASSERT_EQ(ut_sent_log[0].payload_len, GCS_BLOCK_REPLY_PAYLOAD_TOTAL_SIZE); + UT_ASSERT(ut_sent_log[0].reply_block_zero); + UT_ASSERT_EQ(ut_sent_log[0].reply_header.status, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED); + UT_ASSERT_EQ(ut_sent_log[0].reply_header.page_lsn, 1); + UT_ASSERT_EQ(ut_sent_log[0].reply_header.checksum, UINT32_C(0xA55A7E11)); + UT_ASSERT_EQ(GcsBlockReplyHeaderGetForwardingMasterNode(&ut_sent_log[0].reply_header), + GCS_BLOCK_REPLY_NO_FORWARDING_MASTER); +} + +UT_TEST(test_r4_cap_bound_zero_reply_drops_drift_before_zero_expansion) +{ + const uint32 cap = PGRAC_IC_HELLO_CAP_SEMANTIC_ACTIVATION_V1 + | PGRAC_IC_HELLO_CAP_R4_SYNC_CR_V1; + GcsBlockReplyHeader hdr = ut_r4_refusal_header(GCS_BLOCK_REPLY_R4_DENIED, 0); + + ut_reset_log(); + ut_peer_capabilities[UT_PEER_X] = cap; + ut_peer_cap_generation[UT_PEER_X] = 43; + UT_ASSERT(cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + 2, UT_PEER_X, &hdr, cap, 42)); + UT_ASSERT_EQ(cluster_lms_outbound_drain_send(2), 0); + UT_ASSERT_EQ(cluster_lms_outbound_depth(2), 0); + UT_ASSERT_EQ(ut_sent_n, 0); + UT_ASSERT_EQ(ut_checksum_call_count, 0); + UT_ASSERT_EQ(ut_cap_guard_drop_count, 1); +} + +UT_TEST(test_zero_reply_wrappers_reject_the_other_status_domain) +{ + const uint32 cap = PGRAC_IC_HELLO_CAP_SEMANTIC_ACTIVATION_V1 + | PGRAC_IC_HELLO_CAP_R4_SYNC_CR_V1; + GcsBlockReplyHeader legacy + = ut_r4_refusal_header(GCS_BLOCK_REPLY_DENIED_PENDING_X, 0); + GcsBlockReplyHeader retryable + = ut_r4_refusal_header(GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, 0); + GcsBlockReplyHeader denied = ut_r4_refusal_header(GCS_BLOCK_REPLY_R4_DENIED, 1); + + ut_reset_log(); + UT_ASSERT(!cluster_lms_outbound_enqueue_zero_block_reply(0, UT_PEER_X, &retryable, false)); + UT_ASSERT(!cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + 0, UT_PEER_X, &legacy, cap, 42)); + UT_ASSERT(!cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + 0, UT_PEER_X, &denied, cap, 42)); + retryable.reserved_0[0] = 1; + UT_ASSERT(!cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + 0, UT_PEER_X, &retryable, cap, 42)); + UT_ASSERT_EQ(cluster_lms_outbound_depth(0), 0); +} + /* A producer must receive false when the selected worker ring is full. The * PI durable-note drain couples this real return contract with its structural * false->break-before-seq-advance unit, so a full shard retains the source * note for the next tick instead of losing it. */ UT_TEST(test_full_worker_ring_refuses_without_overwrite) { + const uint32 cap = PGRAC_IC_HELLO_CAP_SEMANTIC_ACTIVATION_V1 + | PGRAC_IC_HELLO_CAP_R4_SYNC_CR_V1; + GcsBlockReplyHeader refusal = ut_r4_refusal_header(GCS_BLOCK_REPLY_R4_DENIED, 0); int accepted = 0; int sent = 0; @@ -673,6 +771,8 @@ UT_TEST(test_full_worker_ring_refuses_without_overwrite) UT_ASSERT(accepted < 1024); UT_ASSERT_EQ((int)cluster_lms_outbound_depth(1), accepted); UT_ASSERT(!ut_enqueue_marker(1, UT_PEER_X, 0xE3)); + UT_ASSERT(!cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + 1, UT_PEER_X, &refusal, cap, 42)); UT_ASSERT_EQ((int)cluster_lms_outbound_depth(1), accepted); ut_peer_rc[UT_PEER_X] = CLUSTER_IC_SEND_DONE; @@ -736,7 +836,7 @@ UT_TEST(test_cap_bound_frame_sends_on_exact_connection_capability) int main(void) { - UT_PLAN(15); + UT_PLAN(18); UT_RUN(test_ring_shmem_init); UT_RUN(test_admitted_frame_is_never_resubmitted); @@ -749,6 +849,9 @@ main(void) UT_RUN(test_pcm_x_image_ready_and_prepare_transport_boundaries_are_observable); UT_RUN(test_zero_block_reply_is_expanded_by_data_owner); UT_RUN(test_direct_zero_block_reply_uses_data_owner_direct_lane); + UT_RUN(test_r4_cap_bound_zero_reply_sends_only_on_exact_generation); + UT_RUN(test_r4_cap_bound_zero_reply_drops_drift_before_zero_expansion); + UT_RUN(test_zero_reply_wrappers_reject_the_other_status_domain); UT_RUN(test_full_worker_ring_refuses_without_overwrite); UT_RUN(test_cap_bound_frame_drops_on_connection_generation_drift); UT_RUN(test_cap_bound_frame_drops_on_capability_downgrade); diff --git a/src/test/cluster_unit/test_cluster_qvotec.c b/src/test/cluster_unit/test_cluster_qvotec.c index bfcb110929e..86fa0ffaa97 100644 --- a/src/test/cluster_unit/test_cluster_qvotec.c +++ b/src/test/cluster_unit/test_cluster_qvotec.c @@ -702,6 +702,15 @@ cluster_epoch_get_current(void) { return 0; } +bool cluster_sf_peer_capability_generation_matches(int32 peer_id, uint32 required_capabilities, + uint32 expected_generation); +bool +cluster_sf_peer_capability_generation_matches(int32 peer_id pg_attribute_unused(), + uint32 required_capabilities pg_attribute_unused(), + uint32 expected_generation pg_attribute_unused()) +{ + return false; +} #ifndef CLUSTER_QVOTEC_PGSA_UNIT_TEST void cluster_voting_disk_io_install_timeout_handler(void) diff --git a/src/test/cluster_unit/test_cluster_r4_activation_fsm.c b/src/test/cluster_unit/test_cluster_r4_activation_fsm.c index 043baf64987..0f93d39d429 100644 --- a/src/test/cluster_unit/test_cluster_r4_activation_fsm.c +++ b/src/test/cluster_unit/test_cluster_r4_activation_fsm.c @@ -7,8 +7,10 @@ */ #include "postgres.h" +#include "cluster/cluster_conf.h" #include "cluster/cluster_epoch.h" #include "cluster/cluster_semantic_activation.h" +#include "cluster/cluster_sf_dep.h" #include "port/atomics.h" #include "storage/ipc.h" #include "storage/shmem.h" @@ -35,6 +37,11 @@ static int test_exit_registration_count; static uint64 test_current_epoch = 7; static int test_read_barrier_count; static int test_advance_epoch_on_read_barrier; +static bool test_peer_capability_matches; +static int test_peer_capability_match_calls; +static int32 test_peer_capability_match_peer; +static uint32 test_peer_capability_match_caps; +static uint32 test_peer_capability_match_generation; int MyProcPid = 101; volatile sig_atomic_t InterruptPending = false; @@ -70,6 +77,17 @@ void ProcessInterrupts(void) {} +bool +cluster_sf_peer_capability_generation_matches(int32 peer_id, uint32 required_capabilities, + uint32 expected_generation) +{ + test_peer_capability_match_calls++; + test_peer_capability_match_peer = peer_id; + test_peer_capability_match_caps = required_capabilities; + test_peer_capability_match_generation = expected_generation; + return test_peer_capability_matches; +} + static void test_read_barrier(void) { @@ -167,6 +185,11 @@ test_gate_reset(void) test_current_epoch = 7; test_read_barrier_count = 0; test_advance_epoch_on_read_barrier = 0; + test_peer_capability_matches = false; + test_peer_capability_match_calls = 0; + test_peer_capability_match_peer = -1; + test_peer_capability_match_caps = 0; + test_peer_capability_match_generation = UINT32_MAX; MyProcPid = 101; SemanticActivationShmem = NULL; memset(semantic_activation_local_inflight, 0, sizeof(semantic_activation_local_inflight)); @@ -1036,10 +1059,78 @@ UT_TEST(test_113_recheck_samples_snapshot_before_epoch) cluster_semantic_activation_leave(&token); } +UT_TEST(test_114_peer_open_matcher_stays_closed_until_d13_ack_table) +{ + ClusterSemanticAdmissionToken token; + + test_gate_reset(); + test_gate_publish(2, CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, 23, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_TARGET_SIDE, &token), + CLUSTER_SEMANTIC_ADMISSION_OK); + test_peer_capability_matches = true; + UT_ASSERT(!cluster_semantic_activation_peer_open_matches( + &token, 7, PGRAC_IC_HELLO_CAP_SEMANTIC_ACTIVATION_V1 + | PGRAC_IC_HELLO_CAP_R4_SYNC_CR_V1, + 0)); + UT_ASSERT_EQ(test_peer_capability_match_calls, 1); + UT_ASSERT_EQ(test_peer_capability_match_peer, 7); + UT_ASSERT_EQ(test_peer_capability_match_caps, + PGRAC_IC_HELLO_CAP_SEMANTIC_ACTIVATION_V1 + | PGRAC_IC_HELLO_CAP_R4_SYNC_CR_V1); + UT_ASSERT_EQ(test_peer_capability_match_generation, 0); + cluster_semantic_activation_leave(&token); +} + +UT_TEST(test_115_peer_open_matcher_rejects_invalid_inputs_before_capability_match) +{ + ClusterSemanticAdmissionToken target_token; + ClusterSemanticAdmissionToken source_token; + ClusterSemanticAdmissionToken wrong_feature_token; + uint32 required_caps = PGRAC_IC_HELLO_CAP_SEMANTIC_ACTIVATION_V1 + | PGRAC_IC_HELLO_CAP_R4_SYNC_CR_V1; + + test_gate_reset(); + test_gate_publish(2, 0, 24, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_SOURCE_SIDE, &source_token), + CLUSTER_SEMANTIC_ADMISSION_OK); + UT_ASSERT(!cluster_semantic_activation_peer_open_matches(&source_token, 7, required_caps, 0)); + UT_ASSERT_EQ(test_peer_capability_match_calls, 0); + cluster_semantic_activation_leave(&source_token); + + test_gate_reset(); + test_gate_publish(2, CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, 24, test_current_epoch, false); + UT_ASSERT_EQ(cluster_semantic_activation_enter(CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1, + CLUSTER_SEMANTIC_TARGET_SIDE, &target_token), + CLUSTER_SEMANTIC_ADMISSION_OK); + wrong_feature_token = target_token; + wrong_feature_token.feature_bit = 0; + test_peer_capability_matches = true; + UT_ASSERT(!cluster_semantic_activation_peer_open_matches(NULL, 7, required_caps, 0)); + UT_ASSERT(!cluster_semantic_activation_peer_open_matches(&(ClusterSemanticAdmissionToken){0}, 7, + required_caps, 0)); + UT_ASSERT(!cluster_semantic_activation_peer_open_matches(&wrong_feature_token, 7, + required_caps, 0)); + UT_ASSERT(!cluster_semantic_activation_peer_open_matches(&target_token, -1, required_caps, 0)); + UT_ASSERT(!cluster_semantic_activation_peer_open_matches(&target_token, CLUSTER_MAX_NODES, + required_caps, 0)); + UT_ASSERT(!cluster_semantic_activation_peer_open_matches(&target_token, 7, 0, 0)); + UT_ASSERT_EQ(test_peer_capability_match_calls, 0); + test_current_epoch++; + UT_ASSERT(!cluster_semantic_activation_peer_open_matches(&target_token, 7, required_caps, 0)); + UT_ASSERT_EQ(test_peer_capability_match_calls, 0); + test_current_epoch--; + test_peer_capability_matches = false; + UT_ASSERT(!cluster_semantic_activation_peer_open_matches(&target_token, 7, required_caps, 0)); + UT_ASSERT_EQ(test_peer_capability_match_calls, 1); + cluster_semantic_activation_leave(&target_token); +} + int main(void) { - UT_PLAN(113); + UT_PLAN(115); UT_RUN(test_01_feature_bit_is_one); UT_RUN(test_02_required_hello_caps_are_frozen); UT_RUN(test_03_action_values_are_frozen); @@ -1153,6 +1244,8 @@ main(void) UT_RUN(test_111_formation_change_closes_before_debt_drain); UT_RUN(test_112_enter_samples_second_snapshot_before_epoch); UT_RUN(test_113_recheck_samples_snapshot_before_epoch); + UT_RUN(test_114_peer_open_matcher_stays_closed_until_d13_ack_table); + UT_RUN(test_115_peer_open_matcher_rejects_invalid_inputs_before_capability_match); UT_DONE(); return ut_failed_count == 0 ? 0 : 1; } diff --git a/src/test/cluster_unit/test_cluster_r4_route_policy.c b/src/test/cluster_unit/test_cluster_r4_route_policy.c index 6ace95bccea..bc5da81c7f5 100644 --- a/src/test/cluster_unit/test_cluster_r4_route_policy.c +++ b/src/test/cluster_unit/test_cluster_r4_route_policy.c @@ -7,7 +7,25 @@ */ #include "postgres.h" +#include + +#include "cluster/cluster_conf.h" +#include "cluster/cluster_cr_server.h" +#include "cluster/cluster_cssd.h" +#include "cluster/cluster_gcs.h" #include "cluster/cluster_gcs_block.h" +#include "cluster/cluster_gcs_block_dedup.h" +#include "cluster/cluster_grd.h" +#include "cluster/cluster_ic.h" +#include "cluster/cluster_ic_envelope.h" +#include "cluster/cluster_ic_tier1.h" +#include "cluster/cluster_lms.h" +#include "cluster/cluster_lms_shard.h" +#include "cluster/cluster_pcm_lock.h" +#include "cluster/cluster_recovery_merge.h" +#include "cluster/cluster_semantic_activation.h" +#include "cluster/cluster_sf_dep.h" +#include "miscadmin.h" #undef printf @@ -15,6 +33,560 @@ UT_DEFINE_GLOBALS(); +/* The two symbols exist only in the USE_CLUSTER_UNIT special object. They + * call the same static exact-length branches used by the production envelope + * handlers; neither symbol is present in a production build. */ +extern bool cluster_gcs_block_test_r4_request80(const ClusterICEnvelope *env, + const void *payload); +extern bool cluster_gcs_block_test_r4_forward96(const ClusterICEnvelope *env, + const void *payload); +extern bool cluster_gcs_block_test_r4_refusal_status(ClusterCrBuildResult result, + ClusterCrBuildReason reason, + bool admitted_forward, + GcsBlockReplyStatus *status_out); +extern bool cluster_gcs_block_test_decode_r4_reply( + const ClusterICEnvelope *env, const void *payload, uint64 expected_request_id, + uint64 expected_epoch, int32 expected_requester_backend_id, uint8 expected_transition_id, + int32 expected_sender_node); + +/* Backend globals reached by the narrow production route section. */ +sigjmp_buf *PG_exception_stack = NULL; +ErrorContextCallback *error_context_stack = NULL; +int MaxBackends = 32; +bool cluster_enabled = true; +int cluster_node_id = 1; +int cluster_pcm_grd_max_entries = 0; +bool cluster_recmerge_window_active = false; +bool cluster_online_join = true; +int cluster_lms_workers = 4; +ClusterConf *ClusterConfShmem = NULL; + +#define UT_FORMATION_EPOCH UINT64_C(9) +#define UT_ACTIVATION_GENERATION UINT64_C(12) +#define UT_REQUESTER_CAPABILITY_GENERATION UINT32_C(42) +#define UT_MASTER_CAPABILITY_GENERATION UINT32_C(43) +#define UT_HOLDER_CAPABILITY_GENERATION UINT32_C(44) +#define UT_REQUESTER_NODE 2 +#define UT_MASTER_NODE 1 +#define UT_HOLDER_NODE 3 +#define UT_REQUESTER_BACKEND 7 +#define UT_REQUEST_ID UINT64_C(0x0102030405060708) +#define UT_READ_SCN ((SCN)UINT64_C(0x1234)) +#define UT_EXPECTED_PAGE_SCN ((SCN)UINT64_C(0x2222)) +#define UT_MASTER_GENERATION ((UT_FORMATION_EPOCH << 32) | UINT64_C(4)) +#define UT_MASTER_TRANSITION UINT64_C(7) +#define UT_R4_REQUIRED_CAPABILITIES \ + (PGRAC_IC_HELLO_CAP_SEMANTIC_ACTIVATION_V1 | PGRAC_IC_HELLO_CAP_R4_SYNC_CR_V1) + +typedef struct RouteSeamCapture { + ClusterSemanticAdmissionResult admission_result; + bool capability_ok; + bool peer_open_ok; + bool recheck_ok; + bool snapshot_ok; + GcsBlockR4RouteArmResult arm_result; + bool enqueue_ok; + bool refusal_enqueue_ok; + GcsBlockR4RouteSendResult finish_result; + bool holder_submit_ok; + int lookup_master_node; + + int enter_calls; + int leave_calls; + int capability_calls; + int peer_open_calls; + int snapshot_calls; + int arm_calls; + int enqueue_calls; + int refusal_enqueue_calls; + int finish_calls; + int recheck_calls; + int holder_submit_calls; + + int sequence; + int snapshot_sequence; + int arm_sequence; + int enqueue_sequence; + int refusal_enqueue_sequence; + int finish_sequence; + int recheck_sequence; + int leave_sequence; + + int32 capability_peers[4]; + uint32 capability_required[4]; + uint32 capability_optional[4]; + int32 peer_open_peers[4]; + uint32 peer_open_required[4]; + uint32 peer_open_generation[4]; + + GcsBlockR4RouteIdentity armed_identity; + ClusterR4CrRouteProof armed_proof; + uint8 armed_transition; + uint32 armed_lifetime_hint_ms; + bool armed_lifetime_hint_trusted; + + GcsBlockR4RouteIdentity finished_identity; + ClusterR4CrRouteProof finished_proof; + uint8 finished_transition; + bool finished_outbound_admitted; + + int enqueue_worker; + uint8 enqueue_msg_type; + uint32 enqueue_dest; + uint16 enqueue_payload_len; + uint32 enqueue_required_capability; + uint32 enqueue_connection_generation; + uint8 enqueue_payload[128]; + + int refusal_enqueue_worker; + uint32 refusal_enqueue_dest; + uint32 refusal_enqueue_required_capability; + uint32 refusal_enqueue_connection_generation; + GcsBlockReplyHeader refusal_header; + + ClusterR4CrForwardPayload submitted_forward; +} RouteSeamCapture; + +static RouteSeamCapture route_seam; + +static uint32 +route_test_capability_generation(int32 peer_id) +{ + if (peer_id == UT_REQUESTER_NODE) + return UT_REQUESTER_CAPABILITY_GENERATION; + if (peer_id == UT_MASTER_NODE) + return UT_MASTER_CAPABILITY_GENERATION; + if (peer_id == UT_HOLDER_NODE) + return UT_HOLDER_CAPABILITY_GENERATION; + return 0; +} + +static void +route_seam_reset(void) +{ + memset(&route_seam, 0, sizeof(route_seam)); + route_seam.admission_result = CLUSTER_SEMANTIC_ADMISSION_OK; + route_seam.capability_ok = true; + route_seam.peer_open_ok = true; + route_seam.recheck_ok = true; + route_seam.snapshot_ok = true; + route_seam.arm_result = GCS_BLOCK_R4_ROUTE_ARM_NEW; + route_seam.enqueue_ok = true; + route_seam.refusal_enqueue_ok = true; + route_seam.finish_result = GCS_BLOCK_R4_ROUTE_SEND_FORWARDED; + route_seam.holder_submit_ok = true; + route_seam.lookup_master_node = UT_MASTER_NODE; +} + +static BufferTag +route_test_tag(void) +{ + BufferTag tag; + + memset(&tag, 0, sizeof(tag)); + tag.spcOid = 1663; + tag.dbOid = 5; + tag.relNumber = 20000; + tag.forkNum = MAIN_FORKNUM; + tag.blockNum = 37; + return tag; +} + +static ClusterICEnvelope +route_test_envelope(uint8 msg_type, uint32 source, uint32 dest, uint32 payload_length) +{ + ClusterICEnvelope env; + + memset(&env, 0, sizeof(env)); + env.magic = PGRAC_IC_ENVELOPE_MAGIC; + env.version = PGRAC_IC_ENVELOPE_VERSION_V1; + env.msg_type = msg_type; + env.source_node_id = source; + env.dest_node_id = dest; + env.epoch = UT_FORMATION_EPOCH; + env.payload_length = payload_length; + return env; +} + +/* Build wire fixtures from literal bytes, independently of the production + * encoder under test. */ +static ClusterR4CrRequestPayload +route_test_request80(void) +{ + ClusterR4CrRequestPayload request; + uint8 *extension; + + memset(&request, 0, sizeof(request)); + request.base.request_id = UT_REQUEST_ID; + request.base.epoch = UT_FORMATION_EPOCH; + request.base.tag = route_test_tag(); + request.base.sender_node = UT_REQUESTER_NODE; + request.base.requester_backend_id = UT_REQUESTER_BACKEND; + request.base.transition_id = PCM_TRANS_N_TO_S; + /* A negotiated requester supplies a legal 1000 ms inherited lifetime. */ + request.base.reserved_0[2] = 0xe8; + request.base.reserved_0[3] = 0x03; + extension = (uint8 *)&request.extension; + extension[0] = 1; + extension[1] = 1; + extension[4] = 0x34; + extension[5] = 0x12; + return request; +} + +static ClusterR4CrForwardPayload +route_test_forward96(void) +{ + static const uint8 extension_bytes[32] = { + 0x01, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x22, 0x22, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + ClusterR4CrForwardPayload forward; + + memset(&forward, 0, sizeof(forward)); + forward.base.request_id = UT_REQUEST_ID; + forward.base.epoch = UT_FORMATION_EPOCH; + forward.base.tag = route_test_tag(); + forward.base.original_requester_node = UT_REQUESTER_NODE; + forward.base.requester_backend_id = UT_REQUESTER_BACKEND; + forward.base.master_node = UT_MASTER_NODE; + forward.base.transition_id = PCM_TRANS_N_TO_S; + forward.base.expected_pi_watermark_scn_bytes[0] = 0x34; + forward.base.expected_pi_watermark_scn_bytes[1] = 0x12; + forward.base.reserved_0[4] = 1; + memcpy(&forward.extension, extension_bytes, sizeof(extension_bytes)); + return forward; +} + +void +ExceptionalCondition(const char *conditionName pg_attribute_unused(), + const char *fileName pg_attribute_unused(), int lineNumber pg_attribute_unused()) +{ + abort(); +} + +void +pg_re_throw(void) +{ + abort(); +} + +uint64 +cluster_epoch_get_current(void) +{ + return UT_FORMATION_EPOCH; +} + +int +cluster_gcs_lookup_master(BufferTag tag pg_attribute_unused()) +{ + return route_seam.lookup_master_node; +} + +int +cluster_gcs_lookup_master_static(BufferTag tag pg_attribute_unused()) +{ + return route_seam.lookup_master_node; +} + +int +cluster_conf_node_count(void) +{ + return 1; +} + +bool +cluster_grd_join_remaster_active_for_shard(BufferTag tag pg_attribute_unused()) +{ + return false; +} + +bool +cluster_grd_block_view_rebuilt(BufferTag tag pg_attribute_unused()) +{ + return true; +} + +void +cluster_grd_inc_join_block_failclosed(void) +{} + +bool +cluster_grd_offpath_boot_decided(void) +{ + return true; +} + +bool +cluster_grd_recovery_in_progress(void) +{ + return false; +} + +ClusterCssdPeerState +cluster_cssd_get_peer_state(int node_id pg_attribute_unused()) +{ + return CLUSTER_CSSD_PEER_ALIVE; +} + +bool +cluster_merged_instance_is_materialized(int origin_node pg_attribute_unused()) +{ + return true; +} + +uint64 +cluster_merged_instance_recovered_through(int origin_node pg_attribute_unused()) +{ + return UINT64_MAX; +} + +XLogRecPtr +cluster_pcm_lock_pi_watermark_lsn_query(BufferTag tag pg_attribute_unused()) +{ + return InvalidXLogRecPtr; +} + +int +cluster_ic_tier1_my_data_channel(void) +{ + return 0; +} + +int +cluster_lms_shard_for_tag(const BufferTag *tag pg_attribute_unused(), int n_workers) +{ + return n_workers > 0 ? 0 : -1; +} + +int +cluster_gcs_block_payload_shard(uint8 msg_type, const void *payload, uint16 payload_len, + int n_workers) +{ + if (payload == NULL || n_workers <= 0) + return -1; + if (msg_type == PGRAC_IC_MSG_GCS_BLOCK_FORWARD + && payload_len == sizeof(ClusterR4CrForwardPayload)) + return 0; + return -1; +} + +ClusterSemanticAdmissionResult +cluster_semantic_activation_enter(uint64 feature_bit, ClusterSemanticAdmissionSide side, + ClusterSemanticAdmissionToken *token) +{ + route_seam.enter_calls++; + if (token != NULL) + memset(token, 0, sizeof(*token)); + if (route_seam.admission_result != CLUSTER_SEMANTIC_ADMISSION_OK) + return route_seam.admission_result; + if (feature_bit != CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1 + || side != CLUSTER_SEMANTIC_TARGET_SIDE || token == NULL) + return CLUSTER_SEMANTIC_ADMISSION_CLOSED; + token->feature_bit = feature_bit; + token->record_generation = UT_ACTIVATION_GENERATION; + token->formation_epoch = UT_FORMATION_EPOCH; + token->side = (uint8)side; + token->entered = true; + return CLUSTER_SEMANTIC_ADMISSION_OK; +} + +bool +cluster_semantic_activation_recheck(const ClusterSemanticAdmissionToken *token) +{ + route_seam.recheck_calls++; + route_seam.recheck_sequence = ++route_seam.sequence; + return route_seam.recheck_ok && token != NULL && token->entered; +} + +void +cluster_semantic_activation_leave(ClusterSemanticAdmissionToken *token) +{ + if (token == NULL || !token->entered) + return; + route_seam.leave_calls++; + route_seam.leave_sequence = ++route_seam.sequence; + memset(token, 0, sizeof(*token)); +} + +bool +cluster_sf_peer_capability_family_sample(int32 peer_id, uint32 required_bits, + uint32 optional_bits, bool *optional_out, + uint32 *generation_out) +{ + int slot = route_seam.capability_calls++; + + if (optional_out != NULL) + *optional_out = false; + if (generation_out != NULL) + *generation_out = 0; + if (slot < lengthof(route_seam.capability_peers)) { + route_seam.capability_peers[slot] = peer_id; + route_seam.capability_required[slot] = required_bits; + route_seam.capability_optional[slot] = optional_bits; + } + if (!route_seam.capability_ok || generation_out == NULL) + return false; + if (optional_out != NULL) + *optional_out = true; + *generation_out = route_test_capability_generation(peer_id); + return *generation_out != 0; +} + +bool +cluster_semantic_activation_peer_open_matches(const ClusterSemanticAdmissionToken *token, + int32 authenticated_peer, + uint32 required_capabilities, + uint32 sampled_generation) +{ + int slot = route_seam.peer_open_calls++; + + if (slot < lengthof(route_seam.peer_open_peers)) { + route_seam.peer_open_peers[slot] = authenticated_peer; + route_seam.peer_open_required[slot] = required_capabilities; + route_seam.peer_open_generation[slot] = sampled_generation; + } + return route_seam.peer_open_ok && token != NULL && token->entered + && required_capabilities == UT_R4_REQUIRED_CAPABILITIES + && sampled_generation == route_test_capability_generation(authenticated_peer); +} + +bool +cluster_pcm_lock_r4_route_snapshot(BufferTag tag, PcmAuthoritySnapshot *authority_out, + uint64 *master_authority_generation_out, + SCN *expected_page_scn_out) +{ + route_seam.snapshot_calls++; + route_seam.snapshot_sequence = ++route_seam.sequence; + if (!route_seam.snapshot_ok || authority_out == NULL + || master_authority_generation_out == NULL || expected_page_scn_out == NULL) + return false; + memset(authority_out, 0, sizeof(*authority_out)); + authority_out->state = PCM_STATE_S; + authority_out->x_holder_node = -1; + authority_out->pending_x_requester_node = -1; + authority_out->transition_count = UT_MASTER_TRANSITION; + authority_out->master_holder.node_id = UT_HOLDER_NODE; + authority_out->s_holders_bitmap = UINT32_C(1) << UT_HOLDER_NODE; + *master_authority_generation_out = UT_MASTER_GENERATION; + *expected_page_scn_out = UT_EXPECTED_PAGE_SCN; + (void)tag; + return true; +} + +GcsBlockR4RouteArmResult +cluster_gcs_block_dedup_r4_route_arm_or_match( + int worker_id, const GcsBlockR4RouteIdentity *identity, uint8 transition_id, + const ClusterR4CrRouteProof *fresh_proof, uint32 requester_lifetime_hint_ms, + bool lifetime_hint_trusted, GcsBlockR4RouteRecord *record_out) +{ + (void)worker_id; + route_seam.arm_calls++; + route_seam.arm_sequence = ++route_seam.sequence; + if (identity != NULL) + route_seam.armed_identity = *identity; + if (fresh_proof != NULL) + route_seam.armed_proof = *fresh_proof; + route_seam.armed_transition = transition_id; + route_seam.armed_lifetime_hint_ms = requester_lifetime_hint_ms; + route_seam.armed_lifetime_hint_trusted = lifetime_hint_trusted; + if ((route_seam.arm_result == GCS_BLOCK_R4_ROUTE_ARM_NEW + || route_seam.arm_result == GCS_BLOCK_R4_ROUTE_ARM_REPLAY) + && fresh_proof != NULL && record_out != NULL) { + memset(record_out, 0, sizeof(*record_out)); + record_out->proof = *fresh_proof; + record_out->state = GCS_BLOCK_R4_ROUTE_ROUTING; + } + return route_seam.arm_result; +} + +GcsBlockR4RouteSendResult +cluster_gcs_block_dedup_r4_route_finish_send( + int worker_id, const GcsBlockR4RouteIdentity *identity, uint8 transition_id, + const ClusterR4CrRouteProof *armed_proof, bool outbound_admitted) +{ + (void)worker_id; + route_seam.finish_calls++; + route_seam.finish_sequence = ++route_seam.sequence; + if (identity != NULL) + route_seam.finished_identity = *identity; + if (armed_proof != NULL) + route_seam.finished_proof = *armed_proof; + route_seam.finished_transition = transition_id; + route_seam.finished_outbound_admitted = outbound_admitted; + return route_seam.finish_result; +} + +bool +cluster_lms_outbound_enqueue_cap_bound(int worker_id, uint8 msg_type, uint32 dest_node_id, + const void *payload, uint16 payload_len, + uint32 required_capability, + uint32 connection_generation) +{ + route_seam.enqueue_calls++; + route_seam.enqueue_sequence = ++route_seam.sequence; + route_seam.enqueue_worker = worker_id; + route_seam.enqueue_msg_type = msg_type; + route_seam.enqueue_dest = dest_node_id; + route_seam.enqueue_payload_len = payload_len; + route_seam.enqueue_required_capability = required_capability; + route_seam.enqueue_connection_generation = connection_generation; + if (payload != NULL && payload_len <= sizeof(route_seam.enqueue_payload)) + memcpy(route_seam.enqueue_payload, payload, payload_len); + return route_seam.enqueue_ok; +} + +bool +cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + int worker_id, uint32 dest_node_id, const GcsBlockReplyHeader *header, + uint32 required_capability, uint32 connection_generation) +{ + route_seam.refusal_enqueue_calls++; + route_seam.refusal_enqueue_sequence = ++route_seam.sequence; + route_seam.refusal_enqueue_worker = worker_id; + route_seam.refusal_enqueue_dest = dest_node_id; + route_seam.refusal_enqueue_required_capability = required_capability; + route_seam.refusal_enqueue_connection_generation = connection_generation; + if (header != NULL) + route_seam.refusal_header = *header; + return route_seam.refusal_enqueue_ok; +} + +static void +route_assert_refusal(GcsBlockReplyStatus status, uint64 page_lsn) +{ + int i; + + UT_ASSERT_EQ(route_seam.refusal_enqueue_calls, 1); + UT_ASSERT_EQ(route_seam.refusal_enqueue_worker, 0); + UT_ASSERT_EQ(route_seam.refusal_enqueue_dest, UT_REQUESTER_NODE); + UT_ASSERT_EQ(route_seam.refusal_enqueue_required_capability, UT_R4_REQUIRED_CAPABILITIES); + UT_ASSERT_EQ(route_seam.refusal_enqueue_connection_generation, + UT_REQUESTER_CAPABILITY_GENERATION); + UT_ASSERT_EQ(route_seam.refusal_header.request_id, UT_REQUEST_ID); + UT_ASSERT_EQ(route_seam.refusal_header.page_lsn, page_lsn); + UT_ASSERT_EQ(route_seam.refusal_header.checksum, 0); + UT_ASSERT_EQ(route_seam.refusal_header.sender_node, UT_MASTER_NODE); + UT_ASSERT_EQ(route_seam.refusal_header.requester_backend_id, UT_REQUESTER_BACKEND); + UT_ASSERT_EQ(route_seam.refusal_header.transition_id, PCM_TRANS_N_TO_S); + UT_ASSERT_EQ(route_seam.refusal_header.status, status); + UT_ASSERT_EQ(GcsBlockReplyHeaderGetForwardingMasterNode(&route_seam.refusal_header), + GCS_BLOCK_REPLY_NO_FORWARDING_MASTER); + for (i = 0; i < (int)sizeof(route_seam.refusal_header.reserved_0); i++) + UT_ASSERT_EQ(route_seam.refusal_header.reserved_0[i], 0); +} + +bool +cluster_lms_cr_submit_r4(const ClusterR4CrForwardPayload *forward) +{ + route_seam.holder_submit_calls++; + if (forward != NULL) + route_seam.submitted_forward = *forward; + return route_seam.holder_submit_ok; +} + static PcmAuthoritySnapshot route_snapshot(PcmState state) { @@ -240,10 +812,578 @@ UT_TEST(test_unknown_reason_fails_closed) CLUSTER_CR_BUILD_FAIL_CLOSED); } +UT_TEST(test_d3_result_reason_mapping_is_closed) +{ + static const GcsBlockReplyStatus expected[18] = { + GCS_BLOCK_REPLY_R4_DENIED, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, + GCS_BLOCK_REPLY_R4_DENIED, + GCS_BLOCK_REPLY_R4_DENIED, + GCS_BLOCK_REPLY_R4_DENIED, + GCS_BLOCK_REPLY_R4_DENIED, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, + GCS_BLOCK_REPLY_R4_DENIED, + GCS_BLOCK_REPLY_R4_DENIED, + GCS_BLOCK_REPLY_R4_DENIED + }; + int reason; + + for (reason = CLUSTER_CR_BUILD_TARGET_DISABLED; reason <= CLUSTER_CR_BUILD_PROTOCOL; + reason++) { + ClusterCrBuildResult result + = cluster_cr_build_result_for_reason((ClusterCrBuildReason)reason); + GcsBlockReplyStatus status = GCS_BLOCK_REPLY_GRANTED; + + UT_ASSERT(cluster_gcs_block_test_r4_refusal_status( + result, (ClusterCrBuildReason)reason, false, &status)); + UT_ASSERT_EQ(status, expected[reason]); + } + { + GcsBlockReplyStatus status = GCS_BLOCK_REPLY_GRANTED; + + UT_ASSERT(!cluster_gcs_block_test_r4_refusal_status( + CLUSTER_CR_BUILD_FULL, CLUSTER_CR_BUILD_NONE, true, &status)); + UT_ASSERT(cluster_gcs_block_test_r4_refusal_status( + CLUSTER_CR_BUILD_FULL, CLUSTER_CR_BUILD_NONE, false, &status)); + UT_ASSERT_EQ(status, GCS_BLOCK_REPLY_R4_DENIED); + UT_ASSERT(cluster_gcs_block_test_r4_refusal_status( + CLUSTER_CR_BUILD_FAIL_CLOSED, CLUSTER_CR_BUILD_HOLDER_MOVED, false, &status)); + UT_ASSERT_EQ(status, GCS_BLOCK_REPLY_R4_DENIED); + UT_ASSERT(cluster_gcs_block_test_r4_refusal_status( + (ClusterCrBuildResult)99, (ClusterCrBuildReason)99, false, &status)); + UT_ASSERT_EQ(status, GCS_BLOCK_REPLY_R4_DENIED); + } +} + +UT_TEST(test_r4_refusal_decoder_requires_exact_domain_identity_and_zero_body) +{ + typedef struct TestR4Reply { + GcsBlockReplyHeader header; + char block_data[GCS_BLOCK_DATA_SIZE]; + } TestR4Reply; + TestR4Reply reply; + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REPLY, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(reply)); + + memset(&reply, 0, sizeof(reply)); + reply.header.request_id = UT_REQUEST_ID; + reply.header.epoch = UT_FORMATION_EPOCH; + reply.header.sender_node = UT_REQUESTER_NODE; + reply.header.requester_backend_id = UT_REQUESTER_BACKEND; + reply.header.transition_id = PCM_TRANS_N_TO_S; + reply.header.status = GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED; + reply.header.page_lsn = 1; + GcsBlockReplyHeaderSetForwardingMasterNode( + &reply.header, GCS_BLOCK_REPLY_NO_FORWARDING_MASTER); + reply.header.checksum = cluster_gcs_block_compute_checksum(reply.block_data); + UT_ASSERT(cluster_gcs_block_test_decode_r4_reply( + &env, &reply, UT_REQUEST_ID, UT_FORMATION_EPOCH, UT_REQUESTER_BACKEND, + PCM_TRANS_N_TO_S, UT_REQUESTER_NODE)); + + reply.header.status = GCS_BLOCK_REPLY_DENIED_PENDING_X; + UT_ASSERT(!cluster_gcs_block_test_decode_r4_reply( + &env, &reply, UT_REQUEST_ID, UT_FORMATION_EPOCH, UT_REQUESTER_BACKEND, + PCM_TRANS_N_TO_S, UT_REQUESTER_NODE)); + reply.header.status = GCS_BLOCK_REPLY_R4_DENIED; + reply.header.page_lsn = 1; + UT_ASSERT(!cluster_gcs_block_test_decode_r4_reply( + &env, &reply, UT_REQUEST_ID, UT_FORMATION_EPOCH, UT_REQUESTER_BACKEND, + PCM_TRANS_N_TO_S, UT_REQUESTER_NODE)); + reply.header.page_lsn = 0; + reply.header.reserved_0[0] = 1; + UT_ASSERT(!cluster_gcs_block_test_decode_r4_reply( + &env, &reply, UT_REQUEST_ID, UT_FORMATION_EPOCH, UT_REQUESTER_BACKEND, + PCM_TRANS_N_TO_S, UT_REQUESTER_NODE)); + reply.header.reserved_0[0] = 0; + reply.block_data[0] = 1; + reply.header.checksum = cluster_gcs_block_compute_checksum(reply.block_data); + UT_ASSERT(!cluster_gcs_block_test_decode_r4_reply( + &env, &reply, UT_REQUEST_ID, UT_FORMATION_EPOCH, UT_REQUESTER_BACKEND, + PCM_TRANS_N_TO_S, UT_REQUESTER_NODE)); +} + +/* Removing the real request80 branch, taking a second TARGET admission, + * re-snapshotting PCM, encoding fresh rather than stored proof bytes, using an + * unbound enqueue, or failing to feed the exact forward96 branch all break + * this one request -> holder behavior test. */ +UT_TEST(test_request80_routes_stored_proof_to_real_forward96_handler) +{ + static const uint8 expected_extension[32] = { + 0x01, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x22, 0x22, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterICEnvelope request_env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(request)); + ClusterR4CrForwardPayload forwarded; + ClusterICEnvelope forward_env; + + route_seam_reset(); + UT_ASSERT(cluster_gcs_block_test_r4_request80(&request_env, &request)); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT_EQ(route_seam.capability_calls, 2); + UT_ASSERT_EQ(route_seam.peer_open_calls, 2); + UT_ASSERT_EQ(route_seam.capability_peers[0], UT_REQUESTER_NODE); + UT_ASSERT_EQ(route_seam.capability_peers[1], UT_HOLDER_NODE); + UT_ASSERT_EQ(route_seam.capability_required[0], UT_R4_REQUIRED_CAPABILITIES); + UT_ASSERT_EQ(route_seam.capability_required[1], UT_R4_REQUIRED_CAPABILITIES); + UT_ASSERT_EQ(route_seam.snapshot_calls, 1); + UT_ASSERT_EQ(route_seam.arm_calls, 1); + UT_ASSERT_EQ(route_seam.armed_lifetime_hint_ms, 1000); + UT_ASSERT(route_seam.armed_lifetime_hint_trusted); + UT_ASSERT_EQ(route_seam.armed_identity.legacy_key.origin_node_id, UT_REQUESTER_NODE); + UT_ASSERT_EQ(route_seam.armed_identity.legacy_key.requester_backend_id, + UT_REQUESTER_BACKEND); + UT_ASSERT_EQ(route_seam.armed_identity.legacy_key.request_id, UT_REQUEST_ID); + UT_ASSERT_EQ(route_seam.armed_identity.legacy_key.cluster_epoch, UT_FORMATION_EPOCH); + UT_ASSERT_EQ(route_seam.armed_identity.read_scn, UT_READ_SCN); + UT_ASSERT_EQ(route_seam.armed_identity.activation_generation, UT_ACTIVATION_GENERATION); + UT_ASSERT_EQ(route_seam.armed_proof.formation_epoch, UT_FORMATION_EPOCH); + UT_ASSERT_EQ(route_seam.armed_proof.activation_generation, UT_ACTIVATION_GENERATION); + UT_ASSERT_EQ(route_seam.armed_proof.master_authority_generation, UT_MASTER_GENERATION); + UT_ASSERT_EQ(route_seam.armed_proof.master_resource_transition_count, UT_MASTER_TRANSITION); + UT_ASSERT_EQ(route_seam.armed_proof.expected_page_scn, UT_EXPECTED_PAGE_SCN); + UT_ASSERT_EQ(route_seam.armed_proof.real_master_node, UT_MASTER_NODE); + UT_ASSERT_EQ(route_seam.armed_proof.selected_holder_node, UT_HOLDER_NODE); + UT_ASSERT_EQ(route_seam.enqueue_calls, 1); + UT_ASSERT_EQ(route_seam.finish_calls, 1); + UT_ASSERT_EQ(route_seam.refusal_enqueue_calls, 0); + UT_ASSERT(route_seam.finished_outbound_admitted); + UT_ASSERT_EQ(memcmp(&route_seam.finished_identity, &route_seam.armed_identity, + sizeof(route_seam.armed_identity)), 0); + UT_ASSERT_EQ(memcmp(&route_seam.finished_proof, &route_seam.armed_proof, + sizeof(route_seam.armed_proof)), 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 1); + UT_ASSERT(route_seam.snapshot_sequence < route_seam.arm_sequence); + UT_ASSERT(route_seam.arm_sequence < route_seam.enqueue_sequence); + UT_ASSERT(route_seam.enqueue_sequence < route_seam.finish_sequence); + UT_ASSERT(route_seam.finish_sequence < route_seam.recheck_sequence); + UT_ASSERT(route_seam.recheck_sequence < route_seam.leave_sequence); + + UT_ASSERT_EQ(route_seam.enqueue_msg_type, PGRAC_IC_MSG_GCS_BLOCK_FORWARD); + UT_ASSERT_EQ(route_seam.enqueue_dest, UT_HOLDER_NODE); + UT_ASSERT_EQ(route_seam.enqueue_payload_len, sizeof(ClusterR4CrForwardPayload)); + UT_ASSERT_EQ(route_seam.enqueue_required_capability, UT_R4_REQUIRED_CAPABILITIES); + UT_ASSERT_EQ(route_seam.enqueue_connection_generation, UT_HOLDER_CAPABILITY_GENERATION); + memcpy(&forwarded, route_seam.enqueue_payload, sizeof(forwarded)); + UT_ASSERT_EQ(forwarded.base.request_id, UT_REQUEST_ID); + UT_ASSERT_EQ(forwarded.base.epoch, UT_FORMATION_EPOCH); + UT_ASSERT_EQ(memcmp(&forwarded.base.tag, &request.base.tag, sizeof(BufferTag)), 0); + UT_ASSERT_EQ(forwarded.base.original_requester_node, UT_REQUESTER_NODE); + UT_ASSERT_EQ(forwarded.base.requester_backend_id, UT_REQUESTER_BACKEND); + UT_ASSERT_EQ(forwarded.base.master_node, UT_MASTER_NODE); + UT_ASSERT_EQ(forwarded.base.transition_id, PCM_TRANS_N_TO_S); + UT_ASSERT_EQ(forwarded.base.expected_pi_watermark_scn_bytes[0], 0x34); + UT_ASSERT_EQ(forwarded.base.expected_pi_watermark_scn_bytes[1], 0x12); + UT_ASSERT_EQ(forwarded.base.reserved_0[4], 1); + UT_ASSERT_EQ(memcmp(&forwarded.extension, expected_extension, sizeof(expected_extension)), 0); + /* Absolute bytes 76..83 are the master-resource transition count. */ + UT_ASSERT_EQ(memcmp(((const uint8 *)&forwarded) + 76, expected_extension + 12, 8), 0); + + forward_env = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_FORWARD, UT_MASTER_NODE, + UT_HOLDER_NODE, sizeof(forwarded)); + route_seam_reset(); + UT_ASSERT(cluster_gcs_block_test_r4_forward96(&forward_env, &forwarded)); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT_EQ(route_seam.capability_calls, 1); + UT_ASSERT_EQ(route_seam.peer_open_calls, 1); + UT_ASSERT_EQ(route_seam.capability_peers[0], UT_MASTER_NODE); + UT_ASSERT_EQ(route_seam.capability_required[0], UT_R4_REQUIRED_CAPABILITIES); + UT_ASSERT_EQ(route_seam.snapshot_calls, 0); + UT_ASSERT_EQ(route_seam.arm_calls, 0); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 1); + UT_ASSERT_EQ(memcmp(&route_seam.submitted_forward, &forwarded, sizeof(forwarded)), 0); +} + +/* These hooks are exact-extended-shape classifiers: an exact R4 frame is + * consumed even when its capability gate refuses it, while legacy and + * malformed lengths remain for the outer dispatcher/drop path. */ +UT_TEST(test_r4_try_handlers_consume_only_exact_extended_lengths) +{ + uint8 bytes[sizeof(ClusterR4CrForwardPayload)]; + ClusterICEnvelope env; + + memset(bytes, 0, sizeof(bytes)); + route_seam_reset(); + route_seam.capability_ok = false; + + env = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(ClusterR4CrRequestPayload)); + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, bytes)); + UT_ASSERT_EQ(route_seam.refusal_enqueue_calls, 0); + env.payload_length = sizeof(GcsBlockRequestPayload); + UT_ASSERT(!cluster_gcs_block_test_r4_request80(&env, bytes)); + env.payload_length = sizeof(ClusterR4CrRequestPayload) - 1; + UT_ASSERT(!cluster_gcs_block_test_r4_request80(&env, bytes)); + env.payload_length = sizeof(ClusterR4CrRequestPayload) + 1; + UT_ASSERT(!cluster_gcs_block_test_r4_request80(&env, bytes)); + + env = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_FORWARD, UT_MASTER_NODE, UT_HOLDER_NODE, + sizeof(ClusterR4CrForwardPayload)); + UT_ASSERT(cluster_gcs_block_test_r4_forward96(&env, bytes)); + env.payload_length = sizeof(GcsBlockForwardPayload); + UT_ASSERT(!cluster_gcs_block_test_r4_forward96(&env, bytes)); + env.payload_length = sizeof(ClusterR4CrForwardPayload) - 1; + UT_ASSERT(!cluster_gcs_block_test_r4_forward96(&env, bytes)); + env.payload_length = sizeof(ClusterR4CrForwardPayload) + 1; + UT_ASSERT(!cluster_gcs_block_test_r4_forward96(&env, bytes)); +} + +/* A negotiated-capability sample that cannot be joined to the same committed + * OPEN generation is consumed fail-closed before PCM/dedup/ring/holder state. + * Deleting either matcher call or moving mutation ahead of it breaks this. */ +UT_TEST(test_r4_same_open_mismatch_has_zero_route_or_holder_mutation) +{ + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterR4CrForwardPayload forward = route_test_forward96(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(request)); + + route_seam_reset(); + route_seam.peer_open_ok = false; + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, &request)); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.peer_open_calls, 1); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT_EQ(route_seam.snapshot_calls, 0); + UT_ASSERT_EQ(route_seam.arm_calls, 0); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.finish_calls, 0); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 1); + route_assert_refusal(GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, 0); + + env = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_FORWARD, UT_MASTER_NODE, UT_HOLDER_NODE, + sizeof(forward)); + route_seam_reset(); + route_seam.peer_open_ok = false; + UT_ASSERT(cluster_gcs_block_test_r4_forward96(&env, &forward)); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.peer_open_calls, 1); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT_EQ(route_seam.snapshot_calls, 0); + UT_ASSERT_EQ(route_seam.arm_calls, 0); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.finish_calls, 0); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 0); + UT_ASSERT_EQ(route_seam.refusal_enqueue_calls, 0); +} + +/* Once the requester capability generation has been sampled, admission + * refusal is an authenticated pre-token status-25 outcome. It must not + * acquire/recheck/leave a token or touch PCM, dedup, forwarding, or holder + * state. */ +UT_TEST(test_request80_admission_refusals_publish_without_token) +{ + static const ClusterSemanticAdmissionResult refusals[] = { + CLUSTER_SEMANTIC_ADMISSION_TARGET_DISABLED, + CLUSTER_SEMANTIC_ADMISSION_CLOSED, + CLUSTER_SEMANTIC_ADMISSION_GENERATION_CHANGED + }; + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(request)); + int i; + + for (i = 0; i < lengthof(refusals); i++) { + route_seam_reset(); + route_seam.admission_result = refusals[i]; + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, &request)); + UT_ASSERT_EQ(route_seam.capability_calls, 1); + UT_ASSERT_EQ(route_seam.capability_peers[0], UT_REQUESTER_NODE); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.peer_open_calls, 0); + UT_ASSERT_EQ(route_seam.leave_calls, 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 0); + UT_ASSERT_EQ(route_seam.snapshot_calls, 0); + UT_ASSERT_EQ(route_seam.arm_calls, 0); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.finish_calls, 0); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); + route_assert_refusal(GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, 0); + } +} + +UT_TEST(test_request80_sender_mismatch_is_consumed_without_route_mutation) +{ + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(request)); + + request.base.sender_node = UT_HOLDER_NODE; + route_seam_reset(); + UT_ASSERT_EQ(env.payload_length, 80); + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, &request)); + UT_ASSERT_EQ(route_seam.capability_calls, 1); + UT_ASSERT_EQ(route_seam.capability_peers[0], UT_REQUESTER_NODE); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.peer_open_calls, 1); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT_EQ(route_seam.snapshot_calls, 0); + UT_ASSERT_EQ(route_seam.arm_calls, 0); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.finish_calls, 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 1); + route_assert_refusal(GCS_BLOCK_REPLY_R4_DENIED, 0); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); +} + +UT_TEST(test_request80_reserved_extension_is_consumed_without_route_mutation) +{ + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(request)); + + request.extension.reserved[0] = 1; + route_seam_reset(); + UT_ASSERT_EQ(env.payload_length, 80); + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, &request)); + UT_ASSERT_EQ(route_seam.capability_calls, 1); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.peer_open_calls, 1); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT_EQ(route_seam.snapshot_calls, 0); + UT_ASSERT_EQ(route_seam.arm_calls, 0); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.finish_calls, 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 1); + route_assert_refusal(GCS_BLOCK_REPLY_R4_DENIED, 0); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); +} + +UT_TEST(test_request80_epoch_mismatch_is_consumed_without_route_mutation) +{ + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(request)); + + request.base.epoch = UT_FORMATION_EPOCH + 1; + route_seam_reset(); + UT_ASSERT_EQ(env.payload_length, 80); + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, &request)); + UT_ASSERT_EQ(route_seam.capability_calls, 1); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.peer_open_calls, 1); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT_EQ(route_seam.snapshot_calls, 0); + UT_ASSERT_EQ(route_seam.arm_calls, 0); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.finish_calls, 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 1); + route_assert_refusal(GCS_BLOCK_REPLY_R4_DENIED, 0); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); +} + +UT_TEST(test_request80_wrong_master_is_consumed_before_snapshot) +{ + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(request)); + + route_seam_reset(); + route_seam.lookup_master_node = 0; + UT_ASSERT_EQ(env.payload_length, 80); + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, &request)); + UT_ASSERT_EQ(route_seam.capability_calls, 1); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.peer_open_calls, 1); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT_EQ(route_seam.snapshot_calls, 0); + UT_ASSERT_EQ(route_seam.arm_calls, 0); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.finish_calls, 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 1); + route_assert_refusal(GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, 1); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); +} + +UT_TEST(test_request80_snapshot_failure_stops_before_route_arm) +{ + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(request)); + + route_seam_reset(); + route_seam.snapshot_ok = false; + UT_ASSERT_EQ(env.payload_length, 80); + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, &request)); + UT_ASSERT_EQ(route_seam.capability_calls, 1); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.peer_open_calls, 1); + UT_ASSERT_EQ(route_seam.snapshot_calls, 1); + UT_ASSERT_EQ(route_seam.arm_calls, 0); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.finish_calls, 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 1); + route_assert_refusal(GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, 0); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT(route_seam.snapshot_sequence < route_seam.leave_sequence); +} + +UT_TEST(test_request80_arm_full_stops_before_publication) +{ + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(request)); + + route_seam_reset(); + route_seam.arm_result = GCS_BLOCK_R4_ROUTE_ARM_FULL; + UT_ASSERT_EQ(env.payload_length, 80); + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, &request)); + UT_ASSERT_EQ(route_seam.capability_calls, 2); + UT_ASSERT_EQ(route_seam.peer_open_calls, 2); + UT_ASSERT_EQ(route_seam.snapshot_calls, 1); + UT_ASSERT_EQ(route_seam.arm_calls, 1); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.finish_calls, 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 1); + route_assert_refusal(GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, 0); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT(route_seam.snapshot_sequence < route_seam.arm_sequence); + UT_ASSERT(route_seam.arm_sequence < route_seam.leave_sequence); +} + +UT_TEST(test_request80_enqueue_refusal_finishes_unadmitted_once) +{ + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(request)); + + route_seam_reset(); + route_seam.enqueue_ok = false; + route_seam.finish_result = GCS_BLOCK_R4_ROUTE_SEND_RETRYABLE; + UT_ASSERT_EQ(env.payload_length, 80); + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, &request)); + UT_ASSERT_EQ(route_seam.capability_calls, 2); + UT_ASSERT_EQ(route_seam.peer_open_calls, 2); + UT_ASSERT_EQ(route_seam.snapshot_calls, 1); + UT_ASSERT_EQ(route_seam.arm_calls, 1); + UT_ASSERT_EQ(route_seam.enqueue_calls, 1); + UT_ASSERT_EQ(route_seam.finish_calls, 1); + UT_ASSERT(!route_seam.finished_outbound_admitted); + UT_ASSERT_EQ(route_seam.recheck_calls, 1); + route_assert_refusal(GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, 0); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT(route_seam.arm_sequence < route_seam.enqueue_sequence); + UT_ASSERT(route_seam.enqueue_sequence < route_seam.finish_sequence); + UT_ASSERT(route_seam.finish_sequence < route_seam.recheck_sequence); + UT_ASSERT(route_seam.recheck_sequence < route_seam.refusal_enqueue_sequence); + UT_ASSERT(route_seam.refusal_enqueue_sequence < route_seam.leave_sequence); + UT_ASSERT(route_seam.recheck_sequence < route_seam.leave_sequence); +} + +UT_TEST(test_request80_final_recheck_failure_leaves_after_one_publication) +{ + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, UT_MASTER_NODE, + sizeof(request)); + + route_seam_reset(); + route_seam.recheck_ok = false; + UT_ASSERT_EQ(env.payload_length, 80); + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, &request)); + UT_ASSERT_EQ(route_seam.capability_calls, 2); + UT_ASSERT_EQ(route_seam.peer_open_calls, 2); + UT_ASSERT_EQ(route_seam.snapshot_calls, 1); + UT_ASSERT_EQ(route_seam.arm_calls, 1); + UT_ASSERT_EQ(route_seam.enqueue_calls, 1); + UT_ASSERT_EQ(route_seam.finish_calls, 1); + UT_ASSERT(route_seam.finished_outbound_admitted); + UT_ASSERT_EQ(route_seam.recheck_calls, 1); + route_assert_refusal(GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, 0); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); + UT_ASSERT_EQ(route_seam.leave_calls, 1); + UT_ASSERT(route_seam.enqueue_sequence < route_seam.finish_sequence); + UT_ASSERT(route_seam.finish_sequence < route_seam.recheck_sequence); + UT_ASSERT(route_seam.recheck_sequence < route_seam.refusal_enqueue_sequence); + UT_ASSERT(route_seam.refusal_enqueue_sequence < route_seam.leave_sequence); + UT_ASSERT(route_seam.recheck_sequence < route_seam.leave_sequence); +} + +UT_TEST(test_forward96_master_mismatch_is_consumed_without_holder_submit) +{ + ClusterR4CrForwardPayload forward = route_test_forward96(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_FORWARD, UT_MASTER_NODE, UT_HOLDER_NODE, + sizeof(forward)); + + forward.base.master_node = UT_HOLDER_NODE; + route_seam_reset(); + UT_ASSERT_EQ(env.payload_length, 96); + UT_ASSERT(cluster_gcs_block_test_r4_forward96(&env, &forward)); + UT_ASSERT_EQ(route_seam.capability_calls, 1); + UT_ASSERT_EQ(route_seam.capability_peers[0], UT_MASTER_NODE); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.peer_open_calls, 1); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 0); + UT_ASSERT_EQ(route_seam.snapshot_calls, 0); + UT_ASSERT_EQ(route_seam.arm_calls, 0); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.finish_calls, 0); + UT_ASSERT_EQ(route_seam.leave_calls, 1); +} + +UT_TEST(test_forward96_proof_epoch_mismatch_is_consumed_without_holder_submit) +{ + ClusterR4CrForwardPayload forward = route_test_forward96(); + ClusterICEnvelope env + = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_FORWARD, UT_MASTER_NODE, UT_HOLDER_NODE, + sizeof(forward)); + + /* Corrupt the proof's embedded formation-epoch half while keeping the base + * and authenticated envelope epochs valid. */ + forward.extension.kind.cr.master_authority_generation_le[4] + = (uint8)(UT_FORMATION_EPOCH + 1); + route_seam_reset(); + UT_ASSERT_EQ(env.payload_length, 96); + UT_ASSERT(cluster_gcs_block_test_r4_forward96(&env, &forward)); + UT_ASSERT_EQ(route_seam.capability_calls, 1); + UT_ASSERT_EQ(route_seam.capability_peers[0], UT_MASTER_NODE); + UT_ASSERT_EQ(route_seam.enter_calls, 1); + UT_ASSERT_EQ(route_seam.peer_open_calls, 1); + UT_ASSERT_EQ(route_seam.holder_submit_calls, 0); + UT_ASSERT_EQ(route_seam.recheck_calls, 0); + UT_ASSERT_EQ(route_seam.snapshot_calls, 0); + UT_ASSERT_EQ(route_seam.arm_calls, 0); + UT_ASSERT_EQ(route_seam.enqueue_calls, 0); + UT_ASSERT_EQ(route_seam.finish_calls, 0); + UT_ASSERT_EQ(route_seam.leave_calls, 1); +} + int main(void) { - UT_PLAN(51); + UT_PLAN(67); UT_RUN(test_01_null_authority_is_protocol); UT_RUN(test_02_null_output_is_protocol); UT_RUN(test_03_canonical_n_has_no_holder); @@ -295,6 +1435,22 @@ main(void) UT_RUN(test_reason_polarity_16); UT_RUN(test_reason_polarity_17); UT_RUN(test_unknown_reason_fails_closed); + UT_RUN(test_d3_result_reason_mapping_is_closed); + UT_RUN(test_r4_refusal_decoder_requires_exact_domain_identity_and_zero_body); + UT_RUN(test_request80_routes_stored_proof_to_real_forward96_handler); + UT_RUN(test_r4_try_handlers_consume_only_exact_extended_lengths); + UT_RUN(test_r4_same_open_mismatch_has_zero_route_or_holder_mutation); + UT_RUN(test_request80_admission_refusals_publish_without_token); + UT_RUN(test_request80_sender_mismatch_is_consumed_without_route_mutation); + UT_RUN(test_request80_reserved_extension_is_consumed_without_route_mutation); + UT_RUN(test_request80_epoch_mismatch_is_consumed_without_route_mutation); + UT_RUN(test_request80_wrong_master_is_consumed_before_snapshot); + UT_RUN(test_request80_snapshot_failure_stops_before_route_arm); + UT_RUN(test_request80_arm_full_stops_before_publication); + UT_RUN(test_request80_enqueue_refusal_finishes_unadmitted_once); + UT_RUN(test_request80_final_recheck_failure_leaves_after_one_publication); + UT_RUN(test_forward96_master_mismatch_is_consumed_without_holder_submit); + UT_RUN(test_forward96_proof_epoch_mismatch_is_consumed_without_holder_submit); UT_DONE(); return ut_failed_count == 0 ? 0 : 1; } diff --git a/src/test/cluster_unit/test_cluster_r4_wire_codec.c b/src/test/cluster_unit/test_cluster_r4_wire_codec.c index 428f21b021a..5149316a7c9 100644 --- a/src/test/cluster_unit/test_cluster_r4_wire_codec.c +++ b/src/test/cluster_unit/test_cluster_r4_wire_codec.c @@ -604,10 +604,39 @@ DEFINE_WIRE_TEST(87) #define RUN_WIRE_TEST(n) UT_RUN(test_wire_vector_##n) +UT_TEST(test_r4_reply_status_abi_tail_is_exact) +{ + UT_ASSERT_EQ(GCS_BLOCK_REPLY_R4_CR_FULL, 21); + UT_ASSERT_EQ(GCS_BLOCK_REPLY_R4_TX_RESOLVE_RESULT, 22); + UT_ASSERT_EQ(GCS_BLOCK_REPLY_R4_MULTI_RESOLVE_RESULT, 23); + UT_ASSERT_EQ(GCS_BLOCK_REPLY_R4_UNDO_DATA_RESULT, 24); + UT_ASSERT_EQ(GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, 25); + UT_ASSERT_EQ(GCS_BLOCK_REPLY_R4_DENIED, 26); +} + +UT_TEST(test_r4_and_legacy_reply_status_domains_are_disjoint) +{ + int status; + + for (status = 0; status <= 20; status++) { + UT_ASSERT(GcsBlockReplyStatusIsLegacy((GcsBlockReplyStatus)status)); + UT_ASSERT(!GcsBlockReplyStatusIsR4((GcsBlockReplyStatus)status)); + UT_ASSERT(!GcsBlockReplyStatusIsR4Refusal((GcsBlockReplyStatus)status)); + } + for (status = 21; status <= 26; status++) { + UT_ASSERT(!GcsBlockReplyStatusIsLegacy((GcsBlockReplyStatus)status)); + UT_ASSERT(GcsBlockReplyStatusIsR4((GcsBlockReplyStatus)status)); + UT_ASSERT_EQ(GcsBlockReplyStatusIsR4Refusal((GcsBlockReplyStatus)status), + status >= 25); + } + UT_ASSERT(!GcsBlockReplyStatusIsLegacy((GcsBlockReplyStatus)-1)); + UT_ASSERT(!GcsBlockReplyStatusIsR4((GcsBlockReplyStatus)27)); +} + int main(void) { - UT_PLAN(88); + UT_PLAN(90); RUN_WIRE_TEST(0); RUN_WIRE_TEST(1); RUN_WIRE_TEST(2); @@ -696,6 +725,8 @@ main(void) RUN_WIRE_TEST(85); RUN_WIRE_TEST(86); RUN_WIRE_TEST(87); + UT_RUN(test_r4_reply_status_abi_tail_is_exact); + UT_RUN(test_r4_and_legacy_reply_status_domains_are_disjoint); UT_DONE(); return ut_failed_count == 0 ? 0 : 1; } diff --git a/src/test/cluster_unit/test_cluster_sf_dep.c b/src/test/cluster_unit/test_cluster_sf_dep.c index 2a7bf2c0e24..3e2ba75b838 100644 --- a/src/test/cluster_unit/test_cluster_sf_dep.c +++ b/src/test/cluster_unit/test_cluster_sf_dep.c @@ -30,6 +30,46 @@ UT_DEFINE_GLOBALS(); +#define TEST_SF_CAP_PEER 7 +#define TEST_SF_SHMEM_BYTES 8192 +#define TEST_R4_REQUIRED_CAPS \ + (PGRAC_IC_HELLO_CAP_SEMANTIC_ACTIVATION_V1 | PGRAC_IC_HELLO_CAP_R4_SYNC_CR_V1) + +typedef union TestSfShmemStorage { + LWLock align; + uint8 bytes[TEST_SF_SHMEM_BYTES]; +} TestSfShmemStorage; + +static TestSfShmemStorage test_sf_shmem; + +ProcessingMode Mode = NormalProcessing; +bool cluster_enabled = true; +bool cluster_smart_fusion = false; +int NBuffers = 0; +int NLocBuffer = 0; + +void * +ShmemInitStruct(const char *name pg_attribute_unused(), Size size, bool *foundPtr) +{ + UT_ASSERT(size <= sizeof(test_sf_shmem.bytes)); + *foundPtr = false; + return test_sf_shmem.bytes; +} + +void +LWLockInitialize(LWLock *lock pg_attribute_unused(), int tranche_id pg_attribute_unused()) +{} + +bool +LWLockAcquire(LWLock *lock pg_attribute_unused(), LWLockMode mode pg_attribute_unused()) +{ + return true; +} + +void +LWLockRelease(LWLock *lock pg_attribute_unused()) +{} + void ExceptionalCondition(const char *conditionName pg_attribute_unused(), const char *fileName pg_attribute_unused(), @@ -38,6 +78,13 @@ ExceptionalCondition(const char *conditionName pg_attribute_unused(), abort(); } +static void +test_sf_cap_store_reset(void) +{ + memset(&test_sf_shmem, 0, sizeof(test_sf_shmem)); + cluster_sf_dep_shmem_init(); +} + UT_TEST(test_vec_set_union_and_clear) { ClusterSfDepVec a; @@ -331,6 +378,128 @@ UT_TEST(test_pcm_x_source_floor_capability_guard_is_generation_exact) &cap, PGRAC_IC_HELLO_CAP_PCM_X_SOURCE_FLOOR_V1, 42)); } +UT_TEST(test_r4_exported_family_sample_requires_both_bits_and_canonicalizes_outputs) +{ + static const struct { + uint32 bits; + uint32 noted_generation; + uint32 required; + uint32 optional; + bool want_supported; + bool want_done; + uint32 want_generation; + } cases[] = { + {PGRAC_IC_HELLO_CAP_SEMANTIC_ACTIVATION_V1, 11, TEST_R4_REQUIRED_CAPS, + PGRAC_IC_HELLO_CAP_GCS_DONE_V1, false, false, 0}, + {PGRAC_IC_HELLO_CAP_R4_SYNC_CR_V1, 12, TEST_R4_REQUIRED_CAPS, + PGRAC_IC_HELLO_CAP_GCS_DONE_V1, false, false, 0}, + {PGRAC_IC_HELLO_CAP_GCS_DONE_V1, 13, TEST_R4_REQUIRED_CAPS, + PGRAC_IC_HELLO_CAP_GCS_DONE_V1, false, false, 0}, + {TEST_R4_REQUIRED_CAPS, 14, TEST_R4_REQUIRED_CAPS, + PGRAC_IC_HELLO_CAP_GCS_DONE_V1, true, false, 14}, + {TEST_R4_REQUIRED_CAPS | PGRAC_IC_HELLO_CAP_GCS_DONE_V1, 15, + TEST_R4_REQUIRED_CAPS, PGRAC_IC_HELLO_CAP_GCS_DONE_V1, true, true, 15}, + {TEST_R4_REQUIRED_CAPS | PGRAC_IC_HELLO_CAP_GCS_DONE_V1, 16, 0, + PGRAC_IC_HELLO_CAP_GCS_DONE_V1, false, false, 0}, + {TEST_R4_REQUIRED_CAPS | PGRAC_IC_HELLO_CAP_GCS_DONE_V1, 17, + TEST_R4_REQUIRED_CAPS, 0, true, false, 17}, + }; + bool done = true; + uint32 generation = UINT32_MAX; + Size i; + + test_sf_cap_store_reset(); + UT_ASSERT(!cluster_sf_peer_capability_family_sample( + TEST_SF_CAP_PEER, TEST_R4_REQUIRED_CAPS, PGRAC_IC_HELLO_CAP_GCS_DONE_V1, &done, + &generation)); + UT_ASSERT(!done); + UT_ASSERT_EQ(generation, (uint32)0); + + for (i = 0; i < lengthof(cases); i++) { + cluster_sf_note_peer_hello_capabilities_gen(TEST_SF_CAP_PEER, cases[i].bits, + cases[i].noted_generation); + done = !cases[i].want_done; + generation = UINT32_MAX; + UT_ASSERT_EQ(cluster_sf_peer_capability_family_sample( + TEST_SF_CAP_PEER, cases[i].required, cases[i].optional, &done, &generation), + cases[i].want_supported); + UT_ASSERT_EQ(done, cases[i].want_done); + UT_ASSERT_EQ(generation, cases[i].want_generation); + } + + done = true; + generation = UINT32_MAX; + UT_ASSERT(!cluster_sf_peer_capability_family_sample( + -1, TEST_R4_REQUIRED_CAPS, PGRAC_IC_HELLO_CAP_GCS_DONE_V1, &done, &generation)); + UT_ASSERT(!done); + UT_ASSERT_EQ(generation, (uint32)0); +} + +UT_TEST(test_r4_exported_family_sample_accepts_registered_generation_zero) +{ + bool done = false; + uint32 generation = UINT32_MAX; + + test_sf_cap_store_reset(); + cluster_sf_note_peer_hello_capabilities_gen( + TEST_SF_CAP_PEER, TEST_R4_REQUIRED_CAPS | PGRAC_IC_HELLO_CAP_GCS_DONE_V1, 0); + UT_ASSERT(cluster_sf_peer_capability_family_sample( + TEST_SF_CAP_PEER, TEST_R4_REQUIRED_CAPS, PGRAC_IC_HELLO_CAP_GCS_DONE_V1, &done, + &generation)); + UT_ASSERT(done); + UT_ASSERT_EQ(generation, (uint32)0); + UT_ASSERT(cluster_sf_peer_capability_generation_matches(TEST_SF_CAP_PEER, + TEST_R4_REQUIRED_CAPS, 0)); +} + +UT_TEST(test_r4_exported_family_sample_reconnect_generation_is_exact) +{ + bool done = false; + uint32 generation = UINT32_MAX; + + test_sf_cap_store_reset(); + cluster_sf_note_peer_hello_capabilities_gen( + TEST_SF_CAP_PEER, TEST_R4_REQUIRED_CAPS | PGRAC_IC_HELLO_CAP_GCS_DONE_V1, 21); + UT_ASSERT(cluster_sf_peer_capability_family_sample( + TEST_SF_CAP_PEER, TEST_R4_REQUIRED_CAPS, PGRAC_IC_HELLO_CAP_GCS_DONE_V1, &done, + &generation)); + UT_ASSERT(done); + UT_ASSERT_EQ(generation, (uint32)21); + UT_ASSERT(cluster_sf_peer_capability_generation_matches(TEST_SF_CAP_PEER, + TEST_R4_REQUIRED_CAPS, 21)); + + cluster_sf_note_peer_hello_capabilities_gen( + TEST_SF_CAP_PEER, TEST_R4_REQUIRED_CAPS | PGRAC_IC_HELLO_CAP_GCS_DONE_V1, 22); + UT_ASSERT(!cluster_sf_peer_capability_generation_matches(TEST_SF_CAP_PEER, + TEST_R4_REQUIRED_CAPS, 21)); + UT_ASSERT(cluster_sf_peer_capability_generation_matches(TEST_SF_CAP_PEER, + TEST_R4_REQUIRED_CAPS, 22)); + done = false; + generation = UINT32_MAX; + UT_ASSERT(cluster_sf_peer_capability_family_sample( + TEST_SF_CAP_PEER, TEST_R4_REQUIRED_CAPS, PGRAC_IC_HELLO_CAP_GCS_DONE_V1, &done, + &generation)); + UT_ASSERT(done); + UT_ASSERT_EQ(generation, (uint32)22); + + cluster_sf_note_peer_hello_capabilities_gen(TEST_SF_CAP_PEER, + PGRAC_IC_HELLO_CAP_GCS_DONE_V1, 23); + done = true; + generation = UINT32_MAX; + UT_ASSERT(!cluster_sf_peer_capability_family_sample( + TEST_SF_CAP_PEER, TEST_R4_REQUIRED_CAPS, PGRAC_IC_HELLO_CAP_GCS_DONE_V1, &done, + &generation)); + UT_ASSERT(!done); + UT_ASSERT_EQ(generation, (uint32)0); + UT_ASSERT(!cluster_sf_peer_capability_generation_matches(TEST_SF_CAP_PEER, + TEST_R4_REQUIRED_CAPS, 23)); + + cluster_sf_note_peer_disconnected_gen(TEST_SF_CAP_PEER, 22); + UT_ASSERT(cluster_sf_peer_supports_gcs_done(TEST_SF_CAP_PEER)); + cluster_sf_note_peer_disconnected_gen(TEST_SF_CAP_PEER, 23); + UT_ASSERT(!cluster_sf_peer_supports_gcs_done(TEST_SF_CAP_PEER)); +} + int main(void) { @@ -347,5 +516,8 @@ main(void) UT_RUN(test_pcm_x_capability_generation_snapshot_is_exact); UT_RUN(test_pcm_x_capability_family_sample_is_record_coherent); UT_RUN(test_pcm_x_source_floor_capability_guard_is_generation_exact); + UT_RUN(test_r4_exported_family_sample_requires_both_bits_and_canonicalizes_outputs); + UT_RUN(test_r4_exported_family_sample_accepts_registered_generation_zero); + UT_RUN(test_r4_exported_family_sample_reconnect_generation_is_exact); UT_DONE(); }