From df00b64934b46d4fefeea15660aab010587de22a Mon Sep 17 00:00:00 2001 From: SqlRush Date: Thu, 17 Sep 2026 18:29:52 +0800 Subject: [PATCH 01/10] fix(cluster): deliver holder CR refusals through outbound ring --- src/backend/cluster/cluster_lms_outbound.c | 15 +- src/test/cluster_unit/Makefile | 19 +- .../cluster_unit/test_cluster_lms_outbound.c | 219 +++++++++++++++++- 3 files changed, 246 insertions(+), 7 deletions(-) diff --git a/src/backend/cluster/cluster_lms_outbound.c b/src/backend/cluster/cluster_lms_outbound.c index 6431cc8e13..062d03d3ab 100644 --- a/src/backend/cluster/cluster_lms_outbound.c +++ b/src/backend/cluster/cluster_lms_outbound.c @@ -680,19 +680,26 @@ cluster_lms_outbound_resource_x_intent_pump(void) static bool lms_outbound_r4_refusal_header_valid(const GcsBlockReplyHeader *header) { + int32 forwarding_master; 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) + || header->transition_id != (uint8)PCM_TRANS_N_TO_S) + return false; + forwarding_master = GcsBlockReplyHeaderGetForwardingMasterNode(header); + if (forwarding_master < GCS_BLOCK_REPLY_NO_FORWARDING_MASTER + || forwarding_master >= CLUSTER_MAX_NODES) 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) + /* A holder replies directly with the real forwarding master retained for + * requester authentication. Only a master-produced retry may redirect; + * neither a forwarded refusal nor a denial carries a page LSN. */ + if (forwarding_master != GCS_BLOCK_REPLY_NO_FORWARDING_MASTER + || 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]. */ diff --git a/src/test/cluster_unit/Makefile b/src/test/cluster_unit/Makefile index 3b584551b3..e9fd6c7753 100644 --- a/src/test/cluster_unit/Makefile +++ b/src/test/cluster_unit/Makefile @@ -3567,8 +3567,25 @@ CLUSTER_LMS_OUTBOUND_O = $(top_builddir)/src/backend/cluster/cluster_lms_outboun cluster_lms_outbound_test.o: $(top_srcdir)/src/backend/cluster/cluster_lms_outbound.c $(CC) $(CFLAGS) $(CPPFLAGS) -DCLUSTER_LMS_OUTBOUND_UNIT_TEST -c $< -o $@ +# Run the real refusal producers and decoder against the real outbound ring. +# Fail extraction if an owner disappears or is duplicated; do not mirror its +# implementation in a fixture or stub the enqueue that joins these components. +test_cluster_r4_refusal_handoff.inc: $(top_srcdir)/src/backend/cluster/cluster_gcs_block.c Makefile + awk '/^typedef enum ClusterGcsBlockReplyDomain/ { emit=1; domain++ } \ + /^typedef struct GcsBlockR4ReplyExpectation/ { emit=1; expectation++ } \ + /^gcs_block_compute_checksum\(/ { print "static uint32"; emit=1; checksum++ } \ + /^#define R4_CR_REQUIRED_HELLO_CAPS/ { cap=1; caps++ } \ + cap { print; if ($$0 !~ /\\$$/) cap=0; next } \ + /^gcs_block_r4_refusal_status_for_build\(/ { print "static bool"; emit=1; status++ } \ + /^gcs_block_decode_r4_reply_payload\(/ { print "static bool"; emit=1; decode++ } \ + /^gcs_block_r4_publish_refusal\(/ { print "static bool"; emit=1; master++ } \ + /^gcs_block_r4_publish_holder_refusal\(/ { print "static bool"; emit=1; holder++ } \ + emit { print } /^}/ { emit=0 } \ + END { if (domain != 1 || expectation != 1 || checksum != 1 || caps != 1 || status != 1 || decode != 1 || master != 1 || holder != 1 || emit || cap) exit 1 }' $< > $@.tmp + mv $@.tmp $@ + test_cluster_lms_outbound: test_cluster_lms_outbound.c unit_test.h \ - $(CLUSTER_VERSION_O) cluster_lms_outbound_test.o + test_cluster_r4_refusal_handoff.inc $(CLUSTER_VERSION_O) cluster_lms_outbound_test.o $(CC) $(CFLAGS) $(CPPFLAGS) $< \ $(CLUSTER_VERSION_O) cluster_lms_outbound_test.o \ $(top_builddir)/src/common/libpgcommon_srv.a \ diff --git a/src/test/cluster_unit/test_cluster_lms_outbound.c b/src/test/cluster_unit/test_cluster_lms_outbound.c index 60a3f41d7f..6ccfe6c615 100644 --- a/src/test/cluster_unit/test_cluster_lms_outbound.c +++ b/src/test/cluster_unit/test_cluster_lms_outbound.c @@ -55,9 +55,11 @@ #include "cluster/cluster_lms.h" #include "cluster/cluster_clean_leave.h" #include "cluster/cluster_pcm_x_bufmgr.h" +#include "cluster/cluster_r4_observe.h" #include "cluster/cluster_shmem.h" #include "cluster/cluster_sf_dep.h" #include "miscadmin.h" +#include "port/pg_crc32c.h" #include "storage/lwlock.h" #include "storage/shmem.h" @@ -555,6 +557,27 @@ 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; +static bool ut_r4_real_checksum = false; +static char ut_r4_reply_payload[GCS_BLOCK_REPLY_PAYLOAD_TOTAL_SIZE]; + +/* Production bodies, including their capability mask and CRC, extracted by + * the Makefile. Only observation and the final transport are fixture seams. */ +#include "test_cluster_r4_refusal_handoff.inc" + +void +cluster_r4_observe_refusal(ClusterR4RefusalStage stage, ClusterCrBuildReason reason, + const BufferTag *tag, uint64 request_id, uint64 epoch, int32 requester, + int32 master, SCN read_scn) +{ + (void)stage; + (void)reason; + (void)tag; + (void)request_id; + (void)epoch; + (void)requester; + (void)master; + (void)read_scn; +} bool cluster_ic_envelope_build(ClusterICEnvelope *out_env, uint8 msg_type, uint32 source_node_id, @@ -596,6 +619,8 @@ cluster_ic_send_envelope(uint8 msg_type, int32 dest_node_id, const void *payload uint32 i; memcpy(&ut_sent_log[ut_sent_n].reply_header, payload, sizeof(GcsBlockReplyHeader)); + if (payload_len == sizeof(ut_r4_reply_payload)) + memcpy(ut_r4_reply_payload, payload, payload_len); 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++) @@ -611,8 +636,9 @@ cluster_ic_send_envelope(uint8 msg_type, int32 dest_node_id, const void *payload uint32 cluster_gcs_block_compute_checksum(const char *block_data) { - (void)block_data; ut_checksum_call_count++; + if (ut_r4_real_checksum) + return gcs_block_compute_checksum(block_data); return UINT32_C(0xA55A7E11); } @@ -644,6 +670,8 @@ ut_reset_log(void) ut_local_dispatch_marker = 0; ut_direct_zero_reply_count = 0; ut_checksum_call_count = 0; + ut_r4_real_checksum = false; + memset(ut_r4_reply_payload, 0, sizeof(ut_r4_reply_payload)); memset(&ut_direct_zero_reply_header, 0, sizeof(ut_direct_zero_reply_header)); ut_cap_guard_drop_count = 0; memset(ut_peer_capabilities, 0, sizeof(ut_peer_capabilities)); @@ -1020,6 +1048,189 @@ UT_TEST(test_r4_cap_bound_zero_reply_drops_drift_before_zero_expansion) UT_ASSERT_EQ(ut_cap_guard_drop_count, 1); } +/* Cover the missing handoff: a real holder refusal carries the master identity, + * unlike a master refusal. Both enqueue and drain must accept it, and the real + * requester decoder must still bind it to that exact master/request/epoch. */ +UT_TEST(test_r4_real_refusal_producers_cross_outbound_and_requester_boundary) +{ + const int masters[] = { 0, 1, UT_PEER_X, CLUSTER_MAX_NODES - 1 }; + const ClusterCrBuildReason reasons[] + = { CLUSTER_CR_BUILD_CAPACITY, CLUSTER_CR_BUILD_HOLDER_MOVED, CLUSTER_CR_BUILD_PROTOCOL }; + int m; + int r; + + for (m = 0; m < lengthof(masters); m++) { + for (r = 0; r < lengthof(reasons); r++) { + ClusterR4CrForwardPayload forward = { 0 }; + GcsBlockR4ReplyExpectation expected = { 0 }; + ClusterICEnvelope env = { 0 }; + ClusterCrBuildResult result + = r == 2 ? CLUSTER_CR_BUILD_FAIL_CLOSED : CLUSTER_CR_BUILD_RETRYABLE; + bool accepted; + bool wrong_master; + bool wrong_request; + bool wrong_epoch; + + ut_reset_log(); + ut_r4_real_checksum = true; + ut_peer_rc[UT_PEER_X] = CLUSTER_IC_SEND_DONE; + ut_peer_capabilities[UT_PEER_X] = R4_CR_REQUIRED_HELLO_CAPS; + ut_peer_cap_generation[UT_PEER_X] = 42; + forward.base.request_id = 123; + forward.base.epoch = 9; + forward.base.master_node = masters[m]; + forward.base.original_requester_node = UT_PEER_X; + forward.base.requester_backend_id = 17; + forward.base.transition_id = PCM_TRANS_N_TO_S; + UT_ASSERT(gcs_block_r4_publish_holder_refusal(2, &forward, 42, result, reasons[r])); + UT_ASSERT_EQ(cluster_lms_outbound_depth(2), 1); + UT_ASSERT_EQ(ut_sent_n, 0); + UT_ASSERT_EQ(cluster_lms_outbound_drain_send(2), 1); + UT_ASSERT_EQ(cluster_lms_outbound_depth(2), 0); + UT_ASSERT_EQ(ut_sent_n, 1); + UT_ASSERT_EQ(ut_sent_log[0].dest, UT_PEER_X); + UT_ASSERT(ut_sent_log[0].reply_block_zero); + UT_ASSERT_EQ(ut_sent_log[0].reply_header.status, + r == 2 ? GCS_BLOCK_REPLY_R4_DENIED + : GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED); + UT_ASSERT_EQ(GcsBlockReplyHeaderGetForwardingMasterNode(&ut_sent_log[0].reply_header), + masters[m]); + expected.request_id = forward.base.request_id; + expected.epoch = forward.base.epoch; + expected.sender_node = cluster_node_id; + expected.forwarding_master_node = masters[m]; + expected.requester_backend_id = forward.base.requester_backend_id; + expected.transition_id = PCM_TRANS_N_TO_S; + expected.reply_domain = CLUSTER_GCS_BLOCK_REPLY_DOMAIN_R4_CR; + env.msg_type = PGRAC_IC_MSG_GCS_BLOCK_REPLY; + env.source_node_id = cluster_node_id; + env.dest_node_id = UT_PEER_X; + env.payload_length = sizeof(ut_r4_reply_payload); + cluster_node_id = UT_PEER_X; + accepted = gcs_block_decode_r4_reply_payload(&env, ut_r4_reply_payload, &expected); + expected.forwarding_master_node = GCS_BLOCK_REPLY_NO_FORWARDING_MASTER; + wrong_master = gcs_block_decode_r4_reply_payload(&env, ut_r4_reply_payload, &expected); + expected.forwarding_master_node = masters[m]; + expected.request_id++; + wrong_request = gcs_block_decode_r4_reply_payload(&env, ut_r4_reply_payload, &expected); + expected.request_id--; + expected.epoch++; + wrong_epoch = gcs_block_decode_r4_reply_payload(&env, ut_r4_reply_payload, &expected); + cluster_node_id = 0; + UT_ASSERT(accepted); + UT_ASSERT(!wrong_master && !wrong_request && !wrong_epoch); + } + } +} + +UT_TEST(test_r4_real_master_refusal_preserves_redirect) +{ + ClusterR4CrRequestPayload request = { 0 }; + ClusterICEnvelope env = { 0 }; + + ut_reset_log(); + ut_peer_capabilities[UT_PEER_X] = R4_CR_REQUIRED_HELLO_CAPS; + ut_peer_cap_generation[UT_PEER_X] = 42; + request.base.request_id = 321; + request.base.epoch = 9; + request.base.requester_backend_id = 17; + request.base.transition_id = PCM_TRANS_N_TO_S; + env.source_node_id = UT_PEER_X; + UT_ASSERT(gcs_block_r4_publish_refusal(2, &env, &request, 42, CLUSTER_CR_BUILD_RETRYABLE, + CLUSTER_CR_BUILD_WRONG_MASTER, false, + CLUSTER_MAX_NODES - 1)); + UT_ASSERT_EQ(cluster_lms_outbound_drain_send(2), 1); + UT_ASSERT_EQ(ut_sent_n, 1); + UT_ASSERT_EQ(ut_sent_log[0].reply_header.page_lsn, CLUSTER_MAX_NODES); + UT_ASSERT_EQ(GcsBlockReplyHeaderGetForwardingMasterNode(&ut_sent_log[0].reply_header), + GCS_BLOCK_REPLY_NO_FORWARDING_MASTER); +} + +UT_TEST(test_r4_holder_refusal_retains_backpressure_and_rejects_reconnect) +{ + GcsBlockReplyHeader hdr = ut_r4_refusal_header(GCS_BLOCK_REPLY_R4_DENIED, 0); + + ut_reset_log(); + GcsBlockReplyHeaderSetForwardingMasterNode(&hdr, 1); + ut_peer_capabilities[UT_PEER_X] = R4_CR_REQUIRED_HELLO_CAPS; + ut_peer_cap_generation[UT_PEER_X] = 42; + UT_ASSERT(cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + 2, UT_PEER_X, &hdr, R4_CR_REQUIRED_HELLO_CAPS, 42)); + ut_peer_rc[UT_PEER_X] = CLUSTER_IC_SEND_NOT_ADMITTED; + UT_ASSERT_EQ(cluster_lms_outbound_drain_send(2), 0); + UT_ASSERT_EQ(cluster_lms_outbound_depth(2), 1); + UT_ASSERT_EQ(ut_sent_n, 1); + ut_peer_rc[UT_PEER_X] = CLUSTER_IC_SEND_DONE; + UT_ASSERT_EQ(cluster_lms_outbound_drain_send(2), 1); + UT_ASSERT_EQ(cluster_lms_outbound_depth(2), 0); + UT_ASSERT_EQ(ut_sent_n, 2); + UT_ASSERT_EQ(memcmp(&ut_sent_log[0].reply_header, &ut_sent_log[1].reply_header, sizeof(hdr)), + 0); + UT_ASSERT(cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + 2, UT_PEER_X, &hdr, R4_CR_REQUIRED_HELLO_CAPS, 42)); + ut_peer_cap_generation[UT_PEER_X] = 43; + 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, 2); + UT_ASSERT_EQ(ut_cap_guard_drop_count, 1); +} + +UT_TEST(test_r4_holder_refusal_rejects_malformed_identity) +{ + int mutation; + + ut_reset_log(); + for (mutation = 0; mutation < 12; mutation++) { + GcsBlockReplyHeader hdr + = ut_r4_refusal_header(GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED, 0); + + GcsBlockReplyHeaderSetForwardingMasterNode(&hdr, 1); + switch (mutation) { + case 0: + GcsBlockReplyHeaderSetForwardingMasterNode(&hdr, -2); + break; + case 1: + GcsBlockReplyHeaderSetForwardingMasterNode(&hdr, CLUSTER_MAX_NODES); + break; + case 2: + hdr.page_lsn = 1; + break; + case 3: + hdr.status = GCS_BLOCK_REPLY_R4_DENIED; + hdr.page_lsn = 1; + break; + case 4: + hdr.request_id = 0; + break; + case 5: + hdr.sender_node = -1; + break; + case 6: + hdr.sender_node = CLUSTER_MAX_NODES; + break; + case 7: + hdr.requester_backend_id = 0; + break; + case 8: + hdr.transition_id = PCM_TRANS_N_TO_X; + break; + case 9: + hdr.checksum = 1; + break; + case 10: + hdr.reserved_0[0] = 1; + break; + case 11: + hdr.status = GCS_BLOCK_REPLY_R4_CR_FULL; + break; + } + UT_ASSERT(!cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + 2, UT_PEER_X, &hdr, R4_CR_REQUIRED_HELLO_CAPS, 42)); + } + UT_ASSERT_EQ(cluster_lms_outbound_depth(2), 0); + UT_ASSERT_EQ(ut_sent_n, 0); +} + 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; @@ -1709,7 +1920,7 @@ UT_TEST(test_normal_stop_full_and_bad_frames_remain_debt) int main(void) { - UT_PLAN(36); + UT_PLAN(40); UT_RUN(test_normal_stop_missing_outbound_is_not_empty); UT_RUN(test_ring_shmem_init); @@ -1722,6 +1933,10 @@ main(void) 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_r4_real_refusal_producers_cross_outbound_and_requester_boundary); + UT_RUN(test_r4_real_master_refusal_preserves_redirect); + UT_RUN(test_r4_holder_refusal_retains_backpressure_and_rejects_reconnect); + UT_RUN(test_r4_holder_refusal_rejects_malformed_identity); 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); From 429aa6b9793a8ff69153c9d2ace775de8a40bd2c Mon Sep 17 00:00:00 2001 From: SqlRush Date: Thu, 17 Sep 2026 19:41:44 +0800 Subject: [PATCH 02/10] fix(cluster): honor deadlock cancellation in exact TX waits --- src/backend/cluster/cluster_tx_enqueue.c | 7 +++ .../data/r11-source-removal-census-v1.json | 2 +- .../cluster_unit/test_cluster_r4_tx_enqueue.c | 51 ++++++++++++++++++- src/tools/check_r11_source_removal_census.py | 2 +- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/backend/cluster/cluster_tx_enqueue.c b/src/backend/cluster/cluster_tx_enqueue.c index 3cab38a8bd..f6a19c6667 100644 --- a/src/backend/cluster/cluster_tx_enqueue.c +++ b/src/backend/cluster/cluster_tx_enqueue.c @@ -738,6 +738,13 @@ cluster_tx_enqueue_wait_exact(const ClusterTxLocator *locator, int effective_tim final_reason = CLUSTER_TX_RESOLVE_RF_DEFERRED; break; } + /* Consume against the published wait before another resolve. + * Leave through the same exact cleanup as every other exit. */ + if (cluster_cancel_token_consume()) { + result = CLUSTER_TXW_DEADLOCK; + final_reason = CLUSTER_TX_RESOLVE_NONE; + break; + } memset(&resolution, 0, sizeof(resolution)); current_outcome = cluster_tx_resolve_exact( &target_locator, CLUSTER_TX_RESOLVE_ROW_WAIT, &resolution, ¤t_reason); diff --git a/src/test/cluster_unit/data/r11-source-removal-census-v1.json b/src/test/cluster_unit/data/r11-source-removal-census-v1.json index a44eafbe1d..050202204f 100644 --- a/src/test/cluster_unit/data/r11-source-removal-census-v1.json +++ b/src/test/cluster_unit/data/r11-source-removal-census-v1.json @@ -16,7 +16,7 @@ "current_product_snapshot": { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "526de2a0572d0c41dccfe05c6c01f1779646383a15a54db61f91e7cb3dde4d4a" + "sha256": "8a0167c312cb1208856076e1b968998acdb1c12435bb8472deffeba3e72247df" }, "gates": { "L1": { 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 7806cc4fb5..e5f2bcac97 100644 --- a/src/test/cluster_unit/test_cluster_r4_tx_enqueue.c +++ b/src/test/cluster_unit/test_cluster_r4_tx_enqueue.c @@ -101,6 +101,7 @@ static int test_wait_latch_calls; static int test_set_latch_calls[TEST_NSLOTS]; static bool test_wait_latch_throws; static bool test_wait_latch_sleeps; +static bool test_wait_latch_delivers_cancel; static TransactionId test_local_xid; static bool test_legacy_tt_found; static ClusterTTStatus test_legacy_tt_status; @@ -403,6 +404,8 @@ WaitLatch(Latch *latch pg_attribute_unused(), int wakeEvents pg_attribute_unused test_wait_latch_calls++; if (test_wait_latch_throws) siglongjmp(*PG_exception_stack, 1); + if (test_wait_latch_delivers_cancel) + test_cancel_token_pending = true; if (test_wait_latch_sleeps) pg_usleep((long)Max(timeout, 2) * 1000L); return WL_TIMEOUT; @@ -573,6 +576,7 @@ reset_fixture(void) memset(test_current_mx_stats, 0, sizeof(test_current_mx_stats)); test_wait_latch_throws = false; test_wait_latch_sleeps = false; + test_wait_latch_delivers_cancel = false; test_local_xid = (TransactionId)700; test_legacy_tt_found = false; test_legacy_tt_status = CLUSTER_TT_STATUS_IN_PROGRESS; @@ -859,6 +863,49 @@ UT_TEST(test_monotonic_timeout_uses_only_timeout_counter) assert_slot_clean(); } +UT_TEST(test_exact_wait_consumes_deadlock_token_before_repoll) +{ + ClusterTxLocator locator = test_locator(); + ClusterTxResolveReason reason = CLUSTER_TX_RESOLVE_PROTOCOL; + + reset_fixture(); + script_resolve(0, CLUSTER_TX_IN_PROGRESS, CLUSTER_TX_RESOLVE_NONE); + script_resolve(1, CLUSTER_TX_COMMITTED, CLUSTER_TX_RESOLVE_NONE); + test_cancel_token_pending = true; + UT_ASSERT_EQ(cluster_tx_enqueue_wait_exact(&locator, 1000, &reason), CLUSTER_TXW_DEADLOCK); + UT_ASSERT_EQ(reason, CLUSTER_TX_RESOLVE_NONE); + UT_ASSERT(!test_cancel_token_pending); + UT_ASSERT_EQ(test_resolve_pos, 1); + UT_ASSERT_EQ(test_wait_latch_calls, 0); + UT_ASSERT_EQ(test_wait_clear_calls, 1); + UT_ASSERT_EQ(test_wfg_exact_cancel_calls, 1); + UT_ASSERT(!test_wfg_live); + UT_ASSERT_EQ(pg_atomic_read_u64(&ClusterTxw->timeout_count), 0); + assert_slot_clean(); +} + +UT_TEST(test_exact_wait_consumes_deadlock_token_after_latch_wake) +{ + ClusterTxLocator locator = test_locator(); + ClusterTxResolveReason reason = CLUSTER_TX_RESOLVE_PROTOCOL; + + reset_fixture(); + script_resolve(0, CLUSTER_TX_IN_PROGRESS, CLUSTER_TX_RESOLVE_NONE); + script_resolve(1, CLUSTER_TX_IN_PROGRESS, CLUSTER_TX_RESOLVE_NONE); + script_resolve(2, CLUSTER_TX_COMMITTED, CLUSTER_TX_RESOLVE_NONE); + test_wait_latch_delivers_cancel = true; + UT_ASSERT_EQ(cluster_tx_enqueue_wait_exact(&locator, 1000, &reason), CLUSTER_TXW_DEADLOCK); + UT_ASSERT_EQ(reason, CLUSTER_TX_RESOLVE_NONE); + UT_ASSERT(!test_cancel_token_pending); + UT_ASSERT_EQ(test_resolve_pos, 2); + UT_ASSERT_EQ(test_wait_latch_calls, 1); + UT_ASSERT_EQ(test_wait_clear_calls, 1); + UT_ASSERT_EQ(test_wfg_exact_cancel_calls, 1); + UT_ASSERT(!test_wfg_live); + UT_ASSERT_EQ(pg_atomic_read_u64(&ClusterTxw->timeout_count), 0); + assert_slot_clean(); +} + UT_TEST(test_reentrant_source_and_target_slots_are_not_overwritten) { ClusterTxLocator locator = test_locator(); @@ -1486,7 +1533,7 @@ UT_TEST(test_backend_exit_counter_underflow_fails_stop_without_freeing_slot) int main(void) { - UT_PLAN(38); + UT_PLAN(40); UT_RUN(test_exact_wait_abi_and_shmem_size_are_frozen); UT_RUN(test_fixed_false_precedes_malformed_and_shared_state); UT_RUN(test_initial_terminal_never_registers); @@ -1497,6 +1544,8 @@ main(void) UT_RUN(test_zero_epoch_is_a_valid_stable_formation); UT_RUN(test_zero_to_nonzero_epoch_drift_fails_closed); UT_RUN(test_monotonic_timeout_uses_only_timeout_counter); + UT_RUN(test_exact_wait_consumes_deadlock_token_before_repoll); + UT_RUN(test_exact_wait_consumes_deadlock_token_after_latch_wake); UT_RUN(test_reentrant_source_and_target_slots_are_not_overwritten); UT_RUN(test_wfg_capacity_refusal_runs_full_cleanup); UT_RUN(test_error_longjmp_runs_same_cleanup_funnel); diff --git a/src/tools/check_r11_source_removal_census.py b/src/tools/check_r11_source_removal_census.py index 16fa259b99..a9f17209af 100644 --- a/src/tools/check_r11_source_removal_census.py +++ b/src/tools/check_r11_source_removal_census.py @@ -24,7 +24,7 @@ CURRENT_PRODUCT_SNAPSHOT = { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "526de2a0572d0c41dccfe05c6c01f1779646383a15a54db61f91e7cb3dde4d4a", + "sha256": "8a0167c312cb1208856076e1b968998acdb1c12435bb8472deffeba3e72247df", } LAYERS = { From 2e26cf68d096316ccc6eb77fe3db9a5b37e89c67 Mon Sep 17 00:00:00 2001 From: SqlRush Date: Thu, 17 Sep 2026 20:40:59 +0800 Subject: [PATCH 03/10] fix(cluster): preserve ready receipts across ITL capacity waits --- src/backend/access/heap/heapam.c | 6 +- .../data/r11-source-removal-census-v1.json | 2 +- .../test_cluster_heap_prepare_diagnostic.c | 76 +++++++++++++++++-- src/tools/check_r11_source_removal_census.py | 2 +- 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index b049653ab5..1be293b238 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -1885,12 +1885,12 @@ cluster_heap_itl_prepare_prepared_undo(Relation relation, Buffer buffer, HeapTup ClusterTxwResult wait_result; const char *wait_reason; - /* The existing wait owner releases content before exact blocker - * resolution. Its wake cannot validate this DML's old target. */ + /* The wait releases content, so its wake requires fresh page/tuple + * qualification, not receipt invalidation. Preserve the receipt + * until the requalified pending-target checks prove a change. */ wait_result = cluster_heap_itl_wait_capacity_after_census( buffer, buffer, buffer, xid, true, capacity_wait_deadline_us, &wait_reason); *content_unlocked = true; - *targets_invalidated = true; if (wait_result == CLUSTER_TXW_RESOLVED || wait_result == CLUSTER_TXW_RETRY) return CLUSTER_HEAP_PREPARED_UNDO_RETRY_REQUIRED; ereport(ERROR, diff --git a/src/test/cluster_unit/data/r11-source-removal-census-v1.json b/src/test/cluster_unit/data/r11-source-removal-census-v1.json index 050202204f..35e35f566d 100644 --- a/src/test/cluster_unit/data/r11-source-removal-census-v1.json +++ b/src/test/cluster_unit/data/r11-source-removal-census-v1.json @@ -16,7 +16,7 @@ "current_product_snapshot": { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "8a0167c312cb1208856076e1b968998acdb1c12435bb8472deffeba3e72247df" + "sha256": "a33690006785ed01f7bfef085653bd6b916ebe970de64d555c3436e9d11a4e3b" }, "gates": { "L1": { diff --git a/src/test/cluster_unit/test_cluster_heap_prepare_diagnostic.c b/src/test/cluster_unit/test_cluster_heap_prepare_diagnostic.c index 36ba1aba49..c860664e00 100644 --- a/src/test/cluster_unit/test_cluster_heap_prepare_diagnostic.c +++ b/src/test/cluster_unit/test_cluster_heap_prepare_diagnostic.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * test_cluster_heap_prepare_diagnostic.c - * Exact production prepare refusal attribution, without changing decisions. + * Exact production prepare refusals and capacity-wait requalification. * * Portions Copyright (c) 2026, pgrac contributors * Author: SqlRush @@ -157,9 +157,7 @@ check_prepare(int cause, bool applied, int expected, const char *reason) &invalidated); #endif UT_ASSERT_EQ(result, expected); - UT_ASSERT_EQ(invalidated, - cause == 7 || cause == 9 - || (capacity_requested && request_lock_only && cause == 1 && !applied)); + UT_ASSERT_EQ(invalidated, cause == 7 || cause == 9); UT_ASSERT_EQ(receipt.ctrc_applied_mask, applied ? 1 : 0); if (reason != NULL) { UT_ASSERT(observed != NULL); @@ -212,6 +210,72 @@ UT_TEST(unproved_or_failed_wait_is_not_a_retry_success) capacity_requested = false; } +UT_TEST(capacity_wait_preserves_receipt_and_original_budgets) +{ + const ClusterTxwResult wakes[] = { CLUSTER_TXW_RESOLVED, CLUSTER_TXW_RETRY }; + + for (unsigned i = 0; i < lengthof(wakes); i++) { + ClusterUndoRecordPrepareReceipt receipt = { 0 }, before; + const char *reason = "old cause must not leak"; + uint64 capacity_deadline = 12345; + bool invalidated = true; + + /* The wait must not cancel or renew this previously prepared target. + * Real expired-budget requalification is covered by undo_record. */ + receipt.absolute_deadline_us = 100; + receipt.reservation_sequence = 17; + receipt.ctrc_pending_mask = receipt.ctrc_prepared_mask = 1; + before = receipt; + fault = 1; + wait_result = wakes[i]; + wait_calls = 0; + content_unlocked = false; + UT_ASSERT_EQ(cluster_heap_itl_prepare_prepared_undo(NULL, 1, NULL, 700, true, &receipt, 64, + &invalidated, &reason, + &capacity_deadline, &content_unlocked), + CLUSTER_HEAP_PREPARED_UNDO_RETRY_REQUIRED); + UT_ASSERT(!invalidated); + UT_ASSERT(content_unlocked); + UT_ASSERT_EQ(wait_calls, 1); + UT_ASSERT(reason == NULL); + UT_ASSERT_EQ(capacity_deadline, 12345); + UT_ASSERT_EQ(memcmp(&receipt, &before, sizeof(receipt)), 0); + } +} + +UT_TEST(capacity_wake_does_not_bypass_fresh_target_recheck) +{ + const int rechecks[] = { 0, 7, 9 }; + + for (unsigned i = 0; i < lengthof(rechecks); i++) { + ClusterUndoRecordPrepareReceipt receipt = { 0 }; + const char *reason = NULL; + uint64 capacity_deadline = 12345; + bool invalidated = false; + + receipt.ctrc_pending_mask = receipt.ctrc_prepared_mask = 1; + fault = 1; + wait_result = CLUSTER_TXW_RESOLVED; + UT_ASSERT_EQ(cluster_heap_itl_prepare_prepared_undo(NULL, 1, NULL, 700, true, &receipt, 64, + &invalidated, &reason, + &capacity_deadline, &content_unlocked), + CLUSTER_HEAP_PREPARED_UNDO_RETRY_REQUIRED); + UT_ASSERT(!invalidated); + /* Simulate the caller's fresh page bracket. Actual pending-target + * mismatch still invalidates; a wake alone never publishes READY. */ + fault = rechecks[i]; + UT_ASSERT_EQ(cluster_heap_itl_prepare_prepared_undo(NULL, 1, NULL, 700, true, &receipt, 64, + &invalidated, &reason, + &capacity_deadline, &content_unlocked), + i == 0 ? CLUSTER_HEAP_PREPARED_UNDO_READY + : CLUSTER_HEAP_PREPARED_UNDO_RETRY_REQUIRED); + UT_ASSERT_EQ(invalidated, i != 0); + UT_ASSERT(!content_unlocked); + UT_ASSERT_EQ(receipt.ctrc_applied_mask, 0); + UT_ASSERT_EQ(capacity_deadline, 12345); + } +} + UT_TEST(refusals_have_unique_exact_causes) { check_prepare(1, false, CLUSTER_HEAP_PREPARED_UNDO_REFUSED, "ITL_CAPACITY_REFUSED"); @@ -236,11 +300,13 @@ UT_TEST(success_and_preapply_retries_remain_unchanged) int main(void) { - UT_PLAN(4); + UT_PLAN(6); UT_RUN(refusals_have_unique_exact_causes); UT_RUN(success_and_preapply_retries_remain_unchanged); UT_RUN(full_lock_capacity_waits_then_requalifies_without_apply); UT_RUN(unproved_or_failed_wait_is_not_a_retry_success); + UT_RUN(capacity_wait_preserves_receipt_and_original_budgets); + UT_RUN(capacity_wake_does_not_bypass_fresh_target_recheck); UT_DONE(); return ut_failed_count == 0 ? 0 : 1; } diff --git a/src/tools/check_r11_source_removal_census.py b/src/tools/check_r11_source_removal_census.py index a9f17209af..1e37cceb4c 100644 --- a/src/tools/check_r11_source_removal_census.py +++ b/src/tools/check_r11_source_removal_census.py @@ -24,7 +24,7 @@ CURRENT_PRODUCT_SNAPSHOT = { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "8a0167c312cb1208856076e1b968998acdb1c12435bb8472deffeba3e72247df", + "sha256": "a33690006785ed01f7bfef085653bd6b916ebe970de64d555c3436e9d11a4e3b", } LAYERS = { From 13a87959e4acb9aaaa2e5c316745b26e3f0866df Mon Sep 17 00:00:00 2001 From: SqlRush Date: Fri, 18 Sep 2026 18:39:49 +0800 Subject: [PATCH 04/10] fix(cluster): complete nested receipt and CR dependency lifecycles --- src/backend/access/heap/heapam.c | 73 +++++-- src/backend/access/index/indexam.c | 2 +- src/backend/cluster/cluster_cr_server.c | 77 ++++++- src/backend/cluster/cluster_gcs_block.c | 17 +- src/backend/cluster/cluster_lmd_tarjan.c | 15 ++ src/backend/storage/buffer/bufmgr.c | 2 +- src/test/cluster_unit/Makefile | 35 +++- .../data/r11-source-removal-census-v1.json | 2 +- .../cluster_unit/test_cluster_cr_dependency.c | 190 ++++++++++++++++++ .../test_cluster_heap_update_temp_lock.c | 101 +++++++++- .../test_cluster_r4_itl_capacity.c | 59 +++++- .../cluster_unit/test_cluster_r4_lock_order.c | 4 + .../test_cluster_r4_route_policy.c | 40 +++- .../test_cluster_r4_slot_reservation.c | 17 ++ .../cluster_unit/test_cluster_undo_record.c | 16 +- src/tools/check_r11_source_removal_census.py | 2 +- 16 files changed, 607 insertions(+), 45 deletions(-) create mode 100644 src/test/cluster_unit/test_cluster_cr_dependency.c diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index 1be293b238..de23f3e912 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -481,13 +481,32 @@ cluster_heap_undo_receipt_errdetail(bool ctrc) cluster_undo_record_receipt_last_reason(), cluster_node_id); } +/* Start a new preparation only after an explicitly cancelled outer producer's + * nested producer has returned and released its reservation. Ordinary retries + * must keep using cluster_heap_retry_undo_record_exact and their fixed budget. */ +static bool +cluster_heap_resume_update_undo_record_exact(const ClusterCanonicalTxnBinding *binding, + uint64 *deadline_us, + ClusterUndoRecordPrepareReceipt *receipt) +{ + if (binding == NULL || deadline_us == NULL || receipt == NULL || receipt->magic != 0 + || receipt->ctrc_applied_mask != 0) + return false; + *deadline_us = cluster_undo_record_prepare_deadline_us(); + return *deadline_us != 0 + && cluster_heap_prepare_undo_record_exact( + UNDO_RECORD_UPDATE, (uint16)cluster_undo_record_inline_max_bytes, + (uint16)binding->segment_id, binding->slot_offset, (UBA)InvalidUba_init, + *deadline_us, receipt); +} + /* The caller has released content locks and retains its original heap pin. * The ordinary row-lock producer owns the locator, undo and publication; * this adapter owns only its additional pin and the unpublished outer slot. */ static TM_Result cluster_heap_lock_update_predecessor(Relation relation, ItemPointer tid, CommandId cid, LockTupleMode mode, LockWaitPolicy wait_policy, - const ClusterCanonicalTxnBinding *binding, uint64 deadline_us, + const ClusterCanonicalTxnBinding *binding, uint64 *deadline_us, ClusterUndoRecordPrepareReceipt *receipt) { HeapTupleData tuple = { 0 }; @@ -526,11 +545,7 @@ cluster_heap_lock_update_predecessor(Relation relation, ItemPointer tid, Command ReleaseBuffer(nested_buffer); } PG_END_TRY(); - if (resume - && !cluster_heap_prepare_undo_record_exact( - UNDO_RECORD_UPDATE, (uint16)cluster_undo_record_inline_max_bytes, - (uint16)binding->segment_id, binding->slot_offset, (UBA)InvalidUba_init, deadline_us, - receipt)) + if (resume && !cluster_heap_resume_update_undo_record_exact(binding, deadline_us, receipt)) ereport(ERROR, (errcode(ERRCODE_CLUSTER_UNDO_RECORD_INVALID_UBA), errmsg("cluster undo reservation failed after heap temporary lock"), cluster_heap_undo_receipt_errdetail(false))); @@ -1604,6 +1619,7 @@ cluster_heap_itl_wait_capacity_after_census(Buffer old_buffer, Buffer new_buffer uint8 i; int remaining_ms; uint64 wait_deadline; + volatile bool wait_finished = false; Assert(full_buffer == old_buffer || full_buffer == new_buffer); *diagnostic_reason = "ITL_BLOCKER_UNPROVABLE"; @@ -1732,11 +1748,38 @@ cluster_heap_itl_wait_capacity_after_census(Buffer old_buffer, Buffer new_buffer } wait_deadline = *deadline_us; cluster_vis_evidence_note(CLUSTER_VIS_METRIC_ITL_WAIT_STARTED); - result = cluster_tx_enqueue_wait_exact(&blocker, remaining_ms, &wait_reason); + PG_TRY(); + { + result = cluster_tx_enqueue_wait_exact(&blocker, remaining_ms, &wait_reason); + wait_finished = true; + } + PG_FINALLY(); + { + /* Error-only causal evidence. The copied census remains valid as an + * observation, never as a new authority; no page is repinned here. */ + if (!wait_finished || (result != CLUSTER_TXW_RESOLVED && result != CLUSTER_TXW_RETRY)) { + ereport(LOG, + (errmsg("cluster ITL capacity wait terminated"), + errdetail("PGRAC_FAMILY=ITL_CAPACITY_DIAGNOSTIC PGRAC_REASON=WAIT_TERMINAL " + "waiter_xid=%u blocker_xid=%u blocker_wrap=%u lock_only=%d " + "returned=%d result=%d resolve_reason=%d deadline_us=" UINT64_FORMAT, + xid, blocker.xid, blocker.tt_wrap, lock_only, + wait_finished, wait_finished ? (int)result : -1, + wait_finished ? (int)wait_reason : -1, wait_deadline))); + cluster_heap_itl_capacity_diagnostic(&census, capture_result, "WAIT_TERMINAL"); + } + } + PG_END_TRY(); if (*deadline_us != wait_deadline) { cluster_vis_evidence_note(CLUSTER_VIS_METRIC_ITL_DEADLINE_REFRESH); return CLUSTER_TXW_UNPROVABLE; } + /* A consumed, exact deadlock cancellation is the terminal cause even if + * the capacity budget expires during the return/diagnostic boundary. */ + if (result == CLUSTER_TXW_DEADLOCK) { + *diagnostic_reason = "ITL_WAIT_DEADLOCK"; + return result; + } /* Even a terminal reply cannot renew an already spent caller budget. */ if (result == CLUSTER_TXW_TIMEOUT || cluster_heap_itl_remaining_wait_ms(deadline_us, cluster_heap_itl_now_us(), 0) == 0) { @@ -1897,7 +1940,9 @@ cluster_heap_itl_prepare_prepared_undo(Relation relation, Buffer buffer, HeapTup (errcode(wait_result == CLUSTER_TXW_TIMEOUT ? ERRCODE_CLUSTER_GES_TIMEOUT : wait_result == CLUSTER_TXW_DEADLOCK ? ERRCODE_T_R_DEADLOCK_DETECTED : ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("ITL slot OVERFLOW before heap tuple lock (INITRANS=%d full)", + errmsg(wait_result == CLUSTER_TXW_DEADLOCK + ? "deadlock detected while waiting for heap tuple-lock ITL capacity (INITRANS=%d)" + : "ITL slot OVERFLOW before heap tuple lock (INITRANS=%d full)", CLUSTER_ITL_INITRANS_DEFAULT), errdetail("PGRAC_FAMILY=ITL_CAPACITY PGRAC_REASON=%s PGRAC_NODE=%d " "PGRAC_ATTEMPT=0 wait_result=%d deadline_us=" UINT64_FORMAT, @@ -12418,7 +12463,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, LockBuffer(buffer, BUFFER_LOCK_UNLOCK); lock_result = cluster_heap_lock_update_predecessor( relation, &oldtup.t_self, pgrac_entry_cid, *lockmode, - wait ? LockWaitBlock : LockWaitSkip, &canonical_binding, undo_prepare_deadline_us, + wait ? LockWaitBlock : LockWaitSkip, &canonical_binding, &undo_prepare_deadline_us, &undo_receipt); old_tuple_temp_locked = lock_result == TM_Ok; cluster_heap_lock_with_vm_repin(relation, block, buffer, &vmbuffer); @@ -12794,10 +12839,8 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, newtupsize = MAXALIGN(heaptup->t_len); #ifdef USE_PGRAC_CLUSTER if (resume_update_receipt - && !cluster_heap_prepare_undo_record_exact( - UNDO_RECORD_UPDATE, (uint16)cluster_undo_record_inline_max_bytes, - (uint16)canonical_binding.segment_id, canonical_binding.slot_offset, - (UBA)InvalidUba_init, undo_prepare_deadline_us, &undo_receipt)) + && !cluster_heap_resume_update_undo_record_exact( + &canonical_binding, &undo_prepare_deadline_us, &undo_receipt)) ereport(ERROR, (errcode(ERRCODE_CLUSTER_UNDO_RECORD_INVALID_UBA), errmsg("cluster undo reservation failed before heap update"), cluster_heap_undo_receipt_errdetail(false))); @@ -14456,7 +14499,9 @@ l_pgrac_itl_capacity_wait: { (errcode(wait_result == CLUSTER_TXW_TIMEOUT ? ERRCODE_CLUSTER_GES_TIMEOUT : wait_result == CLUSTER_TXW_DEADLOCK ? ERRCODE_T_R_DEADLOCK_DETECTED : ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("ITL slot OVERFLOW on heap page (INITRANS=%d full)", + errmsg(wait_result == CLUSTER_TXW_DEADLOCK + ? "deadlock detected while waiting for heap update ITL capacity (INITRANS=%d)" + : "ITL slot OVERFLOW on heap page (INITRANS=%d full)", CLUSTER_ITL_INITRANS_DEFAULT), errdetail("PGRAC_FAMILY=ITL_CAPACITY PGRAC_REASON=%s PGRAC_NODE=%d " "PGRAC_ATTEMPT=0 wait_result=%d deadline_us=" UINT64_FORMAT, diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c index de9f69e0c1..db087d4b2a 100644 --- a/src/backend/access/index/indexam.c +++ b/src/backend/access/index/indexam.c @@ -717,7 +717,7 @@ index_getnext_slot(IndexScanDesc scan, ScanDirection direction, TupleTableSlot * && scan->xs_snapshot->snapshot_type == SNAPSHOT_MVCC && scan->xs_snapshot->cluster_source == SNAPSHOT_SOURCE_CLUSTER && SCN_VALID(scan->xs_snapshot->read_scn)) - ereport(LOG, + ereport(DEBUG1, (errmsg("R4 index selection exhausted"), errdetail("PGRAC_FAMILY=R4_SELECTION PGRAC_REASON=INDEX_EXHAUSTED " "node=%d relation=%u index=%u read_scn=" UINT64_FORMAT diff --git a/src/backend/cluster/cluster_cr_server.c b/src/backend/cluster/cluster_cr_server.c index bd3a305d6c..7b01633cea 100644 --- a/src/backend/cluster/cluster_cr_server.c +++ b/src/backend/cluster/cluster_cr_server.c @@ -118,6 +118,8 @@ typedef struct ClusterR4CrWorkerContext { int32 requester_node; int32 requester_backend_id; uint64 request_id; + /* Reply period for one unpublished dependency, never a SQL deadline. */ + TimestampTz foreign_reply_deadline; ClusterSemanticAdmissionToken admission; uint32 expected_foreign_physical_generation; bool foreign_physical_generation_frozen; @@ -1401,6 +1403,8 @@ cr_server_r4_send_foreign_undo(uint32 slot_index) if (!pg_atomic_compare_exchange_u32(&slot->state, &expected, CLUSTER_LMS_CR_R4_UNDO_INFLIGHT)) return false; pg_read_barrier(); + context->foreign_reply_deadline + = TimestampTzPlusMilliseconds(GetCurrentTimestamp(), Max(cluster_gcs_reply_timeout_ms, 1)); send_result = cluster_ic_send_envelope(PGRAC_IC_MSG_GCS_BLOCK_FORWARD, slot->r4.foreign_origin_node, &forward, sizeof(forward)); cluster_gcs_block_note_send_outcome(GCS_BLOCK_SEND_FAMILY_FORWARD, send_result); @@ -1698,14 +1702,24 @@ cr_server_r4_ship_terminal(uint32 slot_index) memcpy(frame + sizeof(*header), slot->result_page, BLCKSZ); } header->checksum = cluster_gcs_block_compute_checksum(frame + sizeof(*header)); - if (!cr_server_r4_identity_open_matches(&context->admission, slot->requester_node, slot->r4.requester_capability_generation) - || !cluster_semantic_activation_recheck(&context->admission) - || (terminal_state == CLUSTER_LMS_CR_R4_READY_FULL && cluster_write_fence_enforcing() - && !cluster_write_fence_allowed())) + || !cluster_semantic_activation_recheck(&context->admission)) return cr_server_r4_release_terminal(slot_index, slot_generation); + /* A current authenticated requester may retry a fenced image. Never send + * its bytes, or resurrect FULL if transport admission itself must retry. */ + if (terminal_state == CLUSTER_LMS_CR_R4_READY_FULL && cluster_write_fence_enforcing() + && !cluster_write_fence_allowed()) { + terminal_state = CLUSTER_LMS_CR_R4_READY_RETRY; + terminal_reason = CLUSTER_CR_BUILD_HOLDER_MOVED; + slot->r4.terminal_reason = (uint8)terminal_reason; + header->status = (uint8)GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED; + header->page_lsn = 0; + memset(frame + sizeof(*header), 0, BLCKSZ); + header->checksum = cluster_gcs_block_compute_checksum(frame + sizeof(*header)); + } + if (terminal_reason != CLUSTER_CR_BUILD_NONE) cluster_r4_observe_refusal(CLUSTER_R4_REFUSAL_HOLDER_SHIP, terminal_reason, &slot->tag, slot->request_id, slot->epoch, slot->requester_node, @@ -1723,6 +1737,7 @@ cr_server_r4_ship_terminal(uint32 slot_index) } else send_result = cluster_ic_send_envelope(PGRAC_IC_MSG_GCS_BLOCK_REPLY, slot->requester_node, frame, sizeof(frame)); + cluster_gcs_block_note_send_outcome(GCS_BLOCK_SEND_FAMILY_REPLY, send_result); switch (send_result) { case CLUSTER_IC_SEND_DONE: case CLUSTER_IC_SEND_WOULD_BLOCK: @@ -1738,7 +1753,60 @@ cr_server_r4_ship_terminal(uint32 slot_index) return false; } +/* Worker 0 owns retirement of unpublished dependency continuations. This + * maintenance is safe during origin SCUR work: no build, buffer or undo I/O. + * The original terminal owner retains the slot until transport accepts its + * zero-body retry. Neither an elapsed reply period nor that retry proves any + * transaction outcome. Late replies still face the exact in-flight key gate. */ +static void +cr_server_r4_maintain_dependencies(void) +{ + TimestampTz now; + + if (CrServerShared == NULL || cluster_ic_tier1_my_data_channel() != 0 || MyBackendType != B_LMS + || !cluster_gcs_block_family_on_data_plane()) + return; + now = GetCurrentTimestamp(); + for (uint32 i = 0; i < CLUSTER_LMS_CR_SLOTS; i++) { + ClusterLmsCrSlot *slot = &CrServerShared->slots[i]; + ClusterR4CrWorkerContext *context = &CrServerR4Contexts[i]; + uint32 state = pg_atomic_read_u32(&slot->state); + + if (!context->in_use || context->foreign_reply_deadline == 0 + || now < context->foreign_reply_deadline) + continue; + if (state == CLUSTER_LMS_CR_R4_UNDO_INFLIGHT) { + ClusterR4CrWorkerContext frozen_context = *context; + + if (!cr_server_r4_foreign_landing_key_valid(i, slot, context, &frozen_context)) + continue; + if (!cr_server_r4_publish_foreign_terminal(slot, CLUSTER_LMS_CR_R4_UNDO_INFLIGHT, + CLUSTER_LMS_CR_R4_READY_RETRY, + CLUSTER_CR_BUILD_HOLDER_MOVED)) + continue; + /* Bounded by the four slots and the existing reply period, not by + * the worker polling rate. Ordinary successful work stays silent. */ + ereport(LOG, + (errmsg_internal("R4 CR dependency reply period elapsed"), + errdetail("PGRAC_FAMILY=R4_CR_RETRY " + "PGRAC_REASON=UNDO_REPLY_PERIOD_EXPIRED request=" UINT64_FORMAT + " dependency=" UINT64_FORMAT " slot=%u generation=" UINT64_FORMAT, + slot->request_id, slot->r4.foreign_request_id, i, + context->slot_generation))); + state = CLUSTER_LMS_CR_R4_READY_RETRY; + } + if (state == CLUSTER_LMS_CR_R4_READY_RETRY) + (void)cr_server_r4_ship_terminal(i); + } +} + #ifdef USE_CLUSTER_UNIT +void +cluster_cr_server_test_r4_maintain_dependencies(void) +{ + cr_server_r4_maintain_dependencies(); +} + bool cluster_cr_server_test_r4_claim_queued(uint32 slot_index) { @@ -3782,6 +3850,7 @@ cluster_lms_cr_drain(void) * process-local, so it must progress even when the legacy CR table is * absent. */ cluster_gcs_block_r4_tx_resolve_drain(); + cr_server_r4_maintain_dependencies(); if (cluster_gcs_block_r4_tx_resolve_active()) { cr_server_r4_note_origin_deferral(); return; diff --git a/src/backend/cluster/cluster_gcs_block.c b/src/backend/cluster/cluster_gcs_block.c index 1a27eb7a79..0d492925b1 100644 --- a/src/backend/cluster/cluster_gcs_block.c +++ b/src/backend/cluster/cluster_gcs_block.c @@ -2452,6 +2452,7 @@ gcs_block_r4_publish_refusal(int worker_id, const ClusterICEnvelope *env, GcsBlockReplyHeader header; GcsBlockReplyStatus status; SCN read_scn = InvalidScn; + bool queued; if (!gcs_block_r4_refusal_status_for_build(result, reason, admitted_forward, &status)) return true; @@ -2471,9 +2472,16 @@ gcs_block_r4_publish_refusal(int worker_id, const ClusterICEnvelope *env, && 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( + queued = cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( worker_id, env->source_node_id, &header, R4_CR_REQUIRED_HELLO_CAPS, requester_capability_generation); + /* A non-admitted refusal has no transport owner. The requester's existing + * retransmit still owns recovery; expose the refusal instead of claiming + * delivery or blocking the LMS that must drain the full ring. */ + if (!queued) + cluster_gcs_block_note_send_outcome(GCS_BLOCK_SEND_FAMILY_REPLY, + CLUSTER_IC_SEND_NOT_ADMITTED); + return queued; } /* @@ -2488,6 +2496,7 @@ gcs_block_r4_publish_holder_refusal(int worker_id, const ClusterR4CrForwardPaylo { GcsBlockReplyHeader header; GcsBlockReplyStatus status; + bool queued; if (forward == NULL || !gcs_block_r4_refusal_status_for_build(result, reason, true, &status)) return true; @@ -2503,9 +2512,13 @@ gcs_block_r4_publish_holder_refusal(int worker_id, const ClusterR4CrForwardPaylo header.transition_id = forward->base.transition_id; header.status = (uint8)status; GcsBlockReplyHeaderSetForwardingMasterNode(&header, forward->base.master_node); - return cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( + queued = cluster_lms_outbound_enqueue_zero_block_reply_cap_bound( worker_id, (uint32)forward->base.original_requester_node, &header, R4_CR_REQUIRED_HELLO_CAPS, requester_capability_generation); + if (!queued) + cluster_gcs_block_note_send_outcome(GCS_BLOCK_SEND_FAMILY_REPLY, + CLUSTER_IC_SEND_NOT_ADMITTED); + return queued; } static bool diff --git a/src/backend/cluster/cluster_lmd_tarjan.c b/src/backend/cluster/cluster_lmd_tarjan.c index 7e0e4e2fb9..4efacba3bb 100644 --- a/src/backend/cluster/cluster_lmd_tarjan.c +++ b/src/backend/cluster/cluster_lmd_tarjan.c @@ -1479,6 +1479,21 @@ cluster_lmd_tarjan_run_coordinator_scan(int collect_timeout_ms) /* One cancel_id per issued cancel, threaded through the token / CANCEL_ * WAIT / CANCEL_ACK for correlation (spec-5.9 D3/D5). */ cancel_id = lmd_next_cancel_id(); + /* Preserve the already confirmed cycle before cancellation can remove + * its wait edges. This is failure evidence, not a new detector or probe. */ + for (int i = 0; i < Min(round2.n_cycle_vertices, 32); i++) { + const ClusterLmdVertex *member = &round2.cycle_vertices[i]; + + ereport(LOG, + (errmsg("cluster LMD confirmed cycle member"), + errdetail("PGRAC_FAMILY=DEADLOCK PGRAC_REASON=CONFIRMED_CYCLE " + "cycle=" UINT64_FORMAT " cancel=" UINT64_FORMAT + " member=%d total=%d node=%d procno=%u xid=%u epoch=" UINT64_FORMAT + " request=" UINT64_FORMAT " wait_seq=" UINT64_FORMAT, + round2.cycle_hash, cancel_id, i, round2.n_cycle_vertices, + member->node_id, member->procno, member->xid, member->cluster_epoch, + member->request_id, member->wait_seq))); + } if (victim.node_id == self_node) { /* Count only a real cancel — D5 revalidate may refuse a victim that diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 379d325d2c..81b2f7e6fd 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -16741,7 +16741,7 @@ cluster_bufmgr_pcm_own_release_retained_fence_preserve_pi( UnlockBufHdr(buf, buf_state); LWLockRelease(content_lock); if (released) - elog(LOG, + elog(DEBUG1, "cluster PCM retained transfer fence released: image=kept-pi buffer=%d rel=%u fork=%d blk=%u gen=%llu token=%llu", buf->buf_id, tag->relNumber, (int)tag->forkNum, tag->blockNum, (unsigned long long)committed_generation, diff --git a/src/test/cluster_unit/Makefile b/src/test/cluster_unit/Makefile index e9fd6c7753..18f5c3e18c 100644 --- a/src/test/cluster_unit/Makefile +++ b/src/test/cluster_unit/Makefile @@ -46,6 +46,7 @@ endif # Test source files (each becomes a standalone executable) TESTS = test_cluster_basic test_cluster_version test_cluster_backend_types test_cluster_port_runtime \ + test_cluster_cr_dependency \ test_cluster_heap_prepare_diagnostic \ test_cluster_r4_production_reachability test_cluster_heap_update_temp_lock test_cluster_heap_dml_lifetime \ test_cluster_gcs_reqid test_cluster_ctrc_cleaner test_cluster_ctrc_dispatch test_cluster_heap_receipt \ @@ -3357,6 +3358,24 @@ $(CLUSTER_R4_CONTINUATION_CR_O): $(top_srcdir)/src/backend/cluster/cluster_cr.c -Dcluster_cr_r4_extract_resident_record=continuation_real_extract \ -c $< -o $@ +test_cluster_cr_dependency_drain.inc: $(top_srcdir)/src/backend/cluster/cluster_cr_server.c Makefile + awk '/^cluster_lms_cr_drain\(/ { print "void"; emit=1; found++ } \ + emit { print } /^}/ { emit=0 } END { if (found != 1 || emit) exit 1 }' $< > $@.tmp + mv $@.tmp $@ + +test_cluster_cr_dependency: test_cluster_cr_dependency.c test_cluster_cr_dependency_drain.inc \ + test_cluster_r4_slot_reservation.c unit_test.h \ + $(CLUSTER_R4_SLOT_RESERVATION_TEST_O) $(CLUSTER_R4_LMS_DATA_PLANE_TEST_O) \ + $(CLUSTER_R4_SLOT_RESERVATION_UBA_O) $(CLUSTER_R4_CONTINUATION_CR_O) \ + test_cluster_r4_multi_resolve_product.o $(CLUSTER_CR_APPLY_O) $(CLUSTER_R4_CONTINUATION_PAGE_O) + $(CC) $(CFLAGS) $(CPPFLAGS) \ + -DCR_SERVER_SOURCE_PATH='"$(abspath $(top_srcdir))/src/backend/cluster/cluster_cr_server.c"' \ + $< $(CLUSTER_R4_SLOT_RESERVATION_TEST_O) $(CLUSTER_R4_LMS_DATA_PLANE_TEST_O) \ + $(CLUSTER_R4_SLOT_RESERVATION_UBA_O) $(CLUSTER_R4_CONTINUATION_CR_O) \ + test_cluster_r4_multi_resolve_product.o $(CLUSTER_CR_APPLY_O) $(CLUSTER_R4_CONTINUATION_PAGE_O) \ + $(R4_RUNTIME_VIS_TEST_DEAD_STRIP) $(top_builddir)/src/common/libpgcommon_srv.a \ + $(CLUSTER_UNIT_PORT_LIBS) -o $@ + test_cluster_r4_slot_reservation: test_cluster_r4_slot_reservation.c unit_test.h \ $(CLUSTER_R4_SLOT_RESERVATION_TEST_O) $(CLUSTER_R4_LMS_DATA_PLANE_TEST_O) \ $(CLUSTER_R4_SLOT_RESERVATION_UBA_O) $(CLUSTER_R4_CONTINUATION_CR_O) \ @@ -4571,9 +4590,19 @@ test_cluster_r4_production_reachability: test_cluster_r4_production_reachability $(CC) $(CFLAGS) $(CPPFLAGS) $< -o $@ test_cluster_heap_update_temp_lock.inc: $(top_srcdir)/src/backend/access/heap/heapam.c Makefile - awk '/^cluster_heap_lock_update_predecessor\(/ { print "static TM_Result"; emit=1; found++ } \ + awk '/^cluster_heap_resume_update_undo_record_exact\(/ { print "static bool"; emit=1; phase++ } \ + /^cluster_heap_lock_update_predecessor\(/ { print "static TM_Result"; emit=1; found++ } \ emit { print } emit && /^}/ { emit=0; done++ } \ - END { if (found != 1 || done != 1) exit 1 }' $< > $@.tmp + END { if (found != 1 || phase != 1 || done != 2) exit 1; \ + print "#define PREPARE_RESUME_HAS_PHASE 1" }' $< > $@.tmp + mv $@.tmp $@ + +test_cluster_heap_update_toast_resume.inc: $(top_srcdir)/src/backend/access/heap/heapam.c Makefile + awk '/^heap_update\(/ { in_update=1 } \ + in_update && /heaptup = heap_toast_insert_or_update/ { after_toast=1 } \ + after_toast && /if \(resume_update_receipt/ { emit=1; found++ } \ + emit && /^#endif/ { emit=0; after_toast=0; done++ } emit { print } \ + /^}/ { in_update=0 } END { if (found != 1 || done != 1) exit 1 }' $< > $@.tmp mv $@.tmp $@ test_cluster_heap_update_temp_consumer.inc: $(top_srcdir)/src/backend/access/heap/heapam.c Makefile @@ -4662,7 +4691,7 @@ test_cluster_heap_prepare_diagnostic.inc: $(top_srcdir)/src/backend/access/heap/ test_cluster_heap_prepare_diagnostic: test_cluster_heap_prepare_diagnostic.c unit_test.h test_cluster_heap_prepare_diagnostic.inc $(CC) $(CFLAGS) $(CPPFLAGS) $< -o $@ -test_cluster_heap_update_temp_lock: test_cluster_heap_update_temp_lock.c unit_test.h test_cluster_heap_update_temp_lock.inc test_cluster_heap_update_temp_consumer.inc test_cluster_heap_lock_return_receipt.inc test_cluster_heap_update_successor_header.inc +test_cluster_heap_update_temp_lock: test_cluster_heap_update_temp_lock.c unit_test.h test_cluster_heap_update_temp_lock.inc test_cluster_heap_update_temp_consumer.inc test_cluster_heap_lock_return_receipt.inc test_cluster_heap_update_successor_header.inc test_cluster_heap_update_toast_resume.inc $(CC) $(CFLAGS) $(CPPFLAGS) $< -o $@ test_cluster_r4_lock_order: test_cluster_r4_lock_order.c unit_test.h \ diff --git a/src/test/cluster_unit/data/r11-source-removal-census-v1.json b/src/test/cluster_unit/data/r11-source-removal-census-v1.json index 35e35f566d..e05a356104 100644 --- a/src/test/cluster_unit/data/r11-source-removal-census-v1.json +++ b/src/test/cluster_unit/data/r11-source-removal-census-v1.json @@ -16,7 +16,7 @@ "current_product_snapshot": { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "a33690006785ed01f7bfef085653bd6b916ebe970de64d555c3436e9d11a4e3b" + "sha256": "696411d2f2a36a4d32c9a08740ab5eeb3221f61e6f57ccee2109d225badb0554" }, "gates": { "L1": { diff --git a/src/test/cluster_unit/test_cluster_cr_dependency.c b/src/test/cluster_unit/test_cluster_cr_dependency.c new file mode 100644 index 0000000000..8073c25394 --- /dev/null +++ b/src/test/cluster_unit/test_cluster_cr_dependency.c @@ -0,0 +1,190 @@ +/* Author: SqlRush + * Exercise the actual worker drain and production slot/ship owners. Only + * clock, origin activity and transport boundaries are controlled fixtures. + * Portions Copyright (c) 2026, pgrac contributors + */ +int reservation_fixture_main(void); +#define main reservation_fixture_main +#include "test_cluster_r4_slot_reservation.c" +#undef main + +static bool dependency_origin_pending; +static int dependency_legacy_calls; +static uint32 cluster_lms_cr_legacy_drain_cursor; +static UtClusterCrServerShared *dependency_shared; + +void +cluster_gcs_block_r4_tx_resolve_drain(void) +{} + +bool +cluster_gcs_block_r4_tx_resolve_active(void) +{ + return dependency_origin_pending; +} + +static void +dependency_noop(void) +{} + +static void +dependency_legacy_serve(ClusterLmsCrSlot *slot) +{ + (void)slot; + dependency_legacy_calls++; +} + +extern void cluster_cr_server_test_r4_maintain_dependencies(void); +void dependency_real_drain(void); +#define CrServerShared dependency_shared +#define cluster_lms_cr_drain dependency_real_drain +#define cr_server_r4_claim_queued cluster_cr_server_test_r4_claim_queued +#define cr_server_r4_build_step cluster_cr_server_test_r4_build_step +#define cr_server_r4_ship_terminal cluster_cr_server_test_r4_ship_terminal +#define cr_server_r4_send_foreign_undo cluster_cr_server_test_r4_send_foreign_undo +#define cr_server_r4_maintain_dependencies cluster_cr_server_test_r4_maintain_dependencies +#define cr_server_r4_note_origin_deferral dependency_noop +#define cr_serve_slot dependency_legacy_serve +#define cluster_lmon_duty_mark_dirty(duty) ((void)0) +#define cluster_lmon_wakeup dependency_noop +#include "test_cluster_cr_dependency_drain.inc" + +UT_TEST(test_lost_dependency_retires_while_origin_busy_and_stop_sealed) +{ + ClusterLmsSharedState state; + ClusterLmsCrSlot *slot = prepare_worker0_undo_inflight(&state); + const char *reason; + int index; + int builds = ut_builder_step_calls; + + dependency_origin_pending = true; + ut_stop_new_work_allowed = false; + ut_now += (int64)cluster_gcs_reply_timeout_ms * 1000 - 1; + dependency_real_drain(); + UT_ASSERT_EQ(pg_atomic_read_u32(&slot->state), CLUSTER_LMS_CR_R4_UNDO_INFLIGHT); + UT_ASSERT_EQ(ut_send_calls, 1); + ut_now++; + dependency_real_drain(); + UT_ASSERT_EQ(pg_atomic_read_u32(&slot->state), CLUSTER_LMS_CR_FREE); + UT_ASSERT_EQ(ut_builder_step_calls, builds); + UT_ASSERT_EQ(ut_send_calls, 2); + UT_ASSERT_EQ(((GcsBlockReplyHeader *)ut_send_payload)->status, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED); + UT_ASSERT(bytes_are(ut_send_payload + sizeof(GcsBlockReplyHeader), BLCKSZ, 0)); + UT_ASSERT_EQ(ut_forget_calls, 1); + UT_ASSERT_EQ(ut_leave_calls, 1); + UT_ASSERT_EQ(cluster_cr_server_normal_stop_poll(&index, &reason), CLUSTER_NORMAL_STOP_READY); + UT_ASSERT_EQ(dependency_legacy_calls, 0); +} + +UT_TEST(test_dependency_terminal_backpressure_retains_owner_until_send) +{ + ClusterLmsSharedState state; + ClusterLmsCrSlot *slot = prepare_worker0_undo_inflight(&state); + GcsBlockReplyHeader header; + ClusterGcsUndoAuthTrailer auth; + ClusterICEnvelope env; + char page[BLCKSZ]; + + make_foreign_undo_reply(&header, &auth, &env, page); + dependency_origin_pending = true; + ut_send_result = CLUSTER_IC_SEND_NOT_ADMITTED; + ut_now += (int64)cluster_gcs_reply_timeout_ms * 1000; + dependency_real_drain(); + UT_ASSERT_EQ(pg_atomic_read_u32(&slot->state), CLUSTER_LMS_CR_R4_READY_RETRY); + UT_ASSERT_EQ(ut_forget_calls, 0); + UT_ASSERT_EQ(ut_leave_calls, 0); + UT_ASSERT(!cluster_cr_server_r4_land_foreign_undo(&env, &header, page, &auth)); + ut_send_result = CLUSTER_IC_SEND_DONE; + dependency_real_drain(); + UT_ASSERT(slot_is_canonical_free_with_generation(slot, 1)); + UT_ASSERT_EQ(ut_forget_calls, 1); + UT_ASSERT_EQ(ut_leave_calls, 1); + UT_ASSERT(!cluster_cr_server_r4_land_foreign_undo(&env, &header, page, &auth)); +} + +UT_TEST(test_reply_wins_and_maintenance_never_builds_under_origin_guard) +{ + ClusterLmsSharedState state; + ClusterLmsCrSlot *slot = prepare_worker0_undo_inflight(&state); + GcsBlockReplyHeader header; + ClusterGcsUndoAuthTrailer auth; + ClusterICEnvelope env; + char page[BLCKSZ]; + int builds = ut_builder_step_calls; + + make_foreign_undo_reply(&header, &auth, &env, page); + UT_ASSERT(cluster_cr_server_r4_land_foreign_undo(&env, &header, page, &auth)); + dependency_origin_pending = true; + ut_now += (int64)cluster_gcs_reply_timeout_ms * 1000; + dependency_real_drain(); + UT_ASSERT_EQ(pg_atomic_read_u32(&slot->state), CLUSTER_LMS_CR_R4_UNDO_READY); + UT_ASSERT_EQ(ut_builder_step_calls, builds); + UT_ASSERT_EQ(ut_send_calls, 1); +} + +UT_TEST(test_dependency_wrong_worker_or_reused_generation_never_retires) +{ + ClusterLmsSharedState state; + ClusterLmsCrSlot *slot = prepare_worker0_undo_inflight(&state); + ClusterLmsCrSlot before; + + dependency_origin_pending = true; + ut_now += (int64)cluster_gcs_reply_timeout_ms * 1000; + ut_data_worker_id = 1; + before = *slot; + dependency_real_drain(); + UT_ASSERT_EQ(memcmp(slot, &before, sizeof(before)), 0); + ut_data_worker_id = 0; + slot->r4.slot_generation++; + before = *slot; + dependency_real_drain(); + UT_ASSERT_EQ(memcmp(slot, &before, sizeof(before)), 0); + UT_ASSERT_EQ(ut_send_calls, 1); +} + +UT_TEST(test_full_image_fence_loss_returns_zero_body_retry) +{ + ClusterLmsSharedState state; + ClusterLmsCrSlot *slot = prepare_worker0_claim(&state); + + UT_ASSERT(cluster_cr_server_test_r4_claim_queued(0)); + UT_ASSERT(cluster_cr_server_test_r4_build_step(0)); + ut_write_fence_enforcing = true; + ut_write_fence_allowed = false; + UT_ASSERT(cluster_cr_server_test_r4_ship_terminal(0)); + UT_ASSERT_EQ(ut_send_calls, 1); + UT_ASSERT_EQ(((GcsBlockReplyHeader *)ut_send_payload)->status, + GCS_BLOCK_REPLY_R4_RETRYABLE_HOLDER_MOVED); + UT_ASSERT_EQ(((GcsBlockReplyHeader *)ut_send_payload)->page_lsn, 0); + UT_ASSERT(bytes_are(ut_send_payload + sizeof(GcsBlockReplyHeader), BLCKSZ, 0)); + UT_ASSERT(slot_is_canonical_free_with_generation(slot, 1)); +} + +UT_TEST(test_admission_loss_never_sends_image_or_unproved_reply) +{ + ClusterLmsSharedState state; + ClusterLmsCrSlot *slot = prepare_worker0_claim(&state); + + UT_ASSERT(cluster_cr_server_test_r4_claim_queued(0)); + UT_ASSERT(cluster_cr_server_test_r4_build_step(0)); + ut_recheck_ok = false; + UT_ASSERT(cluster_cr_server_test_r4_ship_terminal(0)); + UT_ASSERT_EQ(ut_send_calls, 0); + UT_ASSERT(slot_is_canonical_free_with_generation(slot, 1)); +} + +int +main(void) +{ + dependency_shared = &ut_cr_server_shared; + UT_PLAN(6); + UT_RUN(test_lost_dependency_retires_while_origin_busy_and_stop_sealed); + UT_RUN(test_dependency_terminal_backpressure_retains_owner_until_send); + UT_RUN(test_reply_wins_and_maintenance_never_builds_under_origin_guard); + UT_RUN(test_dependency_wrong_worker_or_reused_generation_never_retires); + UT_RUN(test_full_image_fence_loss_returns_zero_body_retry); + UT_RUN(test_admission_loss_never_sends_image_or_unproved_reply); + UT_DONE(); + return ut_failed_count == 0 ? 0 : 1; +} diff --git a/src/test/cluster_unit/test_cluster_heap_update_temp_lock.c b/src/test/cluster_unit/test_cluster_heap_update_temp_lock.c index 93e01adb15..4175c64253 100644 --- a/src/test/cluster_unit/test_cluster_heap_update_temp_lock.c +++ b/src/test/cluster_unit/test_cluster_heap_update_temp_lock.c @@ -34,6 +34,8 @@ static int pins, releases, cancels, lock_calls, prepares, fault; static TM_Result lock_result; static ClusterUndoRecordPrepareReceipt *outer; static bool content_locked, route_enabled; +static uint64 test_now_us; +static int preparation_phases; static PGAlignedBlock current_page; char *BufferBlocks = current_page.data; Block *LocalBufferBlockPointers; @@ -111,15 +113,24 @@ cluster_heap_prepare_undo_record_exact(uint8 record_type, uint16 capacity, uint1 UT_ASSERT_EQ(capacity, 128); UT_ASSERT_EQ(segment, 1); UT_ASSERT_EQ(offset, 2); - UT_ASSERT_EQ(deadline, 900); + UT_ASSERT_EQ(deadline, preparation_phases ? test_now_us + 900 : 900); UT_ASSERT(UBA_is_invalid(previous)); UT_ASSERT_EQ(receipt->magic, 0); - if (fault == 4) + if (fault == 4 || deadline <= test_now_us) return false; receipt->magic = 2; return true; } +uint64 +cluster_undo_record_prepare_deadline_us(void) +{ + preparation_phases++; + UT_ASSERT(!content_locked); + UT_ASSERT_EQ(pins, 1); + return test_now_us + 900; +} + static int cluster_heap_undo_receipt_errdetail(bool ctrc) { @@ -147,6 +158,8 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, CommandId cid, LockTupleMode pins++; if (fault == 1) siglongjmp(*PG_exception_stack, 1); + if (fault == 6) + test_now_us = 1000; /* The nested producer outlives the cancelled phase. */ return lock_result; } @@ -212,6 +225,8 @@ UT_TEST(real_update_consumer_requalifies_and_preserves_excluded_routes) canonical_binding.segment_id = 1; canonical_binding.slot_offset = 2; fault = 0; + test_now_us = 0; + preparation_phases = 0; pins = 1; cancels = releases = prepares = lock_calls = 0; content_locked = true; @@ -243,11 +258,14 @@ run_adapter(TM_Result expected_result, int failure, bool applied) ItemPointerData tid; volatile bool caught = false; volatile TM_Result result = TM_Invisible; + uint64 deadline = 900; pins = 1; content_locked = false; releases = cancels = lock_calls = prepares = 0; fault = failure; + test_now_us = 0; + preparation_phases = 0; lock_result = expected_result; receipt.magic = 1; receipt.ctrc_applied_mask = applied ? 1 : 0; @@ -258,14 +276,20 @@ run_adapter(TM_Result expected_result, int failure, bool applied) PG_TRY(); { result = cluster_heap_lock_update_predecessor((Relation)1, &tid, 7, LockTupleNoKeyExclusive, - LockWaitBlock, &binding, 900, &receipt); + LockWaitBlock, &binding, +#ifdef PREPARE_RESUME_HAS_PHASE + &deadline, +#else + deadline, +#endif + &receipt); } PG_CATCH(); { caught = true; } PG_END_TRY(); - UT_ASSERT_EQ(caught, applied || failure != 0); + UT_ASSERT_EQ(caught, applied || (failure != 0 && failure != 6)); UT_ASSERT_EQ(pins, 1); UT_ASSERT_EQ(lock_calls, applied || failure == 3 ? 0 : 1); UT_ASSERT_EQ(releases, applied || failure == 3 || failure == 5 ? 0 : 1); @@ -274,10 +298,70 @@ run_adapter(TM_Result expected_result, int failure, bool applied) if (!caught) { UT_ASSERT_EQ(result, expected_result); UT_ASSERT_EQ(receipt.magic, 2); + UT_ASSERT_EQ(deadline, test_now_us + 900); + } +} + +UT_TEST(resume_never_replaces_a_live_receipt_or_renews_without_a_handoff) +{ + ClusterUndoRecordPrepareReceipt receipt = { 0 }; + ClusterCanonicalTxnBinding binding = { 0 }; + uint64 deadline = 900; + + prepares = preparation_phases = 0; + receipt.magic = 1; + UT_ASSERT(!cluster_heap_resume_update_undo_record_exact(&binding, &deadline, &receipt)); + UT_ASSERT(!cluster_heap_resume_update_undo_record_exact(NULL, &deadline, &receipt)); + UT_ASSERT(!cluster_heap_resume_update_undo_record_exact(&binding, NULL, &receipt)); + UT_ASSERT(!cluster_heap_resume_update_undo_record_exact(&binding, &deadline, NULL)); + UT_ASSERT_EQ(receipt.magic, 1); + receipt.magic = 0; + receipt.ctrc_applied_mask = 1; + UT_ASSERT(!cluster_heap_resume_update_undo_record_exact(&binding, &deadline, &receipt)); + UT_ASSERT_EQ(receipt.ctrc_applied_mask, 1); + UT_ASSERT_EQ(deadline, 900); + UT_ASSERT_EQ(prepares, 0); + UT_ASSERT_EQ(preparation_phases, 0); +} + +UT_TEST(completed_nested_producer_starts_its_own_preparation_phase) +{ + run_adapter(TM_Ok, 6, false); + UT_ASSERT_EQ(preparation_phases, 1); +} + +UT_TEST(real_toast_return_uses_the_completed_handoff_boundary) +{ + for (int leg = 0; leg < 2; leg++) { + ClusterUndoRecordPrepareReceipt undo_receipt = { 0 }; + ClusterCanonicalTxnBinding canonical_binding = { 0 }; + uint64 undo_prepare_deadline_us = 900; + bool resume_update_receipt = leg == 0; + volatile bool caught = false; + + canonical_binding.segment_id = 1; + canonical_binding.slot_offset = 2; + pins = 1; + content_locked = false; + test_now_us = 1000; + preparation_phases = prepares = fault = 0; + PG_TRY(); + { +#include "test_cluster_heap_update_toast_resume.inc" + } + PG_CATCH(); + { + caught = true; + } + PG_END_TRY(); + UT_ASSERT(!caught); + UT_ASSERT_EQ(prepares, resume_update_receipt ? 1 : 0); + UT_ASSERT_EQ(preparation_phases, prepares); + UT_ASSERT_EQ(undo_prepare_deadline_us, resume_update_receipt ? 1900 : 900); } } -UT_TEST(success_preserves_outer_pin_and_original_budget) +UT_TEST(success_preserves_outer_pin_and_fixes_new_phase_budget) { run_adapter(TM_Ok, 0, false); } @@ -401,12 +485,15 @@ UT_TEST(real_successor_preserves_all_other_planner_branches) int main(void) { - UT_PLAN(12); + UT_PLAN(15); + UT_RUN(resume_never_replaces_a_live_receipt_or_renews_without_a_handoff); + UT_RUN(real_toast_return_uses_the_completed_handoff_boundary); + UT_RUN(completed_nested_producer_starts_its_own_preparation_phase); UT_RUN(real_successor_does_not_inherit_own_temporary_lock); UT_RUN(real_successor_preserves_all_other_planner_branches); UT_RUN(real_row_lock_return_cancels_only_unpublished_receipt); UT_RUN(real_update_consumer_requalifies_and_preserves_excluded_routes); - UT_RUN(success_preserves_outer_pin_and_original_budget); + UT_RUN(success_preserves_outer_pin_and_fixes_new_phase_budget); UT_RUN(updated_is_not_success); UT_RUN(would_block_is_not_success); UT_RUN(error_after_pin_releases_only_nested_pin); diff --git a/src/test/cluster_unit/test_cluster_r4_itl_capacity.c b/src/test/cluster_unit/test_cluster_r4_itl_capacity.c index 8bd452e669..207697bcd2 100644 --- a/src/test/cluster_unit/test_cluster_r4_itl_capacity.c +++ b/src/test/cluster_unit/test_cluster_r4_itl_capacity.c @@ -246,10 +246,67 @@ UT_TEST(test_lock_capacity_does_not_mistake_self_data_slot_for_lock_capacity) } } +UT_TEST(test_deadlock_retains_its_typed_cause_without_relabeling_as_authority) +{ + UtR4HotProductFixture fixture; + HeapHotSearchResult hot; + PGAlignedBlock before; + uint64 deadline = 0; + const char *reason = NULL; + + ut_itl_census_begin(&fixture, &hot, true); + origin_fault = 0; + ut_itl_wait_result = CLUSTER_TXW_DEADLOCK; + pg_atomic_write_u64(&ut_itl_census_semantic.active_bits, + CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1); + memcpy(before.data, fixture.live_page, BLCKSZ); + ut_itl_capacity_log[0] = '\0'; + ut_capture_miss_log = true; + UT_ASSERT_EQ(cluster_heap_test_itl_wait_capacity(1, 1, 1, 9900, &deadline, &reason), + CLUSTER_TXW_DEADLOCK); + ut_capture_miss_log = false; + UT_ASSERT_STR_EQ(reason, "ITL_WAIT_DEADLOCK"); + UT_ASSERT(strstr(ut_itl_capacity_log, "waiter_xid=9900 blocker_xid=1200") != NULL); + UT_ASSERT(strstr(ut_itl_capacity_log, "stage=WAIT_TERMINAL") != NULL); + UT_ASSERT(strstr(ut_itl_capacity_log, "slot=7 xid=1207") != NULL); + UT_ASSERT_EQ(ut_itl_wait_calls, 1); + UT_ASSERT_EQ(ut_itl_census_dirty_hint_calls, 0); + UT_ASSERT_EQ(memcmp(before.data, fixture.live_page, BLCKSZ), 0); + UT_ASSERT(!ut_hot_content_lock_held); + LockBuffer(1, BUFFER_LOCK_EXCLUSIVE); + ut_itl_census_end(); +} + +UT_TEST(test_confirmed_deadlock_wins_over_simultaneous_capacity_deadline) +{ + UtR4HotProductFixture fixture; + HeapHotSearchResult hot; + uint64 deadline = 0; + const char *reason = NULL; + int saved_budget = cluster_ges_request_timeout_ms; + + ut_itl_census_begin(&fixture, &hot, true); + origin_fault = 0; + ut_itl_wait_result = CLUSTER_TXW_DEADLOCK; + ut_itl_wait_past_budget = true; + cluster_ges_request_timeout_ms = 1; + pg_atomic_write_u64(&ut_itl_census_semantic.active_bits, + CLUSTER_SEMANTIC_FEATURE_R4_SYNC_CR_V1); + UT_ASSERT_EQ(cluster_heap_test_itl_wait_capacity(1, 1, 1, 9900, &deadline, &reason), + CLUSTER_TXW_DEADLOCK); + UT_ASSERT_STR_EQ(reason, "ITL_WAIT_DEADLOCK"); + UT_ASSERT_EQ(ut_itl_wait_calls, 1); + cluster_ges_request_timeout_ms = saved_budget; + LockBuffer(1, BUFFER_LOCK_EXCLUSIVE); + ut_itl_census_end(); +} + int main(void) { - UT_PLAN(4); + UT_PLAN(6); + UT_RUN(test_confirmed_deadlock_wins_over_simultaneous_capacity_deadline); + UT_RUN(test_deadlock_retains_its_typed_cause_without_relabeling_as_authority); UT_RUN(test_exact_active_owner_reaches_capacity_wait_through_real_resolver); UT_RUN(test_capacity_keeps_unknown_identity_prepared_and_admission_refusals); UT_RUN(test_origin_abort_releases_eight_lock_only_slots_through_real_census); 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 fd13985c2d..6ffbc4cb57 100644 --- a/src/test/cluster_unit/test_cluster_r4_lock_order.c +++ b/src/test/cluster_unit/test_cluster_r4_lock_order.c @@ -402,6 +402,7 @@ cluster_vis_evidence_note(ClusterVisEvidenceMetric metric) ut_evidence_metrics[metric]++; } static ClusterTxwResult ut_itl_wait_result = CLUSTER_TXW_RESOLVED; +static bool ut_itl_wait_past_budget; static bool ut_itl_pair_active; static bool ut_itl_pair_content_lock_held[2]; static Buffer ut_itl_pair_lock_buffers[4]; @@ -431,6 +432,8 @@ cluster_tx_enqueue_wait_exact(const ClusterTxLocator *locator, int effective_tim UT_ASSERT(!ut_itl_recycle_guard_active); UT_ASSERT_EQ(semantic_activation_local_inflight[CLUSTER_SEMANTIC_TARGET_SIDE][0], 0); UT_ASSERT(effective_timeout_ms > 0); + if (ut_itl_wait_past_budget) + pg_usleep((long)(effective_timeout_ms + 20) * 1000L); *reason_out = ut_itl_wait_result == CLUSTER_TXW_TIMEOUT ? CLUSTER_TX_RESOLVE_TIMEOUT : CLUSTER_TX_RESOLVE_NONE; return ut_itl_wait_result; @@ -4240,6 +4243,7 @@ ut_itl_census_begin(UtR4HotProductFixture *fixture, HeapHotSearchResult *result, memset(ut_evidence_metrics, 0, sizeof(ut_evidence_metrics)); memset(&ut_itl_wait_locator, 0, sizeof(ut_itl_wait_locator)); ut_itl_wait_result = CLUSTER_TXW_RESOLVED; + ut_itl_wait_past_budget = false; } static void 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 66f5fc686d..f79ab0e404 100644 --- a/src/test/cluster_unit/test_cluster_r4_route_policy.c +++ b/src/test/cluster_unit/test_cluster_r4_route_policy.c @@ -4868,6 +4868,43 @@ UT_TEST(test_forward96_all_local_refuses_open_generation_overflow_before_submit) /* A holder-side typed failure is consumed only after the submit result and * the decoder's final token recheck. Reusing the master-side refusal shape, * replying to env.source, or publishing after leave breaks this boundary. */ +UT_TEST(test_refusal_queue_nonadmission_is_counted_at_each_producer) +{ + ClusterR4CrRequestPayload request = route_test_request80(); + ClusterR4CrForwardPayload forward = route_test_forward96(); + ClusterICEnvelope env; + int saved_node_id = cluster_node_id; + + for (int phase = 0; phase < 3; phase++) { + UT_ASSERT(cluster_gcs_block_test_arm_r4_reply_slot(UT_REQUEST_ID, UT_FORMATION_EPOCH, 1, + PCM_TRANS_N_TO_S, UT_MASTER_NODE)); + route_seam_reset(); + route_seam.refusal_enqueue_ok = false; + if (phase == 2) { + cluster_node_id = UT_HOLDER_NODE; + route_seam.holder_submit_result = CLUSTER_CR_BUILD_RETRYABLE; + route_seam.holder_submit_reason = CLUSTER_CR_BUILD_CAPACITY; + env = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_FORWARD, UT_MASTER_NODE, + UT_HOLDER_NODE, sizeof(forward)); + UT_ASSERT(cluster_gcs_block_test_r4_forward96(&env, &forward)); + } else { + cluster_node_id = UT_MASTER_NODE; + if (phase == 0) + route_seam.admission_result = CLUSTER_SEMANTIC_ADMISSION_CLOSED; + else + route_seam.peer_open_ok = false; + env = route_test_envelope(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, UT_REQUESTER_NODE, + UT_MASTER_NODE, sizeof(request)); + UT_ASSERT(cluster_gcs_block_test_r4_request80(&env, &request)); + } + UT_ASSERT_EQ(route_seam.refusal_enqueue_calls, 1); + UT_ASSERT_EQ(cluster_gcs_get_reply_send_not_admitted_count(), 1); + /* A full ring is not a successful delivery or a second transport send. */ + UT_ASSERT_EQ(route_seam.raw_send_calls, 0); + } + cluster_node_id = saved_node_id; +} + UT_TEST(test_forward96_holder_submit_failure_publishes_typed_remote_refusal) { static const struct { @@ -6129,7 +6166,8 @@ UT_TEST(test_internal_origin_refusals_do_not_enter_backend_reply_table) int main(void) { - UT_PLAN(117); + UT_PLAN(118); + UT_RUN(test_refusal_queue_nonadmission_is_counted_at_each_producer); UT_RUN(test_seal_two_blocks_new_tx_and_undo_contexts_but_not_original_drain); UT_RUN(test_kind2_requester_asks_origin_to_select_and_lands_exact_status22); UT_RUN(test_kind2_origin_generation_selection_and_strict_known_negatives); diff --git a/src/test/cluster_unit/test_cluster_r4_slot_reservation.c b/src/test/cluster_unit/test_cluster_r4_slot_reservation.c index ae1deb1959..e8366565a2 100644 --- a/src/test/cluster_unit/test_cluster_r4_slot_reservation.c +++ b/src/test/cluster_unit/test_cluster_r4_slot_reservation.c @@ -26,6 +26,7 @@ #include "cluster/storage/cluster_undo_block0_current.h" #include "storage/latch.h" #include "storage/shmem.h" +#include "utils/timestamp.h" #undef printf #undef snprintf @@ -38,6 +39,15 @@ UT_DEFINE_GLOBALS(); +int cluster_gcs_reply_timeout_ms = 3000; +static TimestampTz ut_now = INT64CONST(1000000); + +TimestampTz +GetCurrentTimestamp(void) +{ + return ut_now; +} + extern bool cluster_cr_server_test_reserve_legacy_slot(ClusterLmsCrSlot *slot, uint32 reserved_state); /* Expected USE_CLUSTER_UNIT claim-only seams. They expose no builder step. */ @@ -357,6 +367,12 @@ errmsg(const char *format pg_attribute_unused(), ...) return 0; } +int +errdetail(const char *format pg_attribute_unused(), ...) +{ + return 0; +} + int errmsg_internal(const char *format pg_attribute_unused(), ...) { @@ -947,6 +963,7 @@ submit_test_admission(void) static void reset_submit_fixture(ClusterLmsSharedState *state) { + ut_now = INT64CONST(1000000); ut_stop_new_work_allowed = true; ut_stop_new_work_calls = 0; ut_stop_poll_during_forget = false; diff --git a/src/test/cluster_unit/test_cluster_undo_record.c b/src/test/cluster_unit/test_cluster_undo_record.c index 3698963611..55c42ad696 100644 --- a/src/test/cluster_unit/test_cluster_undo_record.c +++ b/src/test/cluster_unit/test_cluster_undo_record.c @@ -2042,9 +2042,9 @@ UT_TEST(test_heap_prepare_retries_transient_result_under_one_deadline) wrapper_calls++; for (hit = source; (hit = strstr(hit, "cluster_undo_record_prepare(")) != NULL; hit++) raw_prepare_calls++; - /* Definition + retry wrapper + unit seam + five initial producer sites. - * Existing retry sites now preserve READY through their dedicated owner. */ - UT_ASSERT_EQ(helper_mentions, 10); /* Explicit TOAST and ordinary row-lock resume. */ + /* Definition, retry wrapper, unit seam, five initial producer sites, + * and the shared post-nested-producer preparation phase. */ + UT_ASSERT_EQ(helper_mentions, 9); UT_ASSERT_EQ(wrapper_calls, 2); UT_ASSERT_EQ(raw_prepare_calls, 0); free(source); @@ -2135,9 +2135,7 @@ UT_TEST(test_all_heap_dml_callers_reprepare_outside_content_lock) if (source == NULL) return; helper = strstr(source, "\ncluster_heap_prepare_undo_record_exact("); - helper_end = helper == NULL - ? NULL - : strstr(helper, "\n}\n\ntypedef enum ClusterHeapPreparedUndoResult"); + helper_end = helper == NULL ? NULL : strstr(helper, "\n}\n"); deadline_parameter = helper == NULL ? NULL : strstr(helper, "uint64 absolute_deadline_us"); UT_ASSERT_NOT_NULL(helper); @@ -2938,13 +2936,13 @@ UT_TEST(test_update_toast_releases_outer_receipt_before_nested_producers) toast = update == NULL ? NULL : strstr(update, "heaptup = heap_toast_insert_or_update("); release = update == NULL ? NULL : strstr(update, "cluster_undo_record_cancel_prepared(&undo_receipt)"); - resume = toast == NULL ? NULL : strstr(toast, "cluster_heap_prepare_undo_record_exact("); + resume = toast == NULL ? NULL : strstr(toast, "cluster_heap_resume_update_undo_record_exact("); relock = toast == NULL ? NULL : strstr(toast, "l_pgrac_reacquire:"); UT_ASSERT(update != NULL && release != NULL && toast != NULL && release < toast); UT_ASSERT(resume != NULL && relock != NULL && toast < resume && resume < relock); if (resume != NULL && relock != NULL) { - const char *original_budget = strstr(resume, "undo_prepare_deadline_us"); - UT_ASSERT(original_budget != NULL && original_budget < relock); + const char *phase_budget = strstr(resume, "&undo_prepare_deadline_us"); + UT_ASSERT(phase_budget != NULL && phase_budget < relock); } free(source); } diff --git a/src/tools/check_r11_source_removal_census.py b/src/tools/check_r11_source_removal_census.py index 1e37cceb4c..674ef63b07 100644 --- a/src/tools/check_r11_source_removal_census.py +++ b/src/tools/check_r11_source_removal_census.py @@ -24,7 +24,7 @@ CURRENT_PRODUCT_SNAPSHOT = { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "a33690006785ed01f7bfef085653bd6b916ebe970de64d555c3436e9d11a4e3b", + "sha256": "696411d2f2a36a4d32c9a08740ab5eeb3221f61e6f57ccee2109d225badb0554", } LAYERS = { From c789e55b589f7827e84adcb7cc6c91bf00fe4c08 Mon Sep 17 00:00:00 2001 From: SqlRush Date: Fri, 18 Sep 2026 20:34:49 +0800 Subject: [PATCH 05/10] fix(cluster): close UPDATE slot and GES cancellation lifecycles --- src/backend/access/heap/heapam.c | 40 +++- src/backend/access/heap/heapam_r4_private.h | 2 + src/backend/cluster/cluster_ges.c | 35 ++-- src/backend/cluster/cluster_grd.c | 45 +++- src/backend/cluster/cluster_itl.c | 92 +++++++++ src/backend/storage/lmgr/lock.c | 8 +- src/include/cluster/cluster_grd.h | 9 + src/include/cluster/cluster_itl.h | 6 + src/test/cluster_unit/Makefile | 2 +- src/test/cluster_unit/ctrc_source_census.tsv | 1 + .../data/r11-source-removal-census-v1.json | 2 +- .../generate_ctrc_source_census.py | 2 +- src/test/cluster_unit/test_cluster_ges.c | 112 +++++++++- src/test/cluster_unit/test_cluster_grd.c | 38 +++- .../cluster_unit/test_cluster_heap_epq_wait.c | 2 +- .../cluster_unit/test_cluster_heap_receipt.c | 42 +++- .../test_cluster_itl_reader_real_triple.c | 194 +++++++++++++++--- .../cluster_unit/test_cluster_r4_cr_walk.c | 21 +- .../cluster_unit/test_cluster_r4_lock_order.c | 6 + src/test/cluster_unit/test_cluster_shmem.c | 7 + src/tools/check_r11_source_removal_census.py | 2 +- 21 files changed, 586 insertions(+), 82 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index de23f3e912..9cff88a559 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -1865,6 +1865,13 @@ cluster_heap_update_needs_successor_prediction(bool current_itl_path, return current_itl_path || current_mx_recomposed; } +static bool +cluster_heap_update_lock_handoff_allowed(bool temp_locked, uint16 successor_infomask) +{ + /* Keep an application lock carrier if the new row would inherit it. */ + return temp_locked && (successor_infomask & HEAP_XMAX_INVALID) != 0; +} + #ifdef USE_CLUSTER_UNIT bool @@ -1889,6 +1896,12 @@ cluster_heap_test_update_needs_successor_prediction( current_itl_path, current_mx_recomposed); } +bool +cluster_heap_test_update_lock_handoff_allowed(bool temp_locked, uint16 successor_infomask) +{ + return cluster_heap_update_lock_handoff_allowed(temp_locked, successor_infomask); +} + bool cluster_heap_test_itl_relation_route( bool storage_mode, bool uses_local_buffers, bool shared_catalog, @@ -2052,7 +2065,7 @@ cluster_heap_capture_undo_prior_lock(Page page, uint8 record_type, uint8 target_ static ClusterHeapPreparedUndoResult cluster_heap_itl_plan_prepared_undo_target( Relation relation, Buffer buffer, HeapTuple tuple, TransactionId xid, - bool lock_only, ClusterUndoRecordPrepareReceipt *receipt, + bool lock_only, bool allow_lock_handoff, ClusterUndoRecordPrepareReceipt *receipt, uint8 target_ordinal, void *payload, uint16 payload_len, ClusterHeapPreparedUndoTargetPlan *plan) { @@ -2074,8 +2087,12 @@ cluster_heap_itl_plan_prepared_undo_target( return receipt->ctrc_applied_mask == 0 ? CLUSTER_HEAP_PREPARED_UNDO_RETRY_REQUIRED : CLUSTER_HEAP_PREPARED_UNDO_REFUSED; - if (!cluster_heap_itl_alloc_once( - buffer, xid, lock_only, &plan->slot_index)) + if (!(allow_lock_handoff && receipt->record_type == UNDO_RECORD_UPDATE && target_ordinal == 0 + && !lock_only && tuple != NULL + ? cluster_itl_alloc_update_slot(buffer, xid, + ItemPointerGetOffsetNumber(&tuple->t_self), &plan->slot_index) + : cluster_heap_itl_alloc_once( + buffer, xid, lock_only, &plan->slot_index))) return CLUSTER_HEAP_PREPARED_UNDO_REFUSED; if (!cluster_heap_dml_authority_guard_bind_itl_slot( buffer, plan->slot_index, &plan->guard)) @@ -6046,7 +6063,7 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, undo_plan_result = CLUSTER_HEAP_PREPARED_UNDO_RETRY_REQUIRED; if (OffsetNumberIsValid(undo_target.offnum)) undo_plan_result = cluster_heap_itl_plan_prepared_undo_target( - relation, buffer, NULL, canonical_xid, false, + relation, buffer, NULL, canonical_xid, false, false, &undo_receipt, 0, &undo_payload, sizeof(undo_payload), &undo_plan); if (undo_plan_result == CLUSTER_HEAP_PREPARED_UNDO_READY) @@ -11186,7 +11203,7 @@ heap_delete(Relation relation, ItemPointer tid, undo_target.blockno = ItemPointerGetBlockNumber(&tp.t_self); undo_target.offnum = ItemPointerGetOffsetNumber(&tp.t_self); undo_plan_result = cluster_heap_itl_plan_prepared_undo_target( - relation, buffer, &tp, canonical_xid, false, &undo_receipt, + relation, buffer, &tp, canonical_xid, false, false, &undo_receipt, 0, undo_payload_buf, undo_payload_len, &undo_plan); if (undo_plan_result == CLUSTER_HEAP_PREPARED_UNDO_READY) { @@ -13043,6 +13060,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, uint16 tt_off; bool census_was_pending = cluster_itl_update_census_pending; bool old_capacity; + uint8 old_capacity_slot; ClusterHeapItlCapacityResult old_capacity_result; ClusterHeapDmlAuthorityGuard old_dml_guard; ClusterHeapDmlAuthorityGuard new_dml_guard; @@ -13108,6 +13126,10 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, if (census_was_pending && census_apply_result.kind == CLUSTER_HEAP_ITL_BATCH_STALE_CURRENT_X) old_capacity_result = CLUSTER_HEAP_ITL_CAPACITY_RETRY_REQUALIFY; + else if (cluster_heap_update_lock_handoff_allowed(old_tuple_temp_locked, infomask_new_tuple) + && cluster_itl_alloc_update_slot(buffer, canonical_xid, + ItemPointerGetOffsetNumber(&oldtup.t_self), &old_capacity_slot)) + old_capacity_result = CLUSTER_HEAP_ITL_CAPACITY_READY; else if (newbuf == buffer && !census_was_pending) old_capacity_result = cluster_heap_itl_ensure_capacity_with_terminal_census( @@ -13872,6 +13894,8 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, undo_plan_result = cluster_heap_itl_plan_prepared_undo_target( relation, buffer, &oldtup, canonical_xid, false, + cluster_heap_update_lock_handoff_allowed(old_tuple_temp_locked, + infomask_new_tuple), &undo_receipt, 0, undo_payload_buf, cluster_itl_undo_payload_len, &undo_plans[0]); if (undo_plan_result == CLUSTER_HEAP_PREPARED_UNDO_READY) @@ -13889,7 +13913,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, { undo_plan_result = cluster_heap_itl_plan_prepared_undo_target( - relation, newbuf, NULL, canonical_xid, false, + relation, newbuf, NULL, canonical_xid, false, false, &undo_receipt, 1, undo_payload_buf, cluster_itl_undo_payload_len, &undo_plans[1]); if (undo_plan_result @@ -16272,7 +16296,7 @@ heap_lock_tuple_internal(Relation relation, HeapTuple tuple, undo_payload.lock_mode = (uint8) mode; undo_payload.lock_xid = xid; undo_plan_result = cluster_heap_itl_plan_prepared_undo_target( - relation, *buffer, tuple, canonical_xid, true, + relation, *buffer, tuple, canonical_xid, true, false, &undo_receipt, 0, &undo_payload, sizeof(undo_payload), &undo_plan); if (undo_plan_result == CLUSTER_HEAP_PREPARED_UNDO_READY) @@ -17629,7 +17653,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, undo_payload.lock_mode = (uint8) mode; undo_payload.lock_xid = xid; plan_result = cluster_heap_itl_plan_prepared_undo_target( - rel, buf, &mytup, canonical_xid, true, + rel, buf, &mytup, canonical_xid, true, false, &cluster_chain_receipt, 0, &undo_payload, sizeof(undo_payload), &undo_plan); if (plan_result diff --git a/src/backend/access/heap/heapam_r4_private.h b/src/backend/access/heap/heapam_r4_private.h index 891ac9d5c0..e6048217c7 100644 --- a/src/backend/access/heap/heapam_r4_private.h +++ b/src/backend/access/heap/heapam_r4_private.h @@ -333,6 +333,8 @@ extern ClusterHeapMultiInsertRoute cluster_heap_test_multi_insert_route( bool current_itl_path); extern bool cluster_heap_test_update_needs_successor_prediction( bool current_itl_path, bool current_mx_recomposed); +extern bool cluster_heap_test_update_lock_handoff_allowed(bool temp_locked, + uint16 successor_infomask); extern bool cluster_heap_test_itl_relation_route( bool storage_mode, bool uses_local_buffers, bool shared_catalog, RelFileNumber rel_number); diff --git a/src/backend/cluster/cluster_ges.c b/src/backend/cluster/cluster_ges.c index 120a1e9b7d..4919c50b5b 100644 --- a/src/backend/cluster/cluster_ges.c +++ b/src/backend/cluster/cluster_ges.c @@ -455,6 +455,10 @@ ges_validate_inbound(const ClusterICEnvelope *env, uint32 payload_node_id, uint6 return true; } +static void ges_dispatch_reject(int32 source_node_id, const ClusterGrdHolderId *holder, + const ClusterResId *resid, uint32 reply_for_opcode, + uint32 reject_reason, uint64 shard_master_generation); + void cluster_ges_request_handler(const ClusterICEnvelope *env, const void *payload) { @@ -590,6 +594,7 @@ cluster_ges_request_handler(const ClusterICEnvelope *env, const void *payload) ClusterResId resid; ClusterGrdHolderId waiter; ClusterGrdEntryResult er; + ClusterGrdGrantIdentity cancelled; /* * spec-5.9 D4 — dedicated 64B payload, early-dispatched so it never hits @@ -614,15 +619,22 @@ cluster_ges_request_handler(const ClusterICEnvelope *env, const void *payload) waiter.request_id = cw->waiter_request_id; if (cw->kind == GES_CANCEL_WAIT_KIND_CONVERT) - er = cluster_grd_cancel_convert_by_id(&resid, &waiter, cw->wait_seq); + er = cluster_grd_cancel_convert_exact(&resid, &waiter, cw->wait_seq, &cancelled); else - er = cluster_grd_cancel_waiter_by_id_seq(&resid, &waiter, cw->wait_seq); + er = cluster_grd_cancel_waiter_exact(&resid, &waiter, cw->wait_seq, &cancelled); /* NOT_FOUND => the named waiter is gone or its wait_seq no longer matches * (stale / retransmitted CANCEL_WAIT after slot reuse) — never dequeue a * since-reused identity (Rule 8.A P0#2). */ if (er != CLUSTER_GRD_ENTRY_OK) cluster_lmd_cancel_wait_stale_rejected_count_inc(1); + else + /* Dequeue is terminal, not an in-flight dedup record forever. + * Use only the removed request's original routing/generation after + * releasing GRD locks. A grant-won race never reaches this branch. */ + ges_dispatch_reject(cancelled.source_node_id, &cancelled.holder, &resid, + cancelled.request_opcode, GES_REJECT_REASON_TIMEOUT, + cancelled.shard_master_generation); /* spec-5.9 D5 — the correlated CANCEL_ACK back to the sender lands here * (cancel_id is carried on the wire for that purpose). */ @@ -2579,7 +2591,8 @@ ges_send_request_opcode_and_wait(const struct ClusterResId *resid, uint32 lockmo * >0 → caller-supplied finite timeout. * * cluster.ges_retransmit_max_attempts (HC52): - * finite mode: abort with 53R70 after attempts exhausted. + * finite mode: stop retransmitting after attempts exhausted; keep + * waiting within the original caller deadline. * perpetual: warning threshold only (priority starvation observability). */ epoch = cluster_epoch_get_current(); @@ -2773,17 +2786,11 @@ ges_send_request_opcode_and_wait(const struct ClusterResId *resid, uint32 lockmo } if (!perpetual && max_attempts > 0 && attempt > max_attempts) { - if (hw_grant != NULL) - cluster_ges_hw_grant_abandon(hw_grant); - else - ges_abandon_wait_or_release(&key, &req, master, send_opcode); - ConditionVariableCancelSleep(); - cluster_xp_end(&xp_wait); /* PGRAC: spec-5.59 D2 profiling */ - cluster_xp_end(&xp_enqueue); /* PGRAC: spec-5.59 D2 profiling */ - cluster_ges_timeout_detail_set(CLUSTER_GES_TSRC_RETRANSMIT_EXHAUSTED, master, - ges_forens_elapsed_ms(forens_start), attempt, -1, - effective_timeout_ms); - return GES_REJECT_REASON_TIMEOUT; + /* Retransmission exhaustion is not a terminal lock verdict. The + * original waiter still owns this request; only its existing deadline + * or another explicit terminal condition can abandon it. */ + backoff_ms = 1600; + continue; } /* HC54 priority starvation observability — two one-shot events per diff --git a/src/backend/cluster/cluster_grd.c b/src/backend/cluster/cluster_grd.c index 906889518e..643cde4c90 100644 --- a/src/backend/cluster/cluster_grd.c +++ b/src/backend/cluster/cluster_grd.c @@ -8341,12 +8341,14 @@ cluster_grd_cancel_reservation_by_id(const ClusterResId *resid, const ClusterGrd */ static ClusterGrdEntryResult grd_cancel_waiter_impl(const ClusterResId *resid, const ClusterGrdHolderId *holder, uint64 wait_seq, - bool match_wait_seq) + bool match_wait_seq, ClusterGrdGrantIdentity *cancelled_out) { ClusterGrdEntry *entry = NULL; ClusterGrdEntryResult er = CLUSTER_GRD_ENTRY_NOT_FOUND; Assert(resid != NULL && holder != NULL); + if (cancelled_out != NULL) + memset(cancelled_out, 0, sizeof(*cancelled_out)); if (cluster_grd_entry_lookup_or_create(resid, false, &entry) != CLUSTER_GRD_ENTRY_OK || entry == NULL) @@ -8359,6 +8361,14 @@ grd_cancel_waiter_impl(const ClusterResId *resid, const ClusterGrdHolderId *hold && entry->waiters[i].cluster_epoch == holder->cluster_epoch && entry->waiters[i].request_id == holder->request_id && (!match_wait_seq || entry->waiters[i].wait_seq == wait_seq)) { + if (cancelled_out != NULL) { + const ClusterGrdWaiter *waiter = &entry->waiters[i]; + cancelled_out->holder = *holder; + cancelled_out->source_node_id = waiter->source_node_id; + cancelled_out->request_opcode = waiter->request_opcode; + cancelled_out->shard_master_generation = waiter->shard_master_generation; + cancelled_out->mode = waiter->mode; + } if (i < entry->nwaiters - 1) entry->waiters[i] = entry->waiters[entry->nwaiters - 1]; memset(&entry->waiters[entry->nwaiters - 1], 0, sizeof(ClusterGrdWaiter)); @@ -8379,7 +8389,7 @@ grd_cancel_waiter_impl(const ClusterResId *resid, const ClusterGrdHolderId *hold ClusterGrdEntryResult cluster_grd_cancel_waiter_by_id(const ClusterResId *resid, const ClusterGrdHolderId *holder) { - return grd_cancel_waiter_impl(resid, holder, 0, false); + return grd_cancel_waiter_impl(resid, holder, 0, false, NULL); } /* @@ -8391,7 +8401,15 @@ ClusterGrdEntryResult cluster_grd_cancel_waiter_by_id_seq(const ClusterResId *resid, const ClusterGrdHolderId *holder, uint64 wait_seq) { - return grd_cancel_waiter_impl(resid, holder, wait_seq, true); + return grd_cancel_waiter_impl(resid, holder, wait_seq, true, NULL); +} + +ClusterGrdEntryResult +cluster_grd_cancel_waiter_exact(const ClusterResId *resid, const ClusterGrdHolderId *holder, + uint64 wait_seq, ClusterGrdGrantIdentity *cancelled_out) +{ + Assert(cancelled_out != NULL); + return grd_cancel_waiter_impl(resid, holder, wait_seq, true, cancelled_out); } /* @@ -8404,13 +8422,15 @@ cluster_grd_cancel_waiter_by_id_seq(const ClusterResId *resid, const ClusterGrdH * waiter variant). */ ClusterGrdEntryResult -cluster_grd_cancel_convert_by_id(const ClusterResId *resid, const ClusterGrdHolderId *holder, - uint64 wait_seq) +cluster_grd_cancel_convert_exact(const ClusterResId *resid, const ClusterGrdHolderId *holder, + uint64 wait_seq, ClusterGrdGrantIdentity *cancelled_out) { ClusterGrdEntry *entry = NULL; ClusterGrdEntryResult er = CLUSTER_GRD_ENTRY_NOT_FOUND; Assert(resid != NULL && holder != NULL); + if (cancelled_out != NULL) + memset(cancelled_out, 0, sizeof(*cancelled_out)); if (cluster_grd_entry_lookup_or_create(resid, false, &entry) != CLUSTER_GRD_ENTRY_OK || entry == NULL) @@ -8423,6 +8443,14 @@ cluster_grd_cancel_convert_by_id(const ClusterResId *resid, const ClusterGrdHold && entry->converts[i].cluster_epoch == holder->cluster_epoch && entry->converts[i].convert_request_id == holder->request_id && entry->converts[i].wait_seq == wait_seq) { + if (cancelled_out != NULL) { + const ClusterGrdConvert *convert = &entry->converts[i]; + cancelled_out->holder = *holder; + cancelled_out->source_node_id = convert->source_node_id; + cancelled_out->request_opcode = convert->request_opcode; + cancelled_out->shard_master_generation = convert->shard_master_generation; + cancelled_out->mode = convert->requested_mode; + } grd_convert_remove(entry, i); er = CLUSTER_GRD_ENTRY_OK; break; @@ -8436,3 +8464,10 @@ cluster_grd_cancel_convert_by_id(const ClusterResId *resid, const ClusterGrdHold grd_wfg_resync_entry(resid, holder, 1); return er; } + +ClusterGrdEntryResult +cluster_grd_cancel_convert_by_id(const ClusterResId *resid, const ClusterGrdHolderId *holder, + uint64 wait_seq) +{ + return cluster_grd_cancel_convert_exact(resid, holder, wait_seq, NULL); +} diff --git a/src/backend/cluster/cluster_itl.c b/src/backend/cluster/cluster_itl.c index 2cccbf7a21..f1e9b62676 100644 --- a/src/backend/cluster/cluster_itl.c +++ b/src/backend/cluster/cluster_itl.c @@ -710,6 +710,88 @@ cluster_itl_alloc_or_reuse_lock_slot(Buffer buf, TransactionId top_xid, uint8 *o return false; } +/* + * Select an UPDATE DATA carrier without acquiring a ninth slot for its own + * sole predecessor lock. Inputs are the content-X buffer, top xid and exact + * old tuple offset; output is a slot index or UNALLOCATED on refusal. + * This function changes no bytes and has no wait or allocation side effects. + * Author: SqlRush + * + * The caller will replace old_offset's plain lock header in the same UPDATE + * publication as the DATA stamp. No other live lock may lose this carrier. + * Selection is read-only; the final caller retains the full prior slot in + * undo and prepares a new DATA receipt before changing either page field. + */ +bool +cluster_itl_alloc_update_slot(Buffer buf, TransactionId top_xid, OffsetNumber old_offset, + uint8 *out_slot_idx) +{ + Page page; + PageHeader header; + const ClusterItlSlotData *slots; + uint8 candidate = CLUSTER_ITL_SLOT_UNALLOCATED; + OffsetNumber maxoff; + bool found_target = false; + + if (out_slot_idx == NULL) + return false; + *out_slot_idx = CLUSTER_ITL_SLOT_UNALLOCATED; + if (!BufferIsValid(buf) || !TransactionIdIsNormal(top_xid)) + return false; + page = BufferGetPage(buf); + header = (PageHeader)page; + if (!PageHasItl(page) || header->pd_lower < SizeOfPageHeaderData + || header->pd_lower > header->pd_upper || header->pd_upper > header->pd_special + || header->pd_special > BLCKSZ - CLUSTER_ITL_ARRAY_SIZE + || (header->pd_lower - SizeOfPageHeaderData) % sizeof(ItemIdData) != 0) + return false; + if (cluster_itl_alloc_or_reuse_slot(buf, top_xid, out_slot_idx)) + return true; + maxoff = PageGetMaxOffsetNumber(page); + if (old_offset < FirstOffsetNumber || old_offset > maxoff) + return false; + slots = ClusterPageGetItlSlots(page); + for (uint8 i = 0; i < CLUSTER_ITL_INITRANS_DEFAULT; i++) { + if (slots[i].flags == ITL_FLAG_FREE || slots[i].xid != top_xid) + continue; + if (candidate != CLUSTER_ITL_SLOT_UNALLOCATED || slots[i].flags != ITL_FLAG_LOCK_ONLY_ACTIVE + || UBA_is_invalid(slots[i].undo_segment_head) || slots[i].wrap == UINT16_MAX) + return false; + candidate = i; + } + if (candidate == CLUSTER_ITL_SLOT_UNALLOCATED) + return false; + for (OffsetNumber off = FirstOffsetNumber; off <= maxoff; off++) { + ItemId lp = PageGetItemId(page, off); + HeapTupleHeader tuple; + + if (!ItemIdIsNormal(lp)) { + if (off == old_offset) + return false; + continue; + } + if (ItemIdGetLength(lp) < SizeofHeapTupleHeader || ItemIdGetOffset(lp) < header->pd_upper + || (uint32)ItemIdGetOffset(lp) + ItemIdGetLength(lp) > header->pd_special) + return false; + tuple = (HeapTupleHeader)PageGetItem(page, lp); + if ((tuple->t_infomask & HEAP_XMAX_INVALID) != 0) + continue; + /* A descriptor may contain this xid even when raw xmax differs. */ + if ((tuple->t_infomask & HEAP_XMAX_IS_MULTI) != 0) + return false; + if (!HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask) + || HeapTupleHeaderGetRawXmax(tuple) != top_xid) + continue; + if (off != old_offset) + return false; + found_target = true; + } + if (!found_target) + return false; + *out_slot_idx = candidate; + return true; +} + /* * cluster_itl_find_multixact_origin_by_xmax (spec-3.6 v0.3 D7b NEW) * @@ -1450,6 +1532,16 @@ cluster_itl_alloc_or_reuse_lock_slot(Buffer buf pg_attribute_unused(), return false; } +bool +cluster_itl_alloc_update_slot(Buffer buf pg_attribute_unused(), + TransactionId top_xid pg_attribute_unused(), + OffsetNumber old_offset pg_attribute_unused(), uint8 *out_slot_idx) +{ + if (out_slot_idx != NULL) + *out_slot_idx = CLUSTER_ITL_SLOT_UNALLOCATED; + return false; +} + bool cluster_itl_find_multixact_origin_by_xmax(Page page pg_attribute_unused(), MultiXactId multixact_id pg_attribute_unused(), diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index cfadef3947..235b5270c5 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -1186,10 +1186,14 @@ LockAcquireExtended(const LOCKTAG *locktag, LOCKMODE lockmode, bool sessionLock, ereport(ERROR, (errcode(ERRCODE_LOCK_NOT_AVAILABLE), errmsg("cluster lock acquire timeout"), errdetail("source=%s master=%d elapsed_ms=%ld attempts=%d " - "conflict_holders=%d effective_timeout_ms=%d.", + "conflict_holders=%d effective_timeout_ms=%d " + "tag=%u/%u/%u/%u/%u method=%u mode=%d.", cluster_ges_timeout_src_text(ges_td->source), ges_td->master_node, ges_td->elapsed_ms, ges_td->attempts, ges_td->conflict_holders, - ges_td->timeout_ms), + ges_td->timeout_ms, locktag->locktag_type, + locktag->locktag_field1, locktag->locktag_field2, + locktag->locktag_field3, locktag->locktag_field4, + locktag->locktag_lockmethodid, lockmode), errhint("Consider increasing cluster.ges_request_timeout_ms."))); pg_unreachable(); } diff --git a/src/include/cluster/cluster_grd.h b/src/include/cluster/cluster_grd.h index 1e46397d07..257005ba72 100644 --- a/src/include/cluster/cluster_grd.h +++ b/src/include/cluster/cluster_grd.h @@ -1442,6 +1442,15 @@ typedef struct ClusterGrdGrantIdentity { LOCKMODE mode; /* granted mode */ } ClusterGrdGrantIdentity; +/* Exact cancellation copies the removed request's original reply/dedup + * identity under the same lock. NOT_FOUND clears output and changes no holder. */ +extern ClusterGrdEntryResult +cluster_grd_cancel_waiter_exact(const ClusterResId *resid, const ClusterGrdHolderId *holder, + uint64 wait_seq, ClusterGrdGrantIdentity *cancelled_out); +extern ClusterGrdEntryResult +cluster_grd_cancel_convert_exact(const ClusterResId *resid, const ClusterGrdHolderId *holder, + uint64 wait_seq, ClusterGrdGrantIdentity *cancelled_out); + /* * D4 — request a convert against an existing holder (caller holds * entry->lock; raw mutator like grant_holder). out_drain_hint is set diff --git a/src/include/cluster/cluster_itl.h b/src/include/cluster/cluster_itl.h index b6e19f2ec3..190c5d0b9c 100644 --- a/src/include/cluster/cluster_itl.h +++ b/src/include/cluster/cluster_itl.h @@ -326,6 +326,12 @@ extern uint8 cluster_itl_stamp_multixact_marker(Buffer buf, MultiXactId multixac * cluster_itl_touch_register() for xact-end finalization. */ extern bool cluster_itl_alloc_or_reuse_slot(Buffer buf, TransactionId top_xid, uint8 *out_slot_idx); + +/* UPDATE old-row only, under content-X. A selected own LOCK_ONLY carrier + * requires full predecessor history and a fresh DATA receipt before the + * atomic slot/tuple publication; other row locks never lose their carrier. */ +extern bool cluster_itl_alloc_update_slot(Buffer buf, TransactionId top_xid, + OffsetNumber old_offset, uint8 *out_slot_idx); extern bool cluster_itl_has_allocatable_slot(Buffer buf, TransactionId top_xid, bool lock_only); /* diff --git a/src/test/cluster_unit/Makefile b/src/test/cluster_unit/Makefile index 18f5c3e18c..923bef1cc8 100644 --- a/src/test/cluster_unit/Makefile +++ b/src/test/cluster_unit/Makefile @@ -1877,7 +1877,7 @@ test_cluster_write_fence: test_cluster_write_fence.c unit_test.h $(top_builddir)/src/common/libpgcommon_srv.a \ $(CLUSTER_UNIT_PORT_LIBS) -o $@ -$(filter-out test_cluster_lms_native_probe test_cluster_bufmgr_stop test_cluster_r4_itl_capacity test_cluster_ctrc_itl_reuse,$(SIMPLE_TESTS)): %: %.c unit_test.h $(CLUSTER_VERSION_O) +$(filter-out test_cluster_cr_dependency test_cluster_lms_native_probe test_cluster_bufmgr_stop test_cluster_r4_itl_capacity test_cluster_ctrc_itl_reuse,$(SIMPLE_TESTS)): %: %.c unit_test.h $(CLUSTER_VERSION_O) $(CC) $(CFLAGS) $(CPPFLAGS) $< $(CLUSTER_VERSION_O) -o $@ # The wire behavior is inline; an updated decoder must rebuild this binary. diff --git a/src/test/cluster_unit/ctrc_source_census.tsv b/src/test/cluster_unit/ctrc_source_census.tsv index 03bad65f5d..b442ce0072 100644 --- a/src/test/cluster_unit/ctrc_source_census.tsv +++ b/src/test/cluster_unit/ctrc_source_census.tsv @@ -86,6 +86,7 @@ cluster_heap_itl_alloc_with_terminal_census src/backend/access/heap/heapam.c REG cluster_heap_itl_plan_prepared_undo_target src/backend/access/heap/heapam.c REGISTERED_REFERENCE CTRC_REF_HEAP_ITL_UBA CTRC_TARGET_PAGE_PENDING_ITL_SLOT|CTRC_TARGET_EXACT_ITL_SLOT cluster_heap_itl_plan_prepared_undo_target cluster_ctrc_receipt_discharge_itl_shared HEAP_ITL_ALLOC_REUSE 1 MXA-T23 cluster_itl_alloc_or_reuse_lock_slot src/backend/cluster/cluster_itl.c REGISTERED_REFERENCE CTRC_REF_HEAP_ITL_UBA CTRC_TARGET_EXACT_ITL_SLOT cluster_heap_itl_plan_prepared_undo_target cluster_ctrc_receipt_discharge_itl_shared HEAP_ITL_ALLOC_REUSE 2 MXA-T23 cluster_itl_alloc_or_reuse_slot src/backend/cluster/cluster_itl.c REGISTERED_REFERENCE CTRC_REF_HEAP_ITL_UBA CTRC_TARGET_EXACT_ITL_SLOT cluster_heap_itl_plan_prepared_undo_target cluster_ctrc_receipt_discharge_itl_shared HEAP_ITL_ALLOC_REUSE 2 MXA-T23 +cluster_itl_alloc_update_slot src/backend/cluster/cluster_itl.c REGISTERED_REFERENCE CTRC_REF_HEAP_ITL_UBA CTRC_TARGET_EXACT_ITL_SLOT cluster_heap_itl_plan_prepared_undo_target cluster_ctrc_receipt_discharge_itl_shared HEAP_ITL_ALLOC_REUSE 1 MXA-T23 heap_delete src/backend/access/heap/heapam.c REGISTERED_REFERENCE CTRC_REF_HEAP_ITL_UBA CTRC_TARGET_EXACT_ITL_SLOT cluster_heap_no_retry_boundary_apply cluster_ctrc_receipt_discharge_itl_shared HEAP_ITL_PUBLISH 1 MXA-T23 heap_insert src/backend/access/heap/heapam.c REGISTERED_REFERENCE CTRC_REF_HEAP_ITL_UBA CTRC_TARGET_EXACT_ITL_SLOT cluster_heap_no_retry_boundary_apply cluster_ctrc_receipt_discharge_itl_shared HEAP_ITL_PUBLISH 1 MXA-T23 heap_lock_tuple_internal src/backend/access/heap/heapam.c REGISTERED_REFERENCE CTRC_REF_HEAP_ITL_UBA CTRC_TARGET_EXACT_ITL_SLOT cluster_heap_no_retry_boundary_apply cluster_ctrc_receipt_discharge_itl_shared HEAP_ITL_PUBLISH 1 MXA-T23 diff --git a/src/test/cluster_unit/data/r11-source-removal-census-v1.json b/src/test/cluster_unit/data/r11-source-removal-census-v1.json index e05a356104..13119ef35e 100644 --- a/src/test/cluster_unit/data/r11-source-removal-census-v1.json +++ b/src/test/cluster_unit/data/r11-source-removal-census-v1.json @@ -16,7 +16,7 @@ "current_product_snapshot": { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "696411d2f2a36a4d32c9a08740ab5eeb3221f61e6f57ccee2109d225badb0554" + "sha256": "17f268db28256b655fef6bac7e92f5c7f15e1d5f17f291dc28555252f727063d" }, "gates": { "L1": { diff --git a/src/test/cluster_unit/generate_ctrc_source_census.py b/src/test/cluster_unit/generate_ctrc_source_census.py index 5ee88c9473..96c98fd9ac 100644 --- a/src/test/cluster_unit/generate_ctrc_source_census.py +++ b/src/test/cluster_unit/generate_ctrc_source_census.py @@ -786,7 +786,7 @@ def _classify_owners( _classify_owners( "HEAP_ITL_ALLOC_REUSE", "src/backend/cluster/cluster_itl.c", - ("cluster_itl_alloc_or_reuse_lock_slot", "cluster_itl_alloc_or_reuse_slot"), + ("cluster_itl_alloc_or_reuse_lock_slot", "cluster_itl_alloc_or_reuse_slot", "cluster_itl_alloc_update_slot"), "REGISTERED_REFERENCE", "CTRC_REF_HEAP_ITL_UBA", "CTRC_TARGET_EXACT_ITL_SLOT", diff --git a/src/test/cluster_unit/test_cluster_ges.c b/src/test/cluster_unit/test_cluster_ges.c index e029222e71..06ba999cac 100644 --- a/src/test/cluster_unit/test_cluster_ges.c +++ b/src/test/cluster_unit/test_cluster_ges.c @@ -506,6 +506,9 @@ static uint32 stub_cancel_wait_last_dest = 0; static GesCancelWaitPayload stub_cancel_wait_last; static uint64 stub_dedup_lookup_count = 0; static uint64 stub_dedup_record_count = 0; +static ClusterGesDedupKey stub_dedup_record_key; +static GesReplyPayload stub_dedup_record_reply; +static bool stub_cancel_match; static uint64 stub_dedup_remove_completed_count = 0; static ClusterGesDedupKey stub_dedup_remove_completed_keys[8]; @@ -791,11 +794,12 @@ cluster_ges_dedup_lookup_or_register(const ClusterGesDedupKey *key pg_attribute_ } void -cluster_ges_dedup_record_reply(const ClusterGesDedupKey *key pg_attribute_unused(), - const uint8 *reply pg_attribute_unused(), - uint16 reply_len pg_attribute_unused()) +cluster_ges_dedup_record_reply(const ClusterGesDedupKey *key, const uint8 *reply, uint16 reply_len) { stub_dedup_record_count++; + stub_dedup_record_key = *key; + if (reply_len == sizeof(stub_dedup_record_reply)) + memcpy(&stub_dedup_record_reply, reply, reply_len); } bool @@ -1068,7 +1072,7 @@ cluster_grd_cancel_waiter_by_id_seq(const struct ClusterResId *r pg_attribute_un const struct ClusterGrdHolderId *h pg_attribute_unused(), uint64 ws pg_attribute_unused()) { - return CLUSTER_GRD_ENTRY_NOT_FOUND; + return stub_cancel_match ? CLUSTER_GRD_ENTRY_OK : CLUSTER_GRD_ENTRY_NOT_FOUND; } ClusterGrdEntryResult @@ -1076,7 +1080,32 @@ cluster_grd_cancel_convert_by_id(const struct ClusterResId *r pg_attribute_unuse const struct ClusterGrdHolderId *h pg_attribute_unused(), uint64 ws pg_attribute_unused()) { - return CLUSTER_GRD_ENTRY_NOT_FOUND; + return stub_cancel_match ? CLUSTER_GRD_ENTRY_OK : CLUSTER_GRD_ENTRY_NOT_FOUND; +} + +ClusterGrdEntryResult +cluster_grd_cancel_waiter_exact(const ClusterResId *r, const ClusterGrdHolderId *h, uint64 ws, + ClusterGrdGrantIdentity *out) +{ + ClusterGrdEntryResult result = cluster_grd_cancel_waiter_by_id_seq(r, h, ws); + memset(out, 0, sizeof(*out)); + if (result == CLUSTER_GRD_ENTRY_OK) { + out->holder = *h; + out->source_node_id = h->node_id; + out->request_opcode = GES_REQ_OPCODE_REQUEST; + out->shard_master_generation = 71; + } + return result; +} + +ClusterGrdEntryResult +cluster_grd_cancel_convert_exact(const ClusterResId *r, const ClusterGrdHolderId *h, uint64 ws, + ClusterGrdGrantIdentity *out) +{ + ClusterGrdEntryResult result = cluster_grd_cancel_waiter_exact(r, h, ws, out); + if (result == CLUSTER_GRD_ENTRY_OK) + out->request_opcode = GES_REQ_OPCODE_CONVERT; + return result; } void @@ -1126,6 +1155,7 @@ static bool stub_clock_advances = false; static TimestampTz stub_now = 0; static bool stub_cv_timeout_expires = true; static TimestampTz stub_cv_now_after_sleep = 0; +static int stub_cv_waits, stub_cv_grant_on_wait; TimestampTz GetCurrentTimestamp(void) @@ -1161,6 +1191,10 @@ ConditionVariableTimedSleep(ConditionVariable *cv pg_attribute_unused(), { if (stub_cv_now_after_sleep > 0) stub_now = stub_cv_now_after_sleep; + if (stub_cv_grant_on_wait > 0 && ++stub_cv_waits >= stub_cv_grant_on_wait) { + stub_reply_wait_entry.ready = true; + stub_reply_wait_entry.reject_reason = GES_REJECT_REASON_NONE; + } return stub_cv_timeout_expires; } @@ -1920,6 +1954,70 @@ UT_TEST(test_ges_request_cv_timeout_retransmits) stub_backend_request_ready_after = 0; } +UT_TEST(test_retry_allowance_does_not_preempt_original_wait_deadline) +{ + ClusterResId resid = { 0 }; + ClusterGrdHolderId holder = { 0 }; + uint32 result; + + holder.node_id = 0; + holder.procno = 21; + holder.request_id = 1001; + stub_remote_master = 7; + stub_reply_wait_insert_enabled = true; + stub_now = 0; + stub_clock_advances = false; + stub_cv_timeout_expires = true; + stub_cv_waits = 0; + stub_cv_grant_on_wait = cluster_ges_retransmit_max_attempts + 3; + stub_backend_request_enqueue_count = 0; + stub_backend_request_ready_after = 0; + result = cluster_ges_send_request_and_wait(&resid, AccessExclusiveLock, &holder, + holder.request_id, 60000, 0); + UT_ASSERT_EQ(result, GES_REJECT_REASON_NONE); + UT_ASSERT_EQ(stub_backend_request_enqueue_count, 1 + cluster_ges_retransmit_max_attempts); + UT_ASSERT_EQ(stub_cv_waits, stub_cv_grant_on_wait); + stub_cv_grant_on_wait = 0; + stub_remote_master = -1; + stub_reply_wait_insert_enabled = false; +} + +UT_TEST(test_exact_cancel_completes_original_dedup_identity) +{ + ClusterICEnvelope env = { 0 }; + GesCancelWaitPayload cancel = { 0 }; + uint64 before; + + cluster_node_id = 0; + env.source_node_id = 2; /* coordinator is not the original requester */ + env.payload_length = sizeof(cancel); + cancel.opcode = GES_REQ_OPCODE_CANCEL_WAIT; + cancel.waiter_node_id = 6; + cancel.waiter_procno = 27; + cancel.waiter_request_id = 1005; + cancel.waiter_cluster_epoch = 19; + cancel.wait_seq = 31; + for (int leg = 0; leg < 2; leg++) { + cancel.kind = leg == 0 ? GES_CANCEL_WAIT_KIND_REQUEST : GES_CANCEL_WAIT_KIND_CONVERT; + before = stub_dedup_record_count; + stub_cancel_match = true; + cluster_ges_request_handler(&env, &cancel); + UT_ASSERT_EQ(stub_dedup_record_count, before + 1); + UT_ASSERT_EQ(stub_dedup_record_key.origin_node_id, 6); + UT_ASSERT_EQ(stub_dedup_record_key.request_id, 1005); + UT_ASSERT_EQ(stub_dedup_record_key.holder_procno, 27); + UT_ASSERT_EQ(stub_dedup_record_key.cluster_epoch, 19); + UT_ASSERT_EQ(stub_dedup_record_key.shard_master_generation, 71); + UT_ASSERT_EQ(stub_dedup_record_key.opcode, + leg == 0 ? GES_REQ_OPCODE_REQUEST : GES_REQ_OPCODE_CONVERT); + UT_ASSERT_EQ(stub_dedup_record_reply.opcode, GES_REPLY_OPCODE_REJECT); + UT_ASSERT_EQ(stub_dedup_record_reply.reject_reason, GES_REJECT_REASON_TIMEOUT); + stub_cancel_match = false; /* absent / stale / grant-won: do not rewrite */ + cluster_ges_request_handler(&env, &cancel); + UT_ASSERT_EQ(stub_dedup_record_count, before + 1); + } +} + UT_TEST(test_ges_release_cv_timeout_retransmits) { ClusterResId resid; @@ -2039,7 +2137,7 @@ UT_TEST(test_ges_probe_validates_identity_then_obeys_final_stop_seal) int main(int argc pg_attribute_unused(), char *argv[] pg_attribute_unused()) { - UT_PLAN(28); + UT_PLAN(30); UT_RUN(test_block0_protected_failure_detail_is_not_elapsed_timeout); UT_RUN(test_ges_request_handler_linkable); @@ -2069,6 +2167,8 @@ main(int argc pg_attribute_unused(), char *argv[] pg_attribute_unused()) UT_RUN(test_ges_release_cv_timeout_retransmits); UT_RUN(test_ges_local_release_requires_exact_holder_and_stable_master); UT_RUN(test_ges_probe_validates_identity_then_obeys_final_stop_seal); + UT_RUN(test_retry_allowance_does_not_preempt_original_wait_deadline); + UT_RUN(test_exact_cancel_completes_original_dedup_identity); UT_DONE(); return ut_failed_count == 0 ? 0 : 1; diff --git a/src/test/cluster_unit/test_cluster_grd.c b/src/test/cluster_unit/test_cluster_grd.c index f2680f19a1..b6618e6acd 100644 --- a/src/test/cluster_unit/test_cluster_grd.c +++ b/src/test/cluster_unit/test_cluster_grd.c @@ -4411,6 +4411,7 @@ UT_TEST(test_5_8_d1e_u4a_request_waiter_carries_wait_seq) ClusterGrdHolderId h; ClusterGrdConflictHolder conflicts[PGRAC_GRD_MAX_HOLDERS_PUBLIC]; int nc = -1; + ClusterGrdGrantIdentity cancelled; cluster_node_id = 0; convert_reset(); @@ -4424,12 +4425,28 @@ UT_TEST(test_5_8_d1e_u4a_request_waiter_carries_wait_seq) (int)CLUSTER_GRD_GRANT_NOW); h = bast_holder(2, 200, 2); UT_ASSERT_EQ((int)cluster_grd_entry_enqueue_or_grant_meta( - &resid, &h, 2, 2, (ClusterGrdWaiterMeta){ (TransactionId)0, 777 }, 0, + &resid, &h, 2, 2, (ClusterGrdWaiterMeta){ (TransactionId)0, 777 }, 71, UT_GES_OPCODE_REQUEST, ExclusiveLock, conflicts, &nc), (int)CLUSTER_GRD_ENQUEUED_WAITER); UT_ASSERT_EQ(ut_wfg_count_waiter(2, 200, 0, 2), 1); UT_ASSERT(ut_wfg_waiter_wait_seq(2, 200, 0, 2) == 777); + memset(&cancelled, 0x5a, sizeof(cancelled)); + UT_ASSERT_EQ(cluster_grd_cancel_waiter_exact(&resid, &h, 778, &cancelled), + CLUSTER_GRD_ENTRY_NOT_FOUND); + UT_ASSERT_EQ(cancelled.request_opcode, 0); + UT_ASSERT_EQ(ut_wfg_count_waiter(2, 200, 0, 2), 1); + UT_ASSERT_EQ(cluster_grd_cancel_waiter_exact(&resid, &h, 777, &cancelled), + CLUSTER_GRD_ENTRY_OK); + UT_ASSERT_EQ(cancelled.source_node_id, 2); + UT_ASSERT_EQ(cancelled.holder.procno, 200); + UT_ASSERT_EQ(cancelled.holder.request_id, 2); + UT_ASSERT_EQ(cancelled.shard_master_generation, 71); + UT_ASSERT_EQ(cancelled.request_opcode, UT_GES_OPCODE_REQUEST); + UT_ASSERT_EQ(ut_wfg_count_waiter(2, 200, 0, 2), 0); + UT_ASSERT_EQ(cluster_grd_cancel_waiter_exact(&resid, &h, 777, &cancelled), + CLUSTER_GRD_ENTRY_NOT_FOUND); + UT_ASSERT_EQ(cancelled.request_opcode, 0); cluster_node_id = saved; convert_teardown(); @@ -4443,6 +4460,8 @@ UT_TEST(test_5_8_d1e_u4b_convert_waiter_carries_wait_seq) ClusterGrdHolderId h; ClusterGrdConflictHolder conflicts[PGRAC_GRD_MAX_HOLDERS_PUBLIC]; int nc = -1; + ClusterGrdGrantIdentity cancelled; + LOCKMODE held_mode; cluster_node_id = 0; convert_reset(); @@ -4462,12 +4481,27 @@ UT_TEST(test_5_8_d1e_u4b_convert_waiter_carries_wait_seq) nc = -1; UT_ASSERT_EQ((int)cluster_grd_convert_or_enqueue_meta( - &resid, 1, 100, 0, ShareLock, ExclusiveLock, 10, 1, 0, + &resid, 1, 100, 0, ShareLock, ExclusiveLock, 10, 1, 73, (ClusterGrdWaiterMeta){ (TransactionId)0, 888 }, conflicts, &nc), (int)CLUSTER_GRD_CONVERT_ENQUEUED); UT_ASSERT_EQ(ut_wfg_count_waiter(1, 100, 0, 10), 1); UT_ASSERT(ut_wfg_waiter_wait_seq(1, 100, 0, 10) == 888); + h = bast_holder(1, 100, 10); + UT_ASSERT_EQ(cluster_grd_cancel_convert_exact(&resid, &h, 889, &cancelled), + CLUSTER_GRD_ENTRY_NOT_FOUND); + UT_ASSERT_EQ(cancelled.request_opcode, 0); + UT_ASSERT_EQ(ut_wfg_count_waiter(1, 100, 0, 10), 1); + UT_ASSERT_EQ(cluster_grd_cancel_convert_exact(&resid, &h, 888, &cancelled), + CLUSTER_GRD_ENTRY_OK); + UT_ASSERT_EQ(cancelled.holder.request_id, 10); + UT_ASSERT_EQ(cancelled.source_node_id, 1); + UT_ASSERT_EQ(cancelled.request_opcode, GES_REQ_OPCODE_CONVERT); + UT_ASSERT_EQ(cancelled.shard_master_generation, 73); + UT_ASSERT_EQ(ut_wfg_count_waiter(1, 100, 0, 10), 0); + h.request_id = 1; + UT_ASSERT(cluster_grd_holder_mode_by_id(&resid, &h, &held_mode)); + UT_ASSERT_EQ(held_mode, ShareLock); cluster_node_id = saved; convert_teardown(); diff --git a/src/test/cluster_unit/test_cluster_heap_epq_wait.c b/src/test/cluster_unit/test_cluster_heap_epq_wait.c index c3db4cfb19..aa3976dc47 100644 --- a/src/test/cluster_unit/test_cluster_heap_epq_wait.c +++ b/src/test/cluster_unit/test_cluster_heap_epq_wait.c @@ -151,7 +151,7 @@ ExceptionalCondition(const char *c, const char *f, int l) bool errstart(int level, const char *domain) { - if (capture_index_log && level == LOG) { + if (capture_index_log && level == DEBUG1) { finishing_index_log = true; return true; } diff --git a/src/test/cluster_unit/test_cluster_heap_receipt.c b/src/test/cluster_unit/test_cluster_heap_receipt.c index adeea68d66..06cb137f37 100644 --- a/src/test/cluster_unit/test_cluster_heap_receipt.c +++ b/src/test/cluster_unit/test_cluster_heap_receipt.c @@ -221,6 +221,45 @@ UT_TEST(final_plan_drift_is_rejected_even_when_intent_is_compatible) cluster_undo_record_cancel_prepared(&receipt); } +UT_TEST(own_lock_handoff_gets_fresh_data_receipt_and_full_predecessor) +{ + PGAlignedBlock image, before; + ClusterUndoRecordPrepareReceipt receipt; + ClusterCtrcTargetV1 pending, final_target; + ClusterItlSlotData prior; + ClusterItlSlotData *slot; + uint8 digest[32]; + + heap_receipt_fixture(image.data, &receipt, &pending); + slot = &ClusterPageGetItlSlots(image.data)[0]; + slot->xid = 700; + slot->flags = ITL_FLAG_LOCK_ONLY_ACTIVE; + slot->wrap = 12; + slot->write_scn = 90; + slot->undo_segment_head = uba_encode(1, 7, 0, 0); + prior = *slot; + memcpy(before.data, image.data, BLCKSZ); + UT_ASSERT(heap_receipt_test_plan_capture(&receipt)); + UT_ASSERT(heap_receipt_test_plan_recheck(&receipt)); + UT_ASSERT(heap_receipt_test_final(&receipt, &final_target)); + UT_ASSERT_EQ(final_target.itl_class, 1); + UT_ASSERT_EQ(final_target.itl_slot_wrap, 13); + UT_ASSERT_EQ(receipt.ctrc_reuse_mask, 0); + UT_ASSERT_EQ(receipt_prepare_calls, 1); + UT_ASSERT_EQ(receipt_apply_calls, 0); + UT_ASSERT_EQ(memcmp(&receipt.itl_history[0].prior, &prior, sizeof(prior)), 0); + UT_ASSERT_EQ(receipt.itl_history[0].after_kind, ITL_FLAG_ACTIVE); + UT_ASSERT(cluster_ctrc_sha256_exact(&prior, sizeof(prior), digest)); + UT_ASSERT_EQ(memcmp(digest, final_target.planned_predecessor_sha256, sizeof(digest)), 0); + UT_ASSERT(memcmp(final_target.planned_predecessor_sha256, final_target.planned_successor_sha256, + sizeof(digest)) + != 0); + UT_ASSERT_EQ(memcmp(image.data, before.data, BLCKSZ), 0); + slot->wrap++; + UT_ASSERT(!heap_receipt_test_plan_recheck(&receipt)); + cluster_undo_record_cancel_prepared(&receipt); +} + UT_TEST(reuse_applied_and_identity_drift_are_not_page_version_refreshes) { PGAlignedBlock image; @@ -404,7 +443,8 @@ UT_TEST(insert_undo_target_matches_real_empty_or_reused_line_pointer) int main(void) { - UT_PLAN(13); + UT_PLAN(14); + UT_RUN(own_lock_handoff_gets_fresh_data_receipt_and_full_predecessor); UT_RUN(logical_tid_cannot_authorize_another_tuple_address); UT_RUN(insert_undo_target_matches_real_empty_or_reused_line_pointer); UT_RUN(cleanout_preserves_exact_unpublished_resource_after_prepare_deadline); diff --git a/src/test/cluster_unit/test_cluster_itl_reader_real_triple.c b/src/test/cluster_unit/test_cluster_itl_reader_real_triple.c index b394bc746a..a24351f4b4 100644 --- a/src/test/cluster_unit/test_cluster_itl_reader_real_triple.c +++ b/src/test/cluster_unit/test_cluster_itl_reader_real_triple.c @@ -1382,6 +1382,127 @@ append_plain_lock_tuple(Page page, TransactionId xid) return tuple; } +/* Counterfactual RED uses the unchanged production DATA selector, not a + * simulated allocator. Normal builds exercise the exact UPDATE selector. */ +#ifdef TEST_ITL_BASELINE_SELECTOR +#define cluster_itl_alloc_update_slot(buf, xid, off, out) \ + cluster_itl_alloc_or_reuse_slot(buf, xid, out) +#endif + +static Page +build_eight_locked_rows(void) +{ + Page page = build_itl_page(); + + for (int i = 0; i < CLUSTER_ITL_INITRANS_DEFAULT; i++) { + ClusterItlSlotData *slot = slot_at(page, i); + + (void)append_plain_lock_tuple(page, 700 + i); + slot->xid = 700 + i; + slot->flags = ITL_FLAG_LOCK_ONLY_ACTIVE; + slot->wrap = 12; + slot->undo_segment_head = uba_encode(1, 2, i, 1); + } + return page; +} + +UT_TEST(update_own_predecessor_lock_does_not_need_a_ninth_slot) +{ + for (int i = 0; i < CLUSTER_ITL_INITRANS_DEFAULT; i++) { + Page page = build_eight_locked_rows(); + Buffer buf = marker_buffer_for(page); + PGAlignedBlock before; + uint8 selected = CLUSTER_ITL_SLOT_UNALLOCATED; + bool found; + + memcpy(before.data, page, BLCKSZ); + found = cluster_itl_alloc_update_slot(buf, 700 + i, i + 1, &selected); + UT_ASSERT(found); + UT_ASSERT_EQ(memcmp(before.data, page, BLCKSZ), 0); + if (!found) + continue; + UT_ASSERT_EQ(selected, i); + /* Selection alone never releases a row lock. Publication retains the + * predecessor via the caller's existing history/receipt boundary. */ + cluster_itl_stamp_active_with_history(buf, selected, 700 + i, 900, uba_encode(1, 3, i, 1)); + UT_ASSERT_EQ(slot_at(page, selected)->flags, ITL_FLAG_ACTIVE); + UT_ASSERT_EQ(slot_at(page, selected)->wrap, 13); + UT_ASSERT_EQ(memcmp(PageGetItem(page, PageGetItemId(page, i + 1)), + PageGetItem((Page)before.data, PageGetItemId((Page)before.data, i + 1)), + SizeofHeapTupleHeader), + 0); + for (int j = 0; j < CLUSTER_ITL_INITRANS_DEFAULT; j++) + if (j != i) + UT_ASSERT_EQ(memcmp(slot_at(page, j), slot_at((Page)before.data, j), + sizeof(ClusterItlSlotData)), + 0); + } +} + +UT_TEST(update_handoff_refuses_unproved_or_still_referenced_lock_slot) +{ + for (int fault = 0; fault < 10; fault++) { + Page page = build_eight_locked_rows(); + Buffer buf = marker_buffer_for(page); + HeapTupleHeader tuple = (HeapTupleHeader)PageGetItem(page, PageGetItemId(page, 1)); + PGAlignedBlock before; + uint8 selected = CLUSTER_ITL_SLOT_UNALLOCATED; + OffsetNumber off = 1; + + switch (fault) { + case 0: + (void)append_plain_lock_tuple(page, 700); + break; + case 1: + tuple->t_infomask |= HEAP_XMAX_IS_MULTI; + break; + case 2: + *slot_at(page, 1) = *slot_at(page, 0); + break; + case 3: + slot_at(page, 0)->undo_segment_head = (UBA)InvalidUba_init; + break; + case 4: + slot_at(page, 0)->wrap = UINT16_MAX; + break; + case 5: + off = 9; + break; + case 6: + HeapTupleHeaderSetXmax(tuple, 701); + break; + case 7: + ItemIdSetNormal(PageGetItemId(page, 1), BLCKSZ - 1, 24); + break; + case 8: + tuple->t_infomask |= HEAP_XMAX_INVALID; + break; + case 9: + append_plain_lock_tuple(page, 901)->t_infomask |= HEAP_XMAX_IS_MULTI; + break; + } + memcpy(before.data, page, BLCKSZ); + UT_ASSERT(!cluster_itl_alloc_update_slot(buf, 700, off, &selected)); + UT_ASSERT_EQ(selected, CLUSTER_ITL_SLOT_UNALLOCATED); + UT_ASSERT_EQ(memcmp(before.data, page, BLCKSZ), 0); + } +} + +UT_TEST(update_handoff_keeps_ordinary_data_allocation_priority) +{ + for (int leg = 0; leg < 2; leg++) { + Page page = build_eight_locked_rows(); + Buffer buf = marker_buffer_for(page); + uint8 selected = CLUSTER_ITL_SLOT_UNALLOCATED; + + slot_at(page, 5)->flags = leg == 0 ? ITL_FLAG_FREE : ITL_FLAG_ACTIVE; + slot_at(page, 5)->xid = 700; + UT_ASSERT(cluster_itl_alloc_update_slot(buf, 700, 1, &selected)); + UT_ASSERT_EQ(selected, 5); + UT_ASSERT_EQ(slot_at(page, 0)->flags, ITL_FLAG_LOCK_ONLY_ACTIVE); + } +} + UT_TEST(completed_lock_reuse_normalizes_only_matching_plain_locks) { int leg; @@ -1608,39 +1729,43 @@ UT_TEST(test_retained_history_stamp_does_not_erase_or_advance_loss_watermark) UT_TEST(test_retained_history_v4_redo_matches_primary_without_fpi) { - Page page = build_itl_page(); - ClusterItlSlotData *slot = slot_at(page, 0); - PGAlignedBlock before; - PGAlignedBlock primary; - xl_heap_itl_delta_block *header = (xl_heap_itl_delta_block *)redo_delta_buf; - xl_heap_itl_delta_v3 *delta = (xl_heap_itl_delta_v3 *)(redo_delta_buf + 8); + for (int variant = 0; variant < 2; variant++) { + Page page = build_itl_page(); + ClusterItlSlotData *slot = slot_at(page, 0); + PGAlignedBlock before; + PGAlignedBlock primary; + xl_heap_itl_delta_block *header = (xl_heap_itl_delta_block *)redo_delta_buf; + xl_heap_itl_delta_v3 *delta = (xl_heap_itl_delta_v3 *)(redo_delta_buf + 8); - slot->xid = 100; - slot->flags = ITL_FLAG_COMMITTED; - slot->wrap = 7; - slot->write_scn = 500; - slot->commit_scn = 600; - slot->undo_segment_head = uba_encode(1, 7, 0, 0); - ClusterPageGetItlHeader(page)->itl_recycle_watermark_scn = 300; - memcpy(before.data, page, BLCKSZ); - cluster_itl_stamp_active_with_history(marker_buffer_for(page), 0, 101, 700, - uba_encode(1, 7, 1, 0)); - memcpy(primary.data, page, BLCKSZ); - memcpy(page, before.data, BLCKSZ); - memset(redo_delta_buf, 0, sizeof(redo_delta_buf)); - header->ndeltas = 1; - header->format_version = CLUSTER_ITL_DELTA_FORMAT_V4; - delta->slot_idx = 0; - delta->flags_after = ITL_FLAG_ACTIVE; - delta->xid = 101; - delta->write_scn = 700; - delta->undo_segment_head = uba_encode(1, 7, 1, 0); - if (sigsetjmp(ereport_recover_jmp, 1) == 0) { - UT_ASSERT_EQ(cluster_itl_redo_apply_block_local_delta(page, NULL, redo_delta_buf), 40); - UT_ASSERT_EQ(cluster_itl_wal_block_consumed_bytes(redo_delta_buf), 40); - UT_ASSERT_EQ(memcmp(page, primary.data, BLCKSZ), 0); - } else - UT_ASSERT(false); + slot->xid = variant == 0 ? 100 : 101; + slot->flags = variant == 0 ? ITL_FLAG_COMMITTED : ITL_FLAG_LOCK_ONLY_ACTIVE; + slot->wrap = 7; + slot->write_scn = 500; + slot->commit_scn = variant == 0 ? 600 : InvalidScn; + slot->undo_segment_head = uba_encode(1, 7, 0, 0); + ClusterPageGetItlHeader(page)->itl_recycle_watermark_scn = 300; + memcpy(before.data, page, BLCKSZ); + cluster_itl_stamp_active_with_history(marker_buffer_for(page), 0, 101, 700, + uba_encode(1, 7, 1, 0)); + memcpy(primary.data, page, BLCKSZ); + memcpy(page, before.data, BLCKSZ); + memset(redo_delta_buf, 0, sizeof(redo_delta_buf)); + header->ndeltas = 1; + header->format_version = CLUSTER_ITL_DELTA_FORMAT_V4; + delta->slot_idx = 0; + delta->flags_after = ITL_FLAG_ACTIVE; + delta->xid = 101; + delta->write_scn = 700; + delta->undo_segment_head = uba_encode(1, 7, 1, 0); + if (sigsetjmp(ereport_recover_jmp, 1) == 0) { + UT_ASSERT_EQ(cluster_itl_redo_apply_block_local_delta(page, NULL, redo_delta_buf), 40); + UT_ASSERT_EQ(cluster_itl_wal_block_consumed_bytes(redo_delta_buf), 40); + UT_ASSERT_EQ(memcmp(page, primary.data, BLCKSZ), 0); + UT_ASSERT_EQ(cluster_itl_redo_apply_block_local_delta(page, NULL, redo_delta_buf), 40); + UT_ASSERT_EQ(memcmp(page, primary.data, BLCKSZ), 0); + } else + UT_ASSERT(false); + } } UT_TEST(test_retained_history_v4_rejects_complete_array_before_mutation) @@ -1745,7 +1870,10 @@ UT_TEST(test_retained_lock_history_v4_redo_matches_primary) int main(void) { - UT_PLAN(69); + UT_PLAN(72); + UT_RUN(update_own_predecessor_lock_does_not_need_a_ninth_slot); + UT_RUN(update_handoff_refuses_unproved_or_still_referenced_lock_slot); + UT_RUN(update_handoff_keeps_ordinary_data_allocation_priority); UT_RUN(test_retained_lock_history_v4_redo_matches_primary); UT_RUN(test_retained_history_v4_rejects_complete_array_before_mutation); UT_RUN(test_retained_history_stamp_does_not_erase_or_advance_loss_watermark); diff --git a/src/test/cluster_unit/test_cluster_r4_cr_walk.c b/src/test/cluster_unit/test_cluster_r4_cr_walk.c index f6a0f711c6..c45708cf07 100644 --- a/src/test/cluster_unit/test_cluster_r4_cr_walk.c +++ b/src/test/cluster_unit/test_cluster_r4_cr_walk.c @@ -3144,24 +3144,33 @@ UT_TEST(test_page_history_more_than_eight_writers_and_interleaved_heads) { unsigned variant; - for (variant = 0; variant < 4; variant++) { + for (variant = 0; variant < 5; variant++) { PGAlignedBlock page; PGAlignedBlock foreign; ClusterR4CrSlotExtension extension = make_builder_extension(607 + variant, 907 + variant); ClusterCrBuildReason reason; unsigned i; + bool same_xid = variant == 1 || variant == 4; - make_many_page_history(page.data, &extension, variant == 1, variant == 2, variant == 3); - if (variant == 1) + /* The last leg crosses LOCK_ONLY -> DATA for the same xid, not just + * successive different transactions. The full predecessor must survive. */ + make_many_page_history(page.data, &extension, same_xid, variant == 2, + variant == 3 || variant == 4); + if (same_xid) extension.route_proof.read_scn = 195; /* five earlier same-xid creations must survive */ UT_ASSERT_EQ(cluster_cr_build_on_holder_step(0, 607 + variant, false, &extension, page.data, foreign.data, &reason), CLUSTER_R4_CR_STEP_FULL); UT_ASSERT_EQ(reason, CLUSTER_CR_BUILD_NONE); - UT_ASSERT_EQ(ut_undo_get_record_calls, variant == 1 ? 7 : TEST_HISTORY_RECORDS); + UT_ASSERT_EQ(ut_undo_get_record_calls, same_xid ? 7 : TEST_HISTORY_RECORDS); for (i = 0; i < TEST_HISTORY_RECORDS; i++) - UT_ASSERT_EQ(ItemIdIsNormal(PageGetItemId((Page)page.data, i + 1)), - variant == 1 && i < 5); + UT_ASSERT_EQ(ItemIdIsNormal(PageGetItemId((Page)page.data, i + 1)), same_xid && i < 5); + if (variant == 4) { + HeapTupleHeader prior + = (HeapTupleHeader)PageGetItem((Page)page.data, PageGetItemId((Page)page.data, 1)); + UT_ASSERT((prior->t_infomask & HEAP_XMAX_INVALID) != 0); + UT_ASSERT_EQ(ClusterPageGetItlSlots((Page)page.data)[0].flags, ITL_FLAG_ACTIVE); + } cluster_cr_build_on_holder_forget(0, 607 + variant); ut_history_sequence = false; } 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 6ffbc4cb57..a543357d02 100644 --- a/src/test/cluster_unit/test_cluster_r4_lock_order.c +++ b/src/test/cluster_unit/test_cluster_r4_lock_order.c @@ -5159,6 +5159,12 @@ UT_TEST(test_75_update_predicts_successor_only_for_receipt_consumers) UT_ASSERT(cluster_heap_test_update_needs_successor_prediction(true, false)); UT_ASSERT(cluster_heap_test_update_needs_successor_prediction(false, true)); UT_ASSERT(cluster_heap_test_update_needs_successor_prediction(true, true)); + UT_ASSERT(cluster_heap_test_update_lock_handoff_allowed(true, HEAP_XMAX_INVALID)); + UT_ASSERT(!cluster_heap_test_update_lock_handoff_allowed(false, HEAP_XMAX_INVALID)); + UT_ASSERT(!cluster_heap_test_update_lock_handoff_allowed(false, HEAP_XMAX_LOCK_ONLY + | HEAP_XMAX_KEYSHR_LOCK)); + UT_ASSERT(!cluster_heap_test_update_lock_handoff_allowed(true, HEAP_XMAX_LOCK_ONLY + | HEAP_XMAX_KEYSHR_LOCK)); } /* A local catalog page has no PCM generation and therefore cannot produce a diff --git a/src/test/cluster_unit/test_cluster_shmem.c b/src/test/cluster_unit/test_cluster_shmem.c index 7c22454632..83d2454de4 100644 --- a/src/test/cluster_unit/test_cluster_shmem.c +++ b/src/test/cluster_unit/test_cluster_shmem.c @@ -98,6 +98,13 @@ RequestAddinShmemSpace(Size size pg_attribute_unused()) #include "utils/guc.h" +void * +guc_malloc(int elevel pg_attribute_unused(), size_t size) +{ + /* The injection-enabled GUC object also references the PG allocator. */ + return malloc(size); +} + void DefineCustomIntVariable(const char *name pg_attribute_unused(), const char *short_desc pg_attribute_unused(), diff --git a/src/tools/check_r11_source_removal_census.py b/src/tools/check_r11_source_removal_census.py index 674ef63b07..fc26d71c02 100644 --- a/src/tools/check_r11_source_removal_census.py +++ b/src/tools/check_r11_source_removal_census.py @@ -24,7 +24,7 @@ CURRENT_PRODUCT_SNAPSHOT = { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "696411d2f2a36a4d32c9a08740ab5eeb3221f61e6f57ccee2109d225badb0554", + "sha256": "17f268db28256b655fef6bac7e92f5c7f15e1d5f17f291dc28555252f727063d", } LAYERS = { From 973474050a818c5d11c45fb7a9c3e0b1b332d2fb Mon Sep 17 00:00:00 2001 From: SqlRush Date: Fri, 18 Sep 2026 21:08:05 +0800 Subject: [PATCH 06/10] fix(cluster): preserve statement visibility and UPDATE child intents --- src/backend/access/heap/heapam.c | 32 +++- src/backend/cluster/cluster_undo_record.c | 90 +++++++++- src/include/cluster/cluster_undo_record_api.h | 14 +- src/test/cluster_unit/Makefile | 9 +- src/test/cluster_unit/ctrc_source_census.tsv | 1 + .../data/r11-source-removal-census-v1.json | 2 +- .../generate_ctrc_source_census.py | 2 +- .../test_cluster_heap_update_temp_lock.c | 51 ++++++ .../cluster_unit/test_cluster_r4_lock_order.c | 57 +++++++ .../test_cluster_terminal_ref_census.c | 43 +++++ .../cluster_unit/test_cluster_undo_record.c | 161 +++++++++++++++++- src/tools/check_r11_source_removal_census.py | 2 +- 12 files changed, 449 insertions(+), 15 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index 9cff88a559..498579a2f1 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -4820,7 +4820,11 @@ heap_hot_r4_data_slot(uint8 flags) * raw xmin is not a creator locator: it is the approved signal to request a * complete holder-built block and restart from the logical HOT root. A * matching creator written after the statement SCN needs the same FULL path; - * the older version may require another instance's undo. Our own creator + * the older version may require another instance's undo. Another local + * transaction also needs FULL: native snapshot membership can still include + * it after the predecessor's canonical commit is visible at read_scn. Mixing + * those two verdicts loses both versions in the commit-publication window. + * Our own creator * keeps the ordinary command-id visibility path; a foreign numeric xid match * is not our transaction. */ @@ -4861,6 +4865,8 @@ heap_hot_r4_updated_xmin_needs_full(Page page, HeapTuple tuple, return heap_hot_r4_data_slot(slot->flags) && cluster_itl_get_tt_ref(page, itl_index, &ref) && ref.tt_slot_id != 0 && TransactionIdIsNormal(ref.local_xid) && (!TransactionIdEquals(ref.local_xid, raw_xmin) + || (ref.origin_node_id == cluster_node_id + && !TransactionIdIsCurrentTransactionId(ref.local_xid)) || (SCN_VALID(slot->write_scn) && !(ref.origin_node_id == cluster_node_id && TransactionIdIsCurrentTransactionId(ref.local_xid)) @@ -13459,6 +13465,30 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, } } LockBuffer(buffer, BUFFER_LOCK_UNLOCK); + /* A different insertion page invalidates its unpublished + * child intent, not an otherwise exact READY undo extent. + * Rebind only a completely captured UPDATE target set, with + * both pages unlocked; the next pass still rechecks and APPLYs + * under current content-X before any reference is published. */ + if (ctrc_target_mismatch && (ctrc_failure_bits & UINT8_C(3)) == 0 + && undo_receipt.tt_slot_segment_id == (uint16)canonical_binding.segment_id + && undo_receipt.tt_slot_offset == canonical_binding.slot_offset) + { + ClusterUndoTargetResetResult reset_result + = cluster_undo_record_reset_update_targets( + &undo_receipt, ctrc_pending_targets, ctrc_required_mask); + + if (reset_result == CLUSTER_UNDO_TARGET_RESET_REFUSED) + ereport(ERROR, + (errcode(ERRCODE_CLUSTER_UNDO_RECORD_INVALID_UBA), + errmsg("UPDATE target intent cancellation could not be proved"), + cluster_heap_undo_receipt_errdetail(true))); + if (reset_result == CLUSTER_UNDO_TARGET_RESET_READY) + { + ctrc_target_mismatch = false; + ctrc_prepare_only = true; + } + } if (ctrc_prepare_only && !ctrc_target_mismatch) { for (ctrc_target_ordinal = 0; diff --git a/src/backend/cluster/cluster_undo_record.c b/src/backend/cluster/cluster_undo_record.c index b274c63e02..3817b9ef48 100644 --- a/src/backend/cluster/cluster_undo_record.c +++ b/src/backend/cluster/cluster_undo_record.c @@ -2338,6 +2338,7 @@ cluster_undo_record_prepare(uint8 record_type, uint16 payload_capacity, uint16 t reservation->receipt.actual_segment_id = ext->segment_id; reservation->receipt.reservation_sequence = reservation->sequence; reservation->receipt.absolute_deadline_us = absolute_deadline_us; + reservation->receipt.ctrc_attempt_generation = 1; reservation->receipt.extent = *ext; reservation->receipt.block0_publication = publication; reservation->receipt.modifier_admission = modifier_admission; @@ -2431,9 +2432,7 @@ cluster_undo_record_ctrc_stage_pending(ClusterUndoRecordPrepareReceipt *receipt, || (receipt->ctrc_prepared_mask & target_bit) != 0 || (receipt->ctrc_applied_mask & target_bit) != 0 || (receipt->ctrc_reuse_mask & target_bit) != 0 - || receipt->ctrc_handles[target_ordinal].valid - || !cluster_undo_record_bytes_zero(receipt->ctrc_reserved8, - sizeof(receipt->ctrc_reserved8))) + || receipt->ctrc_handles[target_ordinal].valid || receipt->ctrc_attempt_generation == 0) return false; if ((receipt->ctrc_pending_mask & target_bit) != 0) return cluster_undo_record_ctrc_pending_recheck(receipt, target_ordinal, pending_target); @@ -2462,8 +2461,7 @@ cluster_undo_record_ctrc_stage_reuse(ClusterUndoRecordPrepareReceipt *receipt, u || (receipt->ctrc_reuse_mask & target_bit) != 0 || receipt->ctrc_handles[target_ordinal].valid || !cluster_undo_record_receipt_extent_matches(receipt) - || !cluster_undo_record_bytes_zero(receipt->ctrc_reserved8, - sizeof(receipt->ctrc_reserved8))) + || receipt->ctrc_attempt_generation == 0) return false; old_handle = receipt->ctrc_handles[target_ordinal]; @@ -2530,8 +2528,7 @@ cluster_undo_record_ctrc_required_prepared(const ClusterUndoRecordPrepareReceipt || (receipt->ctrc_prepared_mask & known_mask) != required_mask || (receipt->ctrc_applied_mask & known_mask) != 0 || (receipt->ctrc_reuse_mask & ~required_mask) != 0 - || !cluster_undo_record_bytes_zero(receipt->ctrc_reserved8, - sizeof(receipt->ctrc_reserved8))) + || receipt->ctrc_attempt_generation == 0) return false; for (target_ordinal = 0; target_ordinal < CLUSTER_UNDO_RECORD_CTRC_TARGETS; target_ordinal++) { uint8 target_bit = UINT8_C(1) << target_ordinal; @@ -2589,7 +2586,7 @@ cluster_undo_record_ctrc_prepare_pending(ClusterUndoRecordPrepareReceipt *receip publication.operation_id = receipt->reservation_sequence + target_ordinal; if (publication.operation_id < receipt->reservation_sequence) return false; - publication.attempt_generation = 1; + publication.attempt_generation = receipt->ctrc_attempt_generation; publication.descriptor_hash = 0; publication.member_ordinal = UINT16_MAX; publication.member_role = 0; @@ -2705,6 +2702,83 @@ cluster_undo_record_retry_evidence(uint64 reservation_sequence, bool *exact_read return true; } +ClusterUndoTargetResetResult +cluster_undo_record_reset_update_targets(ClusterUndoRecordPrepareReceipt *receipt, + const ClusterCtrcTargetV1 *targets, uint8 required_mask) +{ + const ClusterCtrcTargetV1 *source; + bool changed; + + cluster_undo_receipt_reason = "UPDATE_TARGET_REBIND_REFUSED"; + if (receipt == NULL || targets == NULL || (required_mask != 1 && required_mask != 3) + || receipt->record_type != UNDO_RECORD_UPDATE || receipt->ctrc_applied_mask != 0 + || cluster_undo_record_reservation.consume_locked || receipt->ctrc_attempt_generation == 0 + || receipt->ctrc_attempt_generation == UINT32_MAX + || (receipt->ctrc_pending_mask != 1 && receipt->ctrc_pending_mask != 3) + || (receipt->ctrc_prepared_mask & ~receipt->ctrc_pending_mask) != 0 + || (receipt->ctrc_reuse_mask & ~receipt->ctrc_prepared_mask) != 0 + || !cluster_undo_record_receipt_extent_matches(receipt)) + return CLUSTER_UNDO_TARGET_RESET_NOT_APPLICABLE; + source = &receipt->ctrc_pending_targets[0]; + if (source->page_operation_kind != UNDO_RECORD_UPDATE + || !cluster_undo_record_ctrc_pending_recheck(receipt, 0, &targets[0])) + return CLUSTER_UNDO_TARGET_RESET_NOT_APPLICABLE; + changed = required_mask != receipt->ctrc_pending_mask; + for (uint8 i = 0; i < CLUSTER_UNDO_RECORD_CTRC_TARGETS; i++) { + uint8 bit = UINT8_C(1) << i; + const ClusterCtrcTargetV1 *target = &targets[i]; + + if (((receipt->ctrc_prepared_mask & bit) != 0) != receipt->ctrc_handles[i].valid) + return CLUSTER_UNDO_TARGET_RESET_NOT_APPLICABLE; + if ((required_mask & bit) == 0) + continue; + /* A destination may move within this relation, not to a different + * namespace, membership or producer. Self-recheck validates every + * pending-only byte before an old child is cancelled. */ + if (!cluster_ctrc_pending_itl_target_recheck(target, target) + || target->spc_oid != source->spc_oid || target->db_oid != source->db_oid + || target->rel_number != source->rel_number + || target->fork_number != source->fork_number + || target->relation_persistence != source->relation_persistence + || target->needs_wal != source->needs_wal + || target->page_operation_kind != source->page_operation_kind + || target->publication_acquisition_epoch != source->publication_acquisition_epoch) + return CLUSTER_UNDO_TARGET_RESET_NOT_APPLICABLE; + changed |= !cluster_undo_record_ctrc_pending_recheck(receipt, i, target); + } + if (!changed + || !cluster_semantic_activation_modifier_recheck(&receipt->modifier_admission, + cluster_undo_record_writable_admission()) + || !cluster_undo_block0_current_live_owner_publication_recheck( + &receipt->block0_publication)) + return CLUSTER_UNDO_TARGET_RESET_NOT_APPLICABLE; + + /* No heap lock is held. A borrowed APPLIED reference is owned by its + * existing touch/cleaner, never by this unpublished UPDATE intent. + * Keep all private identities until every owned cancellation succeeds; + * partial failure is an error, with original handles available to abort. */ + for (uint8 i = 0; i < CLUSTER_UNDO_RECORD_CTRC_TARGETS; i++) { + uint8 bit = UINT8_C(1) << i; + + if ((receipt->ctrc_prepared_mask & bit) != 0 && (receipt->ctrc_reuse_mask & bit) == 0 + && !cluster_ctrc_receipt_cancel_shared(&receipt->ctrc_handles[i])) + return CLUSTER_UNDO_TARGET_RESET_REFUSED; + } + memset(receipt->ctrc_handles, 0, sizeof(receipt->ctrc_handles)); + memset(receipt->ctrc_pending_targets, 0, sizeof(receipt->ctrc_pending_targets)); + memset(receipt->itl_history, 0, sizeof(receipt->itl_history)); + receipt->ctrc_prepared_mask = receipt->ctrc_reuse_mask = receipt->itl_history_mask = 0; + receipt->ctrc_pending_mask = required_mask; + for (uint8 i = 0; i < CLUSTER_UNDO_RECORD_CTRC_TARGETS; i++) + if ((required_mask & (UINT8_C(1) << i)) != 0) + receipt->ctrc_pending_targets[i] = targets[i]; + receipt->ctrc_attempt_generation++; + if (!cluster_undo_record_receipt_sync(receipt)) + return CLUSTER_UNDO_TARGET_RESET_REFUSED; + cluster_undo_receipt_reason = "NONE"; + return CLUSTER_UNDO_TARGET_RESET_READY; +} + ClusterUndoRecordPrepareResult cluster_undo_record_requalify_for_retry(ClusterUndoRecordPrepareReceipt *receipt, uint16 payload_len, bool targets_invalidated) diff --git a/src/include/cluster/cluster_undo_record_api.h b/src/include/cluster/cluster_undo_record_api.h index 676d85d709..6dadd20bf6 100644 --- a/src/include/cluster/cluster_undo_record_api.h +++ b/src/include/cluster/cluster_undo_record_api.h @@ -119,7 +119,7 @@ typedef struct ClusterUndoRecordPrepareReceipt { uint8 ctrc_prepared_mask; uint8 ctrc_applied_mask; uint8 ctrc_reuse_mask; - uint8 ctrc_reserved8[4]; + uint32 ctrc_attempt_generation; UndoItlHistoryEntry itl_history[UNDO_ITL_HISTORY_TARGETS]; uint8 itl_history_mask; uint8 itl_history_reserved[7]; @@ -156,6 +156,18 @@ extern ClusterUndoRecordPrepareResult cluster_undo_record_requalify_for_retry(ClusterUndoRecordPrepareReceipt *receipt, uint16 payload_len, bool targets_invalidated); +/* No heap ownership held. Replace only UPDATE's unpublished page intents; + * the exact READY undo reservation and its original deadline are retained. */ +typedef enum ClusterUndoTargetResetResult { + CLUSTER_UNDO_TARGET_RESET_NOT_APPLICABLE = 0, + CLUSTER_UNDO_TARGET_RESET_READY, + CLUSTER_UNDO_TARGET_RESET_REFUSED +} ClusterUndoTargetResetResult; + +extern ClusterUndoTargetResetResult +cluster_undo_record_reset_update_targets(ClusterUndoRecordPrepareReceipt *receipt, + const ClusterCtrcTargetV1 *targets, uint8 required_mask); + extern uint64 cluster_undo_record_prepare_deadline_us(void); extern ClusterUndoRecordPrepareResult cluster_undo_record_prepare(uint8 record_type, uint16 payload_capacity, uint16 tt_slot_segment_id, diff --git a/src/test/cluster_unit/Makefile b/src/test/cluster_unit/Makefile index 923bef1cc8..1402165a49 100644 --- a/src/test/cluster_unit/Makefile +++ b/src/test/cluster_unit/Makefile @@ -4691,7 +4691,14 @@ test_cluster_heap_prepare_diagnostic.inc: $(top_srcdir)/src/backend/access/heap/ test_cluster_heap_prepare_diagnostic: test_cluster_heap_prepare_diagnostic.c unit_test.h test_cluster_heap_prepare_diagnostic.inc $(CC) $(CFLAGS) $(CPPFLAGS) $< -o $@ -test_cluster_heap_update_temp_lock: test_cluster_heap_update_temp_lock.c unit_test.h test_cluster_heap_update_temp_lock.inc test_cluster_heap_update_temp_consumer.inc test_cluster_heap_lock_return_receipt.inc test_cluster_heap_update_successor_header.inc test_cluster_heap_update_toast_resume.inc +# PGRAC: actual unlocked UPDATE child-intent decision, including refusal. +test_cluster_heap_update_child_retry.inc: $(top_srcdir)/src/backend/access/heap/heapam.c Makefile + awk '/if \(ctrc_target_mismatch && \(ctrc_failure_bits/ { emit=1; found++ } \ + emit && /if \(ctrc_prepare_only && !ctrc_target_mismatch\)/ { emit=0; done++ } \ + emit { print } END { if (found != 1 || done != 1) exit 1 }' $< > $@.tmp + mv $@.tmp $@ + +test_cluster_heap_update_temp_lock: test_cluster_heap_update_temp_lock.c unit_test.h test_cluster_heap_update_temp_lock.inc test_cluster_heap_update_temp_consumer.inc test_cluster_heap_lock_return_receipt.inc test_cluster_heap_update_successor_header.inc test_cluster_heap_update_toast_resume.inc test_cluster_heap_update_child_retry.inc $(CC) $(CFLAGS) $(CPPFLAGS) $< -o $@ test_cluster_r4_lock_order: test_cluster_r4_lock_order.c unit_test.h \ diff --git a/src/test/cluster_unit/ctrc_source_census.tsv b/src/test/cluster_unit/ctrc_source_census.tsv index b442ce0072..c6ec3e73c5 100644 --- a/src/test/cluster_unit/ctrc_source_census.tsv +++ b/src/test/cluster_unit/ctrc_source_census.tsv @@ -26,6 +26,7 @@ ctrc_cleaner_prepare_current_mx_successor src/backend/cluster/cluster_terminal_r cluster_undo_record_cancel_prepared src/backend/cluster/cluster_undo_record.c TERMINAL_PROJECTION_DISCHARGE CTRC_REF_HEAP_ITL_UBA CTRC_TARGET_PAGE_PENDING_ITL_SLOT|CTRC_TARGET_EXACT_ITL_SLOT NO_SUCCESSOR_PREMUTATION_CANCEL cluster_ctrc_receipt_cancel_shared CTRC_RECEIPT_LIFECYCLE 1 MXA-T23 cluster_undo_record_ctrc_apply_prepared src/backend/cluster/cluster_undo_record.c REGISTERED_REFERENCE CTRC_REF_HEAP_ITL_UBA CTRC_TARGET_PAGE_PENDING_ITL_SLOT|CTRC_TARGET_EXACT_ITL_SLOT cluster_heap_no_retry_boundary_apply cluster_ctrc_receipt_discharge_itl_shared CTRC_RECEIPT_LIFECYCLE 2 MXA-T23 cluster_undo_record_ctrc_prepare_pending src/backend/cluster/cluster_undo_record.c REGISTERED_REFERENCE CTRC_REF_HEAP_ITL_UBA CTRC_TARGET_PAGE_PENDING_ITL_SLOT|CTRC_TARGET_EXACT_ITL_SLOT cluster_heap_no_retry_boundary_apply cluster_ctrc_receipt_discharge_itl_shared CTRC_RECEIPT_LIFECYCLE 2 MXA-T23 +cluster_undo_record_reset_update_targets src/backend/cluster/cluster_undo_record.c TERMINAL_PROJECTION_DISCHARGE CTRC_REF_HEAP_ITL_UBA CTRC_TARGET_PAGE_PENDING_ITL_SLOT|CTRC_TARGET_EXACT_ITL_SLOT NO_SUCCESSOR_PREMUTATION_CANCEL cluster_ctrc_receipt_cancel_shared CTRC_RECEIPT_LIFECYCLE 1 MXA-T23 begin_heap_rewrite src/backend/access/heap/rewriteheap.c SUCCESSOR_BEFORE_PREDECESSOR ALL_CTRC_REFERENCE_KINDS ALL_RELATION_TARGETS NO_REFERENCE_TRANSFER_ALLOWED cluster_ctrc_relation_removal_ready_shared CTRC_RELATION_GATE 1 MXA-T27 cluster_ko_drain_inbound_and_apply src/backend/cluster/cluster_ko_lock.c SUCCESSOR_BEFORE_PREDECESSOR ALL_CTRC_REFERENCE_KINDS ALL_RELATION_TARGETS NO_REFERENCE_TRANSFER_ALLOWED cluster_ctrc_relation_removal_ready_shared CTRC_RELATION_GATE 1 MXA-T27 cluster_ko_flush_and_wait_ack src/backend/cluster/cluster_ko_lock.c SUCCESSOR_BEFORE_PREDECESSOR ALL_CTRC_REFERENCE_KINDS ALL_RELATION_TARGETS NO_REFERENCE_TRANSFER_ALLOWED cluster_ctrc_relation_removal_ready_shared CTRC_RELATION_GATE 1 MXA-T27 diff --git a/src/test/cluster_unit/data/r11-source-removal-census-v1.json b/src/test/cluster_unit/data/r11-source-removal-census-v1.json index 13119ef35e..4f51391e8f 100644 --- a/src/test/cluster_unit/data/r11-source-removal-census-v1.json +++ b/src/test/cluster_unit/data/r11-source-removal-census-v1.json @@ -16,7 +16,7 @@ "current_product_snapshot": { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "17f268db28256b655fef6bac7e92f5c7f15e1d5f17f291dc28555252f727063d" + "sha256": "e18d3eef8e166762085787e0949d1891964a4297ae4048a6c1a0ebcc28b985aa" }, "gates": { "L1": { diff --git a/src/test/cluster_unit/generate_ctrc_source_census.py b/src/test/cluster_unit/generate_ctrc_source_census.py index 96c98fd9ac..dccebbcc79 100644 --- a/src/test/cluster_unit/generate_ctrc_source_census.py +++ b/src/test/cluster_unit/generate_ctrc_source_census.py @@ -410,7 +410,7 @@ def _classify_owners( _classify_owners( "CTRC_RECEIPT_LIFECYCLE", "src/backend/cluster/cluster_undo_record.c", - ("cluster_undo_record_cancel_prepared",), + ("cluster_undo_record_cancel_prepared", "cluster_undo_record_reset_update_targets"), "TERMINAL_PROJECTION_DISCHARGE", "CTRC_REF_HEAP_ITL_UBA", "CTRC_TARGET_PAGE_PENDING_ITL_SLOT|CTRC_TARGET_EXACT_ITL_SLOT", diff --git a/src/test/cluster_unit/test_cluster_heap_update_temp_lock.c b/src/test/cluster_unit/test_cluster_heap_update_temp_lock.c index 4175c64253..5f328074da 100644 --- a/src/test/cluster_unit/test_cluster_heap_update_temp_lock.c +++ b/src/test/cluster_unit/test_cluster_heap_update_temp_lock.c @@ -482,11 +482,62 @@ UT_TEST(real_successor_preserves_all_other_planner_branches) check_successor_header(leg); } +static ClusterUndoTargetResetResult child_reset_result; +static int child_reset_calls; + +ClusterUndoTargetResetResult +cluster_undo_record_reset_update_targets(ClusterUndoRecordPrepareReceipt *receipt, + const ClusterCtrcTargetV1 *targets, uint8 required_mask) +{ + UT_ASSERT(!content_locked); + UT_ASSERT_EQ(required_mask, 3); + child_reset_calls++; + return child_reset_result; +} + +UT_TEST(real_update_child_rebind_refusal_cannot_fall_back) +{ + for (int leg = 0; leg < 6; leg++) { + ClusterUndoRecordPrepareReceipt undo_receipt = { 0 }; + ClusterCanonicalTxnBinding canonical_binding = { 0 }; + ClusterCtrcTargetV1 ctrc_pending_targets[2] = { 0 }; + uint8 ctrc_required_mask = 3; + uint8 ctrc_failure_bits = leg == 4 ? 1 : 0; + volatile bool ctrc_target_mismatch = leg != 3; + volatile bool ctrc_prepare_only = false; + volatile bool caught = false; + + content_locked = false; + child_reset_calls = 0; + child_reset_result = leg == 1 ? CLUSTER_UNDO_TARGET_RESET_REFUSED + : leg == 2 ? CLUSTER_UNDO_TARGET_RESET_NOT_APPLICABLE + : CLUSTER_UNDO_TARGET_RESET_READY; + undo_receipt.tt_slot_segment_id = canonical_binding.segment_id = 17; + undo_receipt.tt_slot_offset = canonical_binding.slot_offset = 4; + if (leg == 5) + canonical_binding.slot_offset++; + PG_TRY(); + { +#include "test_cluster_heap_update_child_retry.inc" + } + PG_CATCH(); + { + caught = true; + } + PG_END_TRY(); + UT_ASSERT_EQ(caught, leg == 1); + UT_ASSERT_EQ(child_reset_calls, leg < 3 ? 1 : 0); + UT_ASSERT_EQ(ctrc_prepare_only, leg == 0); + UT_ASSERT_EQ(ctrc_target_mismatch, leg != 0 && leg != 3); + } +} + int main(void) { UT_PLAN(15); UT_RUN(resume_never_replaces_a_live_receipt_or_renews_without_a_handoff); + UT_RUN(real_update_child_rebind_refusal_cannot_fall_back); UT_RUN(real_toast_return_uses_the_completed_handoff_boundary); UT_RUN(completed_nested_producer_starts_its_own_preparation_phase); UT_RUN(real_successor_does_not_inherit_own_temporary_lock); 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 a543357d02..db9c1aa2e1 100644 --- a/src/test/cluster_unit/test_cluster_r4_lock_order.c +++ b/src/test/cluster_unit/test_cluster_r4_lock_order.c @@ -2932,6 +2932,62 @@ UT_TEST(test_post_snapshot_matching_xmin_uses_holder_full) BufferBlocks = NULL; } +/* A local creator may remain in the native snapshot after its canonical + * commit is visible at the statement SCN. The prior version already uses + * FULL; this successor must use the same authority, never native xip. */ +UT_TEST(test_local_matching_creator_uses_statement_scn_not_native_membership) +{ + for (int leg = 0; leg < 4; leg++) { + UtR4HotProductFixture fixture; + HeapHotSearchResult result; + RelationData relation = { 0 }; + FormData_pg_class form = { 0 }; + SnapshotData snapshot = { 0 }; + ItemPointerData tid; + HeapHotSearchResultKind kind; + + ut_r4_hot_init_product_fixture(&fixture, &result); + ut_hot_live_ref.local_xid = UT_HOT_LIVE_XMIN; + ut_hot_live_ref.origin_node_id = cluster_node_id; + ClusterPageGetItlSlots((Page)fixture.live_page)[2].write_scn = UT_HOT_READ_SCN - 2; + (void)ut_r4_hot_build_page(fixture.full_source, UT_HOT_LIVE_XMIN, 1, UT_HOT_LIVE_XMIN, 4, + UT_HOT_PAYLOAD); + ut_r4_hot_tuple_at((Page)fixture.full_source, UT_HOT_ROOT_OFF)->t_infomask + = HEAP_XMAX_INVALID; + PageSetLSN((Page)fixture.full_source, UINT64_C(0x123450)); + fixture.mutate_hint_offset = UT_HOT_ROOT_OFF; + ut_r4_hot_reset_scratch_authority((Page)result.scratch_page, (Page)fixture.live_page, + UT_HOT_READ_SCN, UINT64_C(0x123450)); + ut_scratch_expected_ref.local_xid = UT_HOT_LIVE_XMIN; + ut_scratch_expected_xid = UT_HOT_LIVE_XMIN; + ut_scratch_resolve_status = leg == 2 ? CLUSTER_TT_STATUS_IN_PROGRESS + : leg == 3 ? CLUSTER_TT_STATUS_ABORTED + : CLUSTER_TT_STATUS_COMMITTED; + ut_scratch_resolve_scn = UT_HOT_READ_SCN + (leg == 1 ? 1 : -1); + ut_live_visible_offnum = MaxHeapTuplesPerPage; + relation.rd_id = UT_HOT_TABLE_OID; + relation.rd_rel = &form; + form.relpersistence = RELPERSISTENCE_PERMANENT; + snapshot.snapshot_type = SNAPSHOT_MVCC; + snapshot.cluster_source = SNAPSHOT_SOURCE_CLUSTER; + snapshot.read_scn = UT_HOT_READ_SCN; + snapshot.read_epoch = 9; + ItemPointerSet(&tid, UT_HOT_BLOCK, UT_HOT_ROOT_OFF); + kind = heap_hot_search_buffer_result(&tid, &relation, UT_HOT_BUFFER, &snapshot, &result, + NULL, true); + UT_ASSERT_EQ(kind, leg == 0 ? HEAP_HOT_SEARCH_OWNED_SCRATCH : HEAP_HOT_SEARCH_NOT_FOUND); + UT_ASSERT_EQ(fixture.fetch_calls, 1); + UT_ASSERT_EQ(ut_live_visibility_calls, 0); + UT_ASSERT_EQ(ut_scratch_exact_resolve_calls, 1); + UT_ASSERT(ut_hot_content_lock_held); + LockBuffer(UT_HOT_BUFFER, BUFFER_LOCK_UNLOCK); + ut_hot_production_core_active = false; + ut_hot_product_fixture = NULL; + ut_hot_live_ref_page = NULL; + BufferBlocks = NULL; + } +} + /* Frozen creation is a tuple proof, not a claim that its old page slot still * names xmin. Keep the real scratch evaluator and trap all live-page paths. */ static void @@ -6820,6 +6876,7 @@ main(void) UT_RUN(test_post_snapshot_own_xmin_keeps_command_visibility); UT_RUN(test_census_clearing_target_lock_returns_to_dml_owner); UT_RUN(test_post_snapshot_matching_xmin_uses_holder_full); + UT_RUN(test_local_matching_creator_uses_statement_scn_not_native_membership); UT_RUN(test_census_changed_page_returns_to_dml_requalification); UT_RUN(test_hot_prune_without_xmin_hint_never_reads_requester_clog); UT_RUN(test_hot_prune_preserves_excluded_tuple_snapshot_and_locator_shapes); diff --git a/src/test/cluster_unit/test_cluster_terminal_ref_census.c b/src/test/cluster_unit/test_cluster_terminal_ref_census.c index b31cad8a78..ab1c8739db 100644 --- a/src/test/cluster_unit/test_cluster_terminal_ref_census.c +++ b/src/test/cluster_unit/test_cluster_terminal_ref_census.c @@ -611,6 +611,48 @@ UT_TEST(test_ctrc_receipt_prepare_apply_full_identity_cross_product) UT_ASSERT_EQ(receipt.target.kind, CTRC_TARGET_EXACT_ITL_SLOT); } +UT_TEST(test_cancelled_child_attempt_cannot_alias_its_replacement) +{ + ClusterCtrcParticipantEntry participant; + ClusterCtrcTxnKeyV1 key = test_key(); + ClusterCtrcParticipantIdentity identity = test_participant_identity(2); + ClusterCtrcPublicationIdV1 publication + = test_publication(31, CTRC_REF_HEAP_ITL_UBA, CTRC_TARGET_PAGE_PENDING_ITL_SLOT); + ClusterCtrcTargetV1 pending = test_pending_itl_target(); + ClusterCtrcTargetV1 exact = test_exact_itl_target(); + ClusterCtrcReceipt receipts[8] = { 0 }; + uint8 probes[8] = { 0 }; + uint64 old_index, new_index; + ClusterCtrcApplyToken token; + + test_open_participant(&participant); + UT_ASSERT_EQ(cluster_ctrc_receipt_prepare_table_locked( + &participant, &key, &identity, TEST_GRANT, &publication, &pending, receipts, + probes, lengthof(receipts), 100, &old_index, NULL), + CLUSTER_CTRC_PREPARE_READY); + UT_ASSERT(cluster_ctrc_receipt_cancel_prepared(&participant, &receipts[old_index])); + publication.attempt_generation++; + pending.block_number++; + exact.block_number++; + UT_ASSERT_EQ(cluster_ctrc_receipt_prepare_table_locked( + &participant, &key, &identity, TEST_GRANT, &publication, &pending, receipts, + probes, lengthof(receipts), 101, &new_index, NULL), + CLUSTER_CTRC_PREPARE_READY); + UT_ASSERT(new_index != old_index); + UT_ASSERT_EQ(receipts[old_index].state, CTRC_RECEIPT_CANCELLED); + UT_ASSERT_EQ( + cluster_ctrc_receipt_apply_prepared(&participant, &receipts[old_index], &exact, &token), + CLUSTER_CTRC_APPLY_FAIL_CLOSED); + UT_ASSERT(!token.valid); + UT_ASSERT_EQ( + cluster_ctrc_receipt_apply_prepared(&participant, &receipts[new_index], &exact, &token), + CLUSTER_CTRC_APPLY_APPLIED); + UT_ASSERT(token.valid); + UT_ASSERT_EQ(participant.prepared_count, 0); + UT_ASSERT_EQ(participant.cancelled_count, 1); + UT_ASSERT_EQ(participant.applied_count, 1); +} + UT_TEST(test_ctrc_unpublished_itl_apply_accepts_forward_page_version) { unsigned variant; @@ -3109,6 +3151,7 @@ main(void) CTRC_TEST_ENTRY(test_ctrc_epoch_zero_identity_is_present_and_exact), CTRC_TEST_ENTRY(test_ctrc_delayed_positive_proof_revalidates_open_grant), CTRC_TEST_ENTRY(test_ctrc_receipt_prepare_apply_full_identity_cross_product), + CTRC_TEST_ENTRY(test_cancelled_child_attempt_cannot_alias_its_replacement), CTRC_TEST_ENTRY(test_ctrc_unpublished_itl_apply_accepts_forward_page_version), CTRC_TEST_ENTRY(test_ctrc_unpublished_itl_reacquired_current_binds_only_at_apply), CTRC_TEST_ENTRY(test_ctrc_unpublished_itl_version_floor_keeps_identity_and_negative_fences), diff --git a/src/test/cluster_unit/test_cluster_undo_record.c b/src/test/cluster_unit/test_cluster_undo_record.c index 55c42ad696..a6ee64aadf 100644 --- a/src/test/cluster_unit/test_cluster_undo_record.c +++ b/src/test/cluster_unit/test_cluster_undo_record.c @@ -135,6 +135,8 @@ static bool receipt_buffer_locked; static int receipt_prepare_calls; static int receipt_apply_calls; static int receipt_cancel_calls; +static int receipt_cancel_fail_at; +static uint32 receipt_child_attempt; static int receipt_leave_calls; static int receipt_unref_calls; static int receipt_install_calls; @@ -418,6 +420,7 @@ cluster_ctrc_receipt_prepare_shared( { UT_ASSERT_EQ(grant, 1); UT_ASSERT_EQ(publication->wire_request_id, cluster_undo_record_reservation.sequence); + receipt_child_attempt = publication->attempt_generation; memset(handle, 0, sizeof(*handle)); handle->valid = true; receipt_prepare_calls++; @@ -449,7 +452,7 @@ cluster_ctrc_receipt_cancel_shared(const ClusterCtrcReceiptHandle *handle) { UT_ASSERT(handle->valid); receipt_cancel_calls++; - return true; + return receipt_cancel_calls != receipt_cancel_fail_at; } @@ -1892,6 +1895,7 @@ UT_TEST(test_terminal_census_precedes_final_receipt_recheck_and_itl_allocation) UT_TEST(test_ctrc_cross_page_update_requires_both_prepared_receipts) { ClusterUndoRecordPrepareReceipt receipt = { 0 }; + receipt.ctrc_attempt_generation = 1; receipt.ctrc_pending_mask = UINT8_C(3); receipt.ctrc_prepared_mask = UINT8_C(3); @@ -2532,6 +2536,8 @@ receipt_fixture_ready(uint8 record_type, ClusterUndoRecordPrepareReceipt *receip receipt_buffer_available = true; receipt_buffer_locked = false; receipt_prepare_calls = receipt_apply_calls = receipt_cancel_calls = 0; + receipt_cancel_fail_at = 0; + receipt_child_attempt = 0; receipt_leave_calls = receipt_unref_calls = receipt_install_calls = 0; cluster_undo_current_extent.segment_id = 1; cluster_undo_current_extent.first_block = 1; @@ -2548,6 +2554,7 @@ receipt_fixture_ready(uint8 record_type, ClusterUndoRecordPrepareReceipt *receip receipt->actual_segment_id = 1; receipt->reservation_sequence = 17; receipt->absolute_deadline_us = 100; + receipt->ctrc_attempt_generation = 1; receipt->extent = cluster_undo_current_extent; receipt->modifier_admission.entered = true; reservation->active = true; @@ -2764,6 +2771,155 @@ UT_TEST(test_prepare_budget_remains_ten_seconds_without_refresh) UT_ASSERT_EQ(cluster_undo_record_prepare_deadline_us(), 0); } +static void +receipt_update_targets_fixture(ClusterUndoRecordPrepareReceipt *receipt, + ClusterCtrcTargetV1 targets[CLUSTER_UNDO_RECORD_CTRC_TARGETS]) +{ + receipt_fixture_ready(UNDO_RECORD_UPDATE, receipt); + memset(targets, 0, sizeof(*targets) * CLUSTER_UNDO_RECORD_CTRC_TARGETS); + for (int i = 0; i < CLUSTER_UNDO_RECORD_CTRC_TARGETS; i++) { + targets[i].kind = CTRC_TARGET_PAGE_PENDING_ITL_SLOT; + targets[i].spc_oid = 1663; + targets[i].db_oid = 5; + targets[i].rel_number = 16386; + targets[i].fork_number = MAIN_FORKNUM; + targets[i].block_number = 16949 + i; + targets[i].predecessor_page_lsn_origin_node_id = CLUSTER_CTRC_PAGE_LSN_ORIGIN_INVALID; + targets[i].publication_own_generation = 1; + targets[i].publication_acquisition_epoch = 7; + targets[i].relation_persistence = 'p'; + targets[i].needs_wal = true; + targets[i].page_operation_kind = UNDO_RECORD_UPDATE; + receipt->ctrc_pending_targets[i] = targets[i]; + receipt->ctrc_handles[i].valid = true; + } + receipt->ctrc_pending_mask = receipt->ctrc_prepared_mask = 3; + receipt->itl_history_mask = 3; + UT_ASSERT(cluster_undo_record_receipt_sync(receipt)); + receipt_clock_us = 101; +} + +UT_TEST(test_update_target_retry_keeps_ready_extent_deadline_and_owns_children) +{ + ClusterUndoRecordPrepareReceipt receipt, before; + ClusterCtrcTargetV1 targets[CLUSTER_UNDO_RECORD_CTRC_TARGETS]; + + receipt_update_targets_fixture(&receipt, targets); + before = receipt; + for (int pass = 0; pass < 3; pass++) { + uint8 mask = pass == 1 ? 1 : 3; + int cancels_before = receipt_cancel_calls; + int expected_cancels = pass == 2 ? 1 : 2; + + targets[1].block_number += 5; + UT_ASSERT_EQ(cluster_undo_record_reset_update_targets(&receipt, targets, mask), + CLUSTER_UNDO_TARGET_RESET_READY); + UT_ASSERT(cluster_undo_record_reservation.active); + UT_ASSERT(cluster_undo_record_reservation.owns_ref); + UT_ASSERT_EQ(receipt.magic, before.magic); + UT_ASSERT_EQ(receipt.reservation_sequence, before.reservation_sequence); + UT_ASSERT_EQ(receipt.absolute_deadline_us, before.absolute_deadline_us); + UT_ASSERT_EQ(memcmp(&receipt.extent, &before.extent, sizeof(receipt.extent)), 0); + UT_ASSERT_EQ(memcmp(&receipt.modifier_admission, &before.modifier_admission, + sizeof(receipt.modifier_admission)), + 0); + UT_ASSERT_EQ(receipt.ctrc_attempt_generation, 2 + pass); + UT_ASSERT_EQ(receipt.ctrc_pending_mask, mask); + UT_ASSERT_EQ(receipt.ctrc_prepared_mask | receipt.itl_history_mask, 0); + UT_ASSERT_EQ(receipt_cancel_calls - cancels_before, expected_cancels); + UT_ASSERT_EQ(receipt_leave_calls + receipt_unref_calls + receipt_apply_calls, 0); + UT_ASSERT(cluster_undo_record_ctrc_prepare_pending(&receipt, 0)); + if (mask == 3) + UT_ASSERT(cluster_undo_record_ctrc_prepare_pending(&receipt, 1)); + UT_ASSERT_EQ(receipt_child_attempt, 2 + pass); + } +} + +UT_TEST(test_update_target_retry_refuses_authority_and_publication_drift) +{ + for (int leg = 0; leg < 14; leg++) { + ClusterUndoRecordPrepareReceipt receipt, before; + ClusterCtrcTargetV1 targets[CLUSTER_UNDO_RECORD_CTRC_TARGETS]; + uint8 mask = 3; + + receipt_update_targets_fixture(&receipt, targets); + targets[1].block_number++; + switch (leg) { + case 0: + receipt.record_type = UNDO_RECORD_DELETE; + break; + case 1: + receipt.ctrc_applied_mask = 1; + break; + case 2: + cluster_undo_record_reservation.consume_locked = true; + break; + case 3: + cluster_undo_current_extent.cur_block++; + break; + case 4: + receipt_modifier_valid = false; + break; + case 5: + receipt_block0_valid = false; + break; + case 6: + targets[0].block_number++; + break; + case 7: + targets[1].rel_number++; + break; + case 8: + targets[1].publication_acquisition_epoch++; + break; + case 9: + targets[1].page_operation_kind = UNDO_RECORD_INSERT; + break; + case 10: + receipt.ctrc_attempt_generation = UINT32_MAX; + break; + case 11: + mask = 2; + break; + case 12: + targets[1].publication_own_generation = 0; + break; + case 13: + targets[1].block_number--; + break; + } + UT_ASSERT(cluster_undo_record_receipt_sync(&receipt)); + before = receipt; + UT_ASSERT(!cluster_undo_record_reset_update_targets(&receipt, targets, mask)); + UT_ASSERT_EQ(memcmp(&receipt, &before, sizeof(receipt)), 0); + UT_ASSERT_EQ(receipt_cancel_calls + receipt_leave_calls + receipt_unref_calls, 0); + } +} + +UT_TEST(test_update_target_retry_retains_borrowed_and_failed_cancel_owners) +{ + for (int leg = 0; leg < 3; leg++) { + ClusterUndoRecordPrepareReceipt receipt, before; + ClusterCtrcTargetV1 targets[CLUSTER_UNDO_RECORD_CTRC_TARGETS]; + + receipt_update_targets_fixture(&receipt, targets); + targets[1].block_number++; + if (leg == 0) + receipt.ctrc_reuse_mask = 1; + else + receipt_cancel_fail_at = leg; + UT_ASSERT(cluster_undo_record_receipt_sync(&receipt)); + before = receipt; + UT_ASSERT_EQ(cluster_undo_record_reset_update_targets(&receipt, targets, 3), + leg == 0 ? CLUSTER_UNDO_TARGET_RESET_READY + : CLUSTER_UNDO_TARGET_RESET_REFUSED); + UT_ASSERT_EQ(receipt_cancel_calls, leg == 2 ? 2 : 1); + UT_ASSERT_EQ(receipt_leave_calls + receipt_unref_calls + receipt_apply_calls, 0); + if (leg != 0) + UT_ASSERT_EQ(memcmp(&receipt, &before, sizeof(receipt)), 0); + } +} + UT_TEST(test_heap_retry_preserves_exact_ready_before_considering_cancel) { char *source = read_heapam_source(); @@ -3492,6 +3648,9 @@ main(int argc, char **argv) UT_RUN(test_ready_identity_mismatch_is_not_hidden_by_lifetime_fix); UT_RUN(test_partial_apply_cancel_releases_only_unapplied_nonreuse_handle); UT_RUN(test_prepare_budget_remains_ten_seconds_without_refresh); + UT_RUN(test_update_target_retry_keeps_ready_extent_deadline_and_owns_children); + UT_RUN(test_update_target_retry_refuses_authority_and_publication_drift); + UT_RUN(test_update_target_retry_retains_borrowed_and_failed_cancel_owners); UT_RUN(test_heap_retry_preserves_exact_ready_before_considering_cancel); UT_RUN(test_retry_requalification_keeps_ready_and_proves_actual_invalidation); UT_RUN(test_retry_after_apply_refuses_without_canceling_shared_owner); diff --git a/src/tools/check_r11_source_removal_census.py b/src/tools/check_r11_source_removal_census.py index fc26d71c02..821a35cbfb 100644 --- a/src/tools/check_r11_source_removal_census.py +++ b/src/tools/check_r11_source_removal_census.py @@ -24,7 +24,7 @@ CURRENT_PRODUCT_SNAPSHOT = { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "17f268db28256b655fef6bac7e92f5c7f15e1d5f17f291dc28555252f727063d", + "sha256": "e18d3eef8e166762085787e0949d1891964a4297ae4048a6c1a0ebcc28b985aa", } LAYERS = { From 80f50c9ff533ef5f90dc46c09b435028569fb522 Mon Sep 17 00:00:00 2001 From: SqlRush Date: Fri, 18 Sep 2026 21:30:15 +0800 Subject: [PATCH 07/10] fix(cluster): wait for coherent remote S reservations --- src/backend/cluster/cluster_gcs_block.c | 26 +++- src/backend/storage/buffer/bufmgr.c | 16 ++- src/test/cluster_unit/Makefile | 3 +- .../data/r11-source-removal-census-v1.json | 2 +- src/test/cluster_unit/test_cluster_pcm_own.c | 114 +++++++++++++++++- src/tools/check_r11_source_removal_census.py | 2 +- 6 files changed, 151 insertions(+), 12 deletions(-) diff --git a/src/backend/cluster/cluster_gcs_block.c b/src/backend/cluster/cluster_gcs_block.c index 0d492925b1..1b17a784c9 100644 --- a/src/backend/cluster/cluster_gcs_block.c +++ b/src/backend/cluster/cluster_gcs_block.c @@ -11330,6 +11330,8 @@ typedef struct ResourceXFirstFailureEvidence { int32 buffer_own_result; uint8 buffer_pcm_state_before; uint8 buffer_pcm_state_after; + uint8 remote_s_image_type; + uint32 remote_s_semantic_state; uint64 base_authority_generation; uint64 authority_generation; uint64 assertion_sequence; @@ -11497,7 +11499,7 @@ gcs_block_resource_x_first_failure_record(const ResourceXFirstFailureEvidence *e LOG, (errmsg_internal("Resource-X first-failure diagnostic"), errdetail( - "tag_hash=%u binding_generation=%llu " + "tag_hash=%u tag=%u/%u/%u/%u/%u binding_generation=%llu " "request_sequence=%llu admission_generation=%llu " "buffer_generation_before=%llu buffer_generation_after=%llu " "buffer_token_before=%llu buffer_token_after=%llu " @@ -11506,6 +11508,7 @@ gcs_block_resource_x_first_failure_record(const ResourceXFirstFailureEvidence *e "buffer_resource_x_generation_after=%llu " "buffer_flags_before=0x%08x buffer_flags_after=0x%08x " "buffer_pcm_state_before=%u buffer_pcm_state_after=%u " + "remote_s_image_type=%u remote_s_semantic_state=0x%08x " "buffer_own_result=%d " "formation=%llu master_session=%llu r4_generation=%llu " "base_authority_generation=%llu authority_generation=%llu " @@ -11544,7 +11547,9 @@ gcs_block_resource_x_first_failure_record(const ResourceXFirstFailureEvidence *e "refused_resource_x=%llu refused_retained=%llu " "refused_requester=%llu refused_sidecar=%llu " "deadline=%llu", - tag_hash, (unsigned long long)evidence->binding_generation, + tag_hash, evidence->tag.spcOid, evidence->tag.dbOid, evidence->tag.relNumber, + (unsigned)evidence->tag.forkNum, evidence->tag.blockNum, + (unsigned long long)evidence->binding_generation, (unsigned long long)evidence->request_sequence, (unsigned long long)(evidence->admission_generation != 0 ? evidence->admission_generation @@ -11559,7 +11564,8 @@ gcs_block_resource_x_first_failure_record(const ResourceXFirstFailureEvidence *e (unsigned long long)evidence->buffer_resource_x_generation_after, evidence->buffer_flags_before, evidence->buffer_flags_after, (unsigned)evidence->buffer_pcm_state_before, - (unsigned)evidence->buffer_pcm_state_after, + (unsigned)evidence->buffer_pcm_state_after, (unsigned)evidence->remote_s_image_type, + evidence->remote_s_semantic_state, evidence->remote_s_stage != RESOURCE_X_REMOTE_S_STAGE_NONE ? evidence->buffer_own_result : -1, @@ -12052,6 +12058,20 @@ gcs_block_pcm_x_resource_x_remote_s_holder_block_to_n( first_failure.r4_generation = r4_record_generation; first_failure.buffer_generation_before = current.generation; first_failure.buffer_generation_after = current.generation; + first_failure.buffer_token_before = current.reservation_token; + first_failure.buffer_token_after = current.reservation_token; + first_failure.buffer_writer_token_before = current.writer_activation_token; + first_failure.buffer_writer_token_after = current.writer_activation_token; + first_failure.buffer_resource_x_generation_before + = current.resource_x_activation_generation; + first_failure.buffer_resource_x_generation_after = current.resource_x_activation_generation; + first_failure.buffer_flags_before = current.flags; + first_failure.buffer_flags_after = current.flags; + first_failure.buffer_pcm_state_before = current.pcm_state; + first_failure.buffer_pcm_state_after = current.pcm_state; + first_failure.buffer_own_result = (int32)own_result; + first_failure.remote_s_image_type = current.buffer_type; + first_failure.remote_s_semantic_state = current.semantic_buf_state; gcs_block_resource_x_first_failure_record(&first_failure); gcs_block_resource_x_failure_decision_apply(&failure_decision); return mapped_result; diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 81b2f7e6fd..e4991c138d 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -1033,10 +1033,7 @@ cluster_bufmgr_pcm_own_s_holder_candidate_exact( uint32 buf_state; if (buf == NULL || expected_s == NULL - || expected_s->pcm_state != (uint8)PCM_STATE_S - || expected_s->flags != 0 - || expected_s->writer_activation_token != 0 - || expected_s->resource_x_activation_generation != 0) + || expected_s->pcm_state != (uint8)PCM_STATE_S) return CLUSTER_PCM_OWN_INVALID; if (ClusterPcmOwnArray == NULL) return CLUSTER_PCM_OWN_NOT_READY; @@ -1047,8 +1044,17 @@ cluster_bufmgr_pcm_own_s_holder_candidate_exact( else if (!cluster_pcm_x_current_image_shape( live.pcm_state, live.buffer_type, (live.semantic_buf_state & BM_VALID) != 0) - || (live.semantic_buf_state & BM_IO_ERROR) != 0) + || (live.semantic_buf_state & BM_IO_ERROR) != 0 + || live.generation == UINT64_MAX + || live.reservation_token == UINT64_MAX + || live.writer_activation_token != 0 + || live.resource_x_activation_generation != 0) result = CLUSTER_PCM_OWN_CORRUPT; + else if (live.flags != 0) + /* A concurrent local source reservation still owns this exact S + * image. The remote revoke must wait, not claim corruption or ACK N. + * Unknown/combined flags and a missing token remain hard failures. */ + result = cluster_pcm_own_classify_live_flags(live.flags, live.reservation_token); else if ((live.semantic_buf_state & (BM_DIRTY | BM_JUST_DIRTIED | BM_CHECKPOINT_NEEDED | BM_IO_IN_PROGRESS)) != 0) diff --git a/src/test/cluster_unit/Makefile b/src/test/cluster_unit/Makefile index 1402165a49..a9d2d4be5b 100644 --- a/src/test/cluster_unit/Makefile +++ b/src/test/cluster_unit/Makefile @@ -2791,6 +2791,7 @@ test_cluster_pcm_transition_owner.inc: $(top_srcdir)/src/backend/storage/buffer/ awk '/^cluster_pcm_own_bump_failure\(/ { print "static ClusterPcmOwnResult"; emit=1; bump_failure++ } \ /^cluster_bufmgr_pcm_own_snapshot\(/ { print "ClusterPcmOwnResult"; emit=1; ordinary_snapshot++ } \ /^cluster_bufmgr_pcm_own_n_assertion_candidate_exact\(/ { print "ClusterPcmOwnResult"; emit=1; n_candidate++ } \ + /^cluster_bufmgr_pcm_own_s_holder_candidate_exact\(/ { print "ClusterPcmOwnResult"; emit=1; s_candidate++ } \ /^cluster_bufmgr_pcm_own_n_retained_release_inflight_exact\(/ { print "bool"; emit=1; retained_candidate++ } \ /^cluster_bufmgr_pcm_own_n_storage_candidate_exact\(/ { print "ClusterPcmOwnResult"; emit=1; storage_candidate++ } \ /^cluster_bufmgr_pcm_own_n_predecessor_observe_exact\(/ { print "ClusterPcmOwnResult"; emit=1; predecessor_observe++ } \ @@ -2824,7 +2825,7 @@ test_cluster_pcm_transition_owner.inc: $(top_srcdir)/src/backend/storage/buffer/ /^cluster_bufmgr_pcm_own_reclaim_read_image_for_delivery_exact\(/ { print "ClusterPcmOwnResult"; emit=1; read_delivery++ } \ /^cluster_pcm_own_abort_grant_reservation\(/ { print "static ClusterPcmOwnResult"; emit=1; grant_abort++ } \ emit { print } /^}/ { emit=0 } \ - END { if (retained_candidate != 1 || storage_candidate != 1 || predecessor_observe != 1 || read_reclaim != 1 || read_delivery != 1) exit 1 } \ + END { if (s_candidate != 1 || retained_candidate != 1 || storage_candidate != 1 || predecessor_observe != 1 || read_reclaim != 1 || read_delivery != 1) exit 1 } \ END { if (ordinary_snapshot != 1 || n_candidate != 1 || bump_failure != 1 || bump != 1 || evict != 1 || shape != 1 || wal_copy != 1 || known_new != 1 || ordinary_lookup != 1 || direct_lookup != 1 || sidecar != 1 || activate != 1 || clear != 1 || copy != 1 || held_valid != 1 || held_validate != 1 || held_adopt != 1 || held_finish != 1 || held_abandon != 1 || drop != 1 || finish != 1 || delivery_begin != 1 || delivery_snapshot != 1 || delivery_release != 1 || normal_reserve != 1 || delivery_reserve != 1 || read_publish != 1 || read_clear != 1 || read_release != 1 || grant_abort != 1) exit 1 }' $< > $@.tmp mv $@.tmp $@ diff --git a/src/test/cluster_unit/data/r11-source-removal-census-v1.json b/src/test/cluster_unit/data/r11-source-removal-census-v1.json index 4f51391e8f..fbb2d0371f 100644 --- a/src/test/cluster_unit/data/r11-source-removal-census-v1.json +++ b/src/test/cluster_unit/data/r11-source-removal-census-v1.json @@ -16,7 +16,7 @@ "current_product_snapshot": { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "e18d3eef8e166762085787e0949d1891964a4297ae4048a6c1a0ebcc28b985aa" + "sha256": "c3f2ab07f937e7b71c8c4791d3ff1a04f99f9eacc6a989b67c65e2a57fccee4d" }, "gates": { "L1": { diff --git a/src/test/cluster_unit/test_cluster_pcm_own.c b/src/test/cluster_unit/test_cluster_pcm_own.c index 4afbc784e9..8677a53f46 100644 --- a/src/test/cluster_unit/test_cluster_pcm_own.c +++ b/src/test/cluster_unit/test_cluster_pcm_own.c @@ -4301,6 +4301,116 @@ UT_TEST(test_remote_s_holder_pending_grant_is_retryable_busy) CLUSTER_PCM_OWN_INVALID); } +UT_TEST(test_real_remote_s_candidate_waits_for_exact_local_reservation) +{ + BufferDesc buf; + ClusterPcmOwnEntry entry; + ClusterPcmOwnEntry *saved = ClusterPcmOwnArray; + ClusterPcmOwnSnapshot before, after; + uint32 flags[] = { PCM_OWN_FLAG_GRANT_PENDING, PCM_OWN_FLAG_REVOKING }; + uint64 token; + uint32 state; + + for (int i = 0; i < lengthof(flags); i++) { + n_predecessor_fixture(&buf, &entry, BUF_TYPE_SCUR); + buf.pcm_state = PCM_STATE_S; + buf.tag.forkNum = MAIN_FORKNUM; + buf.tag.blockNum = 12516; + state = transition_lock_header(&buf); + UT_ASSERT_EQ(cluster_pcm_own_reservation_begin_exact(0, 48, flags[i], &token), + CLUSTER_PCM_OWN_OK); + UnlockBufHdr(&buf, state); + UT_ASSERT_EQ(cluster_bufmgr_pcm_own_snapshot(&buf, &before), CLUSTER_PCM_OWN_OK); + UT_ASSERT_EQ(cluster_bufmgr_pcm_own_s_holder_candidate_exact(&buf, &before), + CLUSTER_PCM_OWN_BUSY); + UT_ASSERT_EQ(cluster_bufmgr_pcm_own_snapshot(&buf, &after), CLUSTER_PCM_OWN_OK); + UT_ASSERT(cluster_pcm_own_snapshot_equal_exact(&before, &after)); + UT_ASSERT((pg_atomic_read_u32(&buf.state) & BM_LOCKED) == 0); + state = transition_lock_header(&buf); + UT_ASSERT_EQ(cluster_pcm_own_reservation_abort_exact(0, 48, token, flags[i]), + CLUSTER_PCM_OWN_OK); + UnlockBufHdr(&buf, state); + UT_ASSERT_EQ(cluster_bufmgr_pcm_own_snapshot(&buf, &after), CLUSTER_PCM_OWN_OK); + UT_ASSERT_EQ(cluster_bufmgr_pcm_own_s_holder_candidate_exact(&buf, &after), + CLUSTER_PCM_OWN_OK); + UT_ASSERT_EQ(cluster_bufmgr_pcm_own_s_holder_candidate_exact(&buf, &before), + CLUSTER_PCM_OWN_STALE); + } + ClusterPcmOwnArray = saved; +} + +UT_TEST(test_real_remote_s_candidate_preserves_corruption_and_observation_guards) +{ + BufferDesc buf; + ClusterPcmOwnEntry entry; + ClusterPcmOwnEntry *saved = ClusterPcmOwnArray; + ClusterPcmOwnSnapshot before, after; + + for (int leg = 0; leg < 16; leg++) { + n_predecessor_fixture(&buf, &entry, BUF_TYPE_SCUR); + buf.pcm_state = PCM_STATE_S; + pg_atomic_write_u32(&entry.flags, PCM_OWN_FLAG_GRANT_PENDING); + switch (leg) { + case 0: + pg_atomic_write_u32(&entry.flags, 3); + break; + case 1: + pg_atomic_write_u32(&entry.flags, 4); + break; + case 2: + pg_atomic_write_u64(&entry.reservation_token, 0); + break; + case 3: + pg_atomic_write_u64(&entry.reservation_token, UINT64_MAX); + break; + case 4: + pg_atomic_write_u64(&entry.generation, UINT64_MAX); + break; + case 5: + pg_atomic_write_u64(&entry.writer_activation_token, 48); + break; + case 6: + pg_atomic_write_u64(&entry.resource_x_activation_generation, 1); + break; + case 7: + buf.buffer_type = BUF_TYPE_PI; + break; + case 8: + pg_atomic_fetch_or_u32(&buf.state, BM_IO_ERROR); + break; + case 9: + pg_atomic_fetch_and_u32(&buf.state, ~BM_VALID); + break; + case 12: + case 13: + case 14: + case 15: + pg_atomic_write_u32(&entry.flags, 0); + pg_atomic_fetch_or_u32(&buf.state, leg == 12 ? BM_IO_IN_PROGRESS + : leg == 13 ? BM_DIRTY + : leg == 14 ? BM_JUST_DIRTIED + : BM_CHECKPOINT_NEEDED); + break; + default: + break; + } + UT_ASSERT_EQ(cluster_bufmgr_pcm_own_snapshot(&buf, &before), CLUSTER_PCM_OWN_OK); + if (leg == 10) + before.tag.blockNum++; + if (leg == 11) + before.generation++; + UT_ASSERT_EQ(cluster_bufmgr_pcm_own_s_holder_candidate_exact(&buf, &before), + leg >= 12 ? CLUSTER_PCM_OWN_BUSY + : leg >= 10 ? CLUSTER_PCM_OWN_STALE + : CLUSTER_PCM_OWN_CORRUPT); + UT_ASSERT_EQ(cluster_bufmgr_pcm_own_snapshot(&buf, &after), CLUSTER_PCM_OWN_OK); + UT_ASSERT_EQ(after.flags, pg_atomic_read_u32(&entry.flags)); + UT_ASSERT_EQ(after.reservation_token, pg_atomic_read_u64(&entry.reservation_token)); + UT_ASSERT((pg_atomic_read_u32(&buf.state) & BM_LOCKED) == 0); + } + ClusterPcmOwnArray = saved; +} + UT_TEST(test_remote_s_holder_stable_n_replay_requires_exact_idle_tuple) { ClusterPcmOwnSnapshot snapshot; @@ -7230,7 +7340,7 @@ UT_TEST(test_resource_x_target_writer_context_is_post_t3_and_local_cleanup_only) int main(void) { - UT_PLAN(127); + UT_PLAN(129); UT_RUN(test_aux_creation_disposition_uses_real_beb_and_excludes_retained_context); UT_RUN(test_real_barrier_refusal_ignores_another_callers_pending); UT_RUN(test_real_barrier_refusal_abort_then_successor_is_not_own_residue); @@ -7286,6 +7396,8 @@ main(void) UT_RUN(test_begin_abort_is_exact_and_monotonic); UT_RUN(test_invalid_live_flag_shapes_are_corrupt_not_busy); UT_RUN(test_remote_s_holder_pending_grant_is_retryable_busy); + UT_RUN(test_real_remote_s_candidate_waits_for_exact_local_reservation); + UT_RUN(test_real_remote_s_candidate_preserves_corruption_and_observation_guards); UT_RUN(test_remote_s_holder_stable_n_replay_requires_exact_idle_tuple); UT_RUN(test_grant_commit_is_exact_and_bumps_once); UT_RUN(test_s_revoke_handoff_reuses_exact_token_and_bumps_once); diff --git a/src/tools/check_r11_source_removal_census.py b/src/tools/check_r11_source_removal_census.py index 821a35cbfb..a40507b1d3 100644 --- a/src/tools/check_r11_source_removal_census.py +++ b/src/tools/check_r11_source_removal_census.py @@ -24,7 +24,7 @@ CURRENT_PRODUCT_SNAPSHOT = { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "e18d3eef8e166762085787e0949d1891964a4297ae4048a6c1a0ebcc28b985aa", + "sha256": "c3f2ab07f937e7b71c8c4791d3ff1a04f99f9eacc6a989b67c65e2a57fccee4d", } LAYERS = { From 45e8111e2c5a6a450664fe8cb22e4e5da9e92411 Mon Sep 17 00:00:00 2001 From: SqlRush Date: Fri, 18 Sep 2026 22:06:31 +0800 Subject: [PATCH 08/10] fix(cluster): retain read ownership across outbound backpressure --- src/backend/cluster/cluster_gcs_block.c | 22 +++- .../data/r11-source-removal-census-v1.json | 2 +- .../test_cluster_r4_route_policy.c | 114 +++++++++++++++--- src/tools/check_r11_source_removal_census.py | 2 +- 4 files changed, 120 insertions(+), 20 deletions(-) diff --git a/src/backend/cluster/cluster_gcs_block.c b/src/backend/cluster/cluster_gcs_block.c index 1b17a784c9..ac0bb6c9ae 100644 --- a/src/backend/cluster/cluster_gcs_block.c +++ b/src/backend/cluster/cluster_gcs_block.c @@ -3136,6 +3136,7 @@ cluster_gcs_send_block_request_and_wait(BufferDesc *buf, PcmLockTransition trans bool terminal_denied = false; bool retry_denied = false; bool read_capacity_refused = false; + bool request_admitted = false; bool retransmit_warning_emitted = false; bool suppress_direct_land = false; bool awaiting_holder_refusal_master_cleanup = false; @@ -3247,6 +3248,7 @@ cluster_gcs_send_block_request_and_wait(BufferDesc *buf, PcmLockTransition trans TimestampTz deadline; bool got_reply = false; bool direct_authoritative_denial = false; + bool request_enqueued; /* Apply backoff for retry attempts (not the initial send). */ if (retry_attempt > 0) { @@ -3333,9 +3335,11 @@ cluster_gcs_send_block_request_and_wait(BufferDesc *buf, PcmLockTransition trans else pg_atomic_fetch_add_u64(&ClusterGcsBlock->retransmit_send_count, 1); - if (!cluster_grd_outbound_enqueue_backend_msg(PGRAC_IC_MSG_GCS_BLOCK_REQUEST, - (uint32)current_master, &payload, - sizeof(payload))) { + request_enqueued = cluster_grd_outbound_enqueue_backend_msg( + PGRAC_IC_MSG_GCS_BLOCK_REQUEST, (uint32)current_master, &payload, sizeof(payload)); + if (request_enqueued) + request_admitted = true; + else if (!request_admitted || !xp_is_read || clean_eligible) { BufferDesc *direct_target_buf = NULL; bool direct_prepared = false; @@ -3357,6 +3361,14 @@ cluster_gcs_send_block_request_and_wait(BufferDesc *buf, PcmLockTransition trans } gcs_block_direct_finish_target(direct_target_buf, direct_prepared, false, InvalidXLogRecPtr); + if (xp_is_read && !clean_eligible && !request_admitted) { + /* No frame was published: there is no remote completion to + * acknowledge. The existing bufmgr owner must exact-abort + * its GRANT_PENDING reservation before yielding/rearming. */ + read_capacity_refused = true; + retry_denied = true; + break; + } ereport( ERROR, (errcode(ERRCODE_CONNECTION_FAILURE), @@ -3369,6 +3381,10 @@ cluster_gcs_send_block_request_and_wait(BufferDesc *buf, PcmLockTransition trans cluster_node_id))); } + /* If only a retransmit was refused, the original admitted request + * still owns its reply. Use this attempt's existing response period + * and retry allowance, not a new unbounded staging wait: the wire + * lifetime hint also bounds the master's dedup retention. */ deadline = GetCurrentTimestamp() + ((TimestampTz)cluster_gcs_reply_timeout_ms) * (TimestampTz)1000; diff --git a/src/test/cluster_unit/data/r11-source-removal-census-v1.json b/src/test/cluster_unit/data/r11-source-removal-census-v1.json index fbb2d0371f..a7f4ae94e3 100644 --- a/src/test/cluster_unit/data/r11-source-removal-census-v1.json +++ b/src/test/cluster_unit/data/r11-source-removal-census-v1.json @@ -16,7 +16,7 @@ "current_product_snapshot": { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "c3f2ab07f937e7b71c8c4791d3ff1a04f99f9eacc6a989b67c65e2a57fccee4d" + "sha256": "d0cc88635549228f7ce0c39546edcc33fd11d8e53e657d705cd73ed92c8517a6" }, "gates": { "L1": { 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 f79ab0e404..998f89bf92 100644 --- a/src/test/cluster_unit/test_cluster_r4_route_policy.c +++ b/src/test/cluster_unit/test_cluster_r4_route_policy.c @@ -638,8 +638,12 @@ WaitLatch(Latch *latch, int wake_events, long timeout, uint32 wait_event) UT_ASSERT(!capacity_content_held); UT_ASSERT_EQ(cluster_pcm_own_flags_get(0), 0); UT_ASSERT_EQ(pg_atomic_read_u32(&capacity_buffer.state) & BM_LOCKED, 0); - } else - UT_ASSERT_EQ(timeout, cluster_gcs_block_retransmit_initial_backoff_ms); + } else { + long factor = timeout / cluster_gcs_block_retransmit_initial_backoff_ms; + + UT_ASSERT(factor > 0 && (factor & (factor - 1)) == 0); + UT_ASSERT_EQ(timeout, factor * cluster_gcs_block_retransmit_initial_backoff_ms); + } UT_ASSERT(wait_event != 0); retry_latch_wait_calls++; if (retry_latch_cancel) @@ -939,6 +943,7 @@ typedef struct RequesterSendCapture { bool suppress_reply; int suppress_reply_calls; int refuse_enqueue_calls; + uint32 refuse_enqueue_mask; bool corrupt_reply_checksum; bool corrupt_reply_identity; bool suppress_done_cap; @@ -1391,7 +1396,9 @@ cluster_grd_outbound_enqueue_backend_msg(uint8 msg_type, uint32 dest_node_id, co if (call_index < requester_send.reply_step_count) reply_step = requester_send.reply_steps[call_index]; } - if (call_index < requester_send.refuse_enqueue_calls) + if (call_index < requester_send.refuse_enqueue_calls + || (call_index < 32 + && (requester_send.refuse_enqueue_mask & (UINT32_C(1) << call_index)) != 0)) return false; if (requester_send.suppress_reply || call_index < requester_send.suppress_reply_calls) return true; @@ -4039,6 +4046,71 @@ UT_TEST(test_capacity_retry_excludes_write_clean_forwarded_and_unverified_reply) cluster_node_id = saved_node; } +/* Real ordinary-read consumer: an unpublished request may return to its + * exact reservation owner; a previously admitted one must retain its ID. */ +UT_TEST(test_read_outbound_refusal_preserves_publication_boundary) +{ + int saved_node = cluster_node_id; + int saved_retries = cluster_gcs_block_retransmit_max_retries; + int saved_timeout = cluster_gcs_reply_timeout_ms; + int mode; + + cluster_node_id = UT_REQUESTER_NODE; + cluster_gcs_block_retransmit_max_retries = 2; + cluster_gcs_reply_timeout_ms = 1; + for (mode = 0; mode < 5; mode++) { + BufferDesc buffer, before; + sigjmp_buf jump; + sigjmp_buf *saved_stack = PG_exception_stack; + volatile bool caught = false; + volatile bool granted = false; + bool retry_denied = false; + + route_test_reset_public_target_requester(); + requester_send.legacy_read = true; + requester_send.refuse_enqueue_mask = mode < 3 ? 1U : 2U; + if (mode >= 3) { + requester_send.suppress_reply_calls = 1; + route_test_first_reply_retry(); + requester_send.reply_steps[2] = requester_send.reply_steps[0]; + requester_send.reply_steps[2].status = GCS_BLOCK_REPLY_DENIED_PENDING_X; + requester_send.reply_step_count = 3; + if (mode == 4) + requester_send.refuse_enqueue_mask = 6U; + } + memset(&buffer, 0, sizeof(buffer)); + buffer.tag = route_test_tag(); + memcpy(&before, &buffer, sizeof(before)); + route_ereport_armed = true; + if (sigsetjmp(jump, 1) == 0) { + PG_exception_stack = &jump; + granted = cluster_gcs_send_block_request_and_wait( + &buffer, mode == 1 ? PCM_TRANS_N_TO_X : PCM_TRANS_N_TO_S, UT_MASTER_NODE, mode == 2, + &retry_denied); + } else + caught = true; + PG_exception_stack = saved_stack; + route_ereport_armed = false; + UT_ASSERT_EQ(caught, mode == 1 || mode == 2 || mode == 4); + UT_ASSERT(!granted); + UT_ASSERT_EQ(retry_denied, mode == 0 || mode == 3); + UT_ASSERT_EQ(requester_send.done_calls, mode == 3 ? 1 : 0); + UT_ASSERT_EQ(memcmp(&buffer, &before, sizeof(buffer)), 0); + if (mode >= 3) { + UT_ASSERT_EQ(requester_send.calls, 3); + UT_ASSERT_EQ(requester_send.request_ids[0], requester_send.request_ids[1]); + UT_ASSERT_EQ(requester_send.request_ids[1], requester_send.request_ids[2]); + } + if (mode == 4) + UT_ASSERT_EQ(process_interrupt_calls, 0); + route_test_assert_public_target_slot_is_canonical(); + } + route_test_reset_public_target_requester(); + cluster_gcs_reply_timeout_ms = saved_timeout; + cluster_gcs_block_retransmit_max_retries = saved_retries; + cluster_node_id = saved_node; +} + UT_TEST(test_real_capacity_refusal_to_exact_rearm_cancel_gate_and_successor) { ClusterPcmOwnEntry *saved_own = ClusterPcmOwnArray; @@ -4050,7 +4122,7 @@ UT_TEST(test_real_capacity_refusal_to_exact_rearm_cancel_gate_and_successor) cluster_node_id = UT_REQUESTER_NODE; cluster_gcs_block_retransmit_max_retries = 0; NBuffers = 1; - for (mode = 0; mode < 5; mode++) { + for (mode = 0; mode < 10; mode++) { ClusterPcmOwnEntry entry, before; ClusterPcmOwnSnapshot base; ClusterPcmGrantBeginWaitReason wait_reason; @@ -4063,6 +4135,7 @@ UT_TEST(test_real_capacity_refusal_to_exact_rearm_cancel_gate_and_successor) route_test_reset_public_target_requester(); requester_send.legacy_read = true; + requester_send.refuse_enqueue_calls = mode >= 5 ? 1 : 0; route_test_first_reply_retry(); requester_send.reply_steps[0].status = GCS_BLOCK_REPLY_DENIED_DEDUP_FULL; requester_send.reply_steps[1] = requester_send.reply_steps[0]; @@ -4084,12 +4157,22 @@ UT_TEST(test_real_capacity_refusal_to_exact_rearm_cancel_gate_and_successor) UT_ASSERT_EQ(token, 8); UT_ASSERT(!covered); memcpy(&before, &entry, sizeof(before)); - UT_ASSERT(!cluster_gcs_send_block_request_and_wait(&capacity_buffer, PCM_TRANS_N_TO_S, - UT_MASTER_NODE, false, &retry_denied)); + route_ereport_armed = true; + if (sigsetjmp(jump, 1) == 0) { + PG_exception_stack = &jump; + UT_ASSERT(!cluster_gcs_send_block_request_and_wait( + &capacity_buffer, PCM_TRANS_N_TO_S, UT_MASTER_NODE, false, &retry_denied)); + } else + caught = true; + PG_exception_stack = saved_stack; + route_ereport_armed = false; + UT_ASSERT(!caught); + if (caught) + continue; UT_ASSERT(retry_denied); UT_ASSERT_EQ(memcmp(&entry, &before, sizeof(entry)), 0); route_test_assert_public_target_slot_is_canonical(); - if (mode == 3) { + if (mode % 5 == 3) { pg_atomic_write_u64(&entry.generation, 6); pg_atomic_write_u64(&entry.reservation_token, 9); pg_atomic_write_u32(&entry.flags, 0); @@ -4097,9 +4180,9 @@ UT_TEST(test_real_capacity_refusal_to_exact_rearm_cancel_gate_and_successor) capacity_buffer.buffer_type = BUF_TYPE_SCUR; memcpy(&before, &entry, sizeof(before)); } - capacity_gate_open = mode != 1; - retry_latch_cancel = mode == 2; - capacity_content_held = mode == 4; + capacity_gate_open = mode % 5 != 1; + retry_latch_cancel = mode % 5 == 2; + capacity_content_held = mode % 5 == 4; capacity_wait_active = true; route_ereport_armed = true; if (sigsetjmp(jump, 1) == 0) { @@ -4111,9 +4194,9 @@ UT_TEST(test_real_capacity_refusal_to_exact_rearm_cancel_gate_and_successor) PG_exception_stack = saved_stack; route_ereport_armed = false; capacity_wait_active = false; - UT_ASSERT_EQ(caught, mode == 2 || mode == 4); - UT_ASSERT_EQ(retry_latch_wait_calls, mode == 0 || mode == 2 ? 1 : 0); - if (mode == 0) { + UT_ASSERT_EQ(caught, mode % 5 == 2 || mode % 5 == 4); + UT_ASSERT_EQ(retry_latch_wait_calls, mode % 5 == 0 || mode % 5 == 2 ? 1 : 0); + if (mode % 5 == 0) { UT_ASSERT_EQ(result, CLUSTER_BUFMGR_PCM_RETRY_REARMED); UT_ASSERT_EQ(token, 9); UT_ASSERT_EQ(cluster_pcm_own_flags_get(0), PCM_OWN_FLAG_GRANT_PENDING); @@ -4126,10 +4209,10 @@ UT_TEST(test_real_capacity_refusal_to_exact_rearm_cancel_gate_and_successor) UT_ASSERT_EQ(requester_send.request_ids[1], UINT64_C(0x0200000000000003)); UT_ASSERT_EQ(cluster_pcm_own_abort_grant_reservation(&capacity_buffer, &base, token), CLUSTER_PCM_OWN_OK); - } else if (mode == 1) { + } else if (mode % 5 == 1) { UT_ASSERT_EQ(result, CLUSTER_BUFMGR_PCM_RETRY_BARRIER_REFUSED); UT_ASSERT(barrier); - } else if (mode == 3) { + } else if (mode % 5 == 3) { UT_ASSERT_EQ(result, CLUSTER_BUFMGR_PCM_RETRY_COVERED); UT_ASSERT_EQ(covered_generation, 6); UT_ASSERT_EQ(memcmp(&entry, &before, sizeof(entry)), 0); @@ -6247,6 +6330,7 @@ main(void) UT_RUN(test_r4_done_missing_capability_or_queue_space_keeps_terminal_result); UT_RUN(test_direct_s_capacity_refusal_returns_owned_retry_without_done); UT_RUN(test_capacity_retry_excludes_write_clean_forwarded_and_unverified_reply); + UT_RUN(test_read_outbound_refusal_preserves_publication_boundary); UT_RUN(test_real_capacity_refusal_to_exact_rearm_cancel_gate_and_successor); UT_RUN(test_target_wrapper_status25_beyond_old_limit_waits_for_full); UT_RUN(test_target_wrapper_lost_reply_and_backpressure_redrive_same_id); diff --git a/src/tools/check_r11_source_removal_census.py b/src/tools/check_r11_source_removal_census.py index a40507b1d3..a33ad1140a 100644 --- a/src/tools/check_r11_source_removal_census.py +++ b/src/tools/check_r11_source_removal_census.py @@ -24,7 +24,7 @@ CURRENT_PRODUCT_SNAPSHOT = { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "c3f2ab07f937e7b71c8c4791d3ff1a04f99f9eacc6a989b67c65e2a57fccee4d", + "sha256": "d0cc88635549228f7ce0c39546edcc33fd11d8e53e657d705cd73ed92c8517a6", } LAYERS = { From 24e752cc0f619f56f2022e219848a437f0c47789 Mon Sep 17 00:00:00 2001 From: SqlRush Date: Fri, 18 Sep 2026 22:16:31 +0800 Subject: [PATCH 09/10] fix(buffer): unwind quiescent shared read reservations on error --- src/backend/storage/buffer/bufmgr.c | 57 +++++++- src/test/cluster_unit/Makefile | 3 +- .../data/r11-source-removal-census-v1.json | 2 +- .../test_cluster_pcm_direct_init.c | 18 ++- .../test_cluster_r4_route_policy.c | 122 ++++++++++++++++++ src/tools/check_r11_source_removal_census.py | 2 +- 6 files changed, 196 insertions(+), 8 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index e4991c138d..24f18b4af4 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -3021,6 +3021,58 @@ cluster_bufmgr_pcm_retry_denied_rearm(BufferDesc *buf, PcmLockMode pcm_mode, return CLUSTER_BUFMGR_PCM_RETRY_REARMED; } +/* An error before local S publication must not strand an ordinary N + * reservation. This catch runs BEFORE AbortBufferIO/ResourceOwner cleanup: + * a cleared IO bit after generic cleanup would not prove DMA quiescence. + * Never release a remote grant speculatively, or touch a successor/retained + * authority. The lower GCS error owner has already retired the TCP reply + * slot, so a late generic reply cannot install after this exact abort. */ +static bool +cluster_bufmgr_pcm_acquire_shared_owned(BufferDesc *buf, + const ClusterPcmOwnSnapshot *base, + uint64 reservation_token, + bool *retry_denied) +{ + volatile bool acquired = false; + + PG_TRY(); + { + acquired = cluster_pcm_lock_acquire_buffer(buf, PCM_LOCK_MODE_S, retry_denied); + } + PG_CATCH(); + { + ClusterPcmOwnResult result = CLUSTER_PCM_OWN_STALE; + uint32 state; + + state = LockBufHdr(buf); + if (ClusterPcmOwnArray != NULL && base->pcm_state == (uint8) PCM_STATE_N + && base->flags == 0 && base->reservation_token != UINT64_MAX + && reservation_token == base->reservation_token + 1 + && BufferTagsEqual(&buf->tag, &base->tag) + && (state & (BM_TAG_VALID | BM_IO_IN_PROGRESS)) == BM_TAG_VALID + && buf->pcm_state == (uint8) PCM_STATE_N + && buf->buffer_type != (uint8) BUF_TYPE_PI + && cluster_pcm_own_gen_get(buf->buf_id) == base->generation + && cluster_pcm_own_reservation_token_get(buf->buf_id) == reservation_token + && cluster_pcm_own_flags_get(buf->buf_id) == PCM_OWN_FLAG_GRANT_PENDING + && cluster_pcm_own_writer_activation_token_get(buf->buf_id) == 0 + && cluster_pcm_own_resource_x_activation_generation_get(buf->buf_id) == 0 + && cluster_pcm_own_delivery_attempt_get(buf->buf_id) == 0) + result = cluster_pcm_own_reservation_abort_exact( + buf->buf_id, base->generation, reservation_token, PCM_OWN_FLAG_GRANT_PENDING); + UnlockBufHdr(buf, state); + if (result != CLUSTER_PCM_OWN_OK) + elog(LOG, + "cluster shared acquisition error cleanup not proven: buffer=%d " + "generation=%llu token=%llu result=%d; exact reservation left unchanged", + buf->buf_id, (unsigned long long) base->generation, + (unsigned long long) reservation_token, (int) result); + PG_RE_THROW(); + } + PG_END_TRY(); + return acquired; +} + static ClusterPcmXWriterLedgerEntry * cluster_bufmgr_pcm_x_writer_find(BufferDesc *buf) { @@ -10820,8 +10872,9 @@ LockBufferInternal(Buffer buffer, int mode, bool *pcm_barrier_refused, bool retry_denied = false; pcm_pending_set = true; - pcm_acquired = cluster_pcm_lock_acquire_buffer( - buf, PCM_LOCK_MODE_S, &retry_denied); + pcm_acquired = cluster_bufmgr_pcm_acquire_shared_owned( + buf, &pcm_pending_base, pcm_pending_token, + &retry_denied); if (!retry_denied) break; pcm_pending_set = false; diff --git a/src/test/cluster_unit/Makefile b/src/test/cluster_unit/Makefile index a9d2d4be5b..eb17706cc2 100644 --- a/src/test/cluster_unit/Makefile +++ b/src/test/cluster_unit/Makefile @@ -3278,9 +3278,10 @@ test_cluster_gcs_read_rearm_owner.inc: $(top_srcdir)/src/backend/storage/buffer/ /^cluster_bufmgr_pcm_begin_grant_reservation_wait\(/ { print "static ClusterPcmOwnResult"; emit=1; begin_wait++ } \ /^typedef enum ClusterBufmgrPcmRetryRearmResult/ { emit=1; result_type++ } \ /^cluster_bufmgr_pcm_retry_denied_rearm\(/ { print "static ClusterBufmgrPcmRetryRearmResult"; emit=1; rearm++ } \ + /^cluster_bufmgr_pcm_acquire_shared_owned\(/ { print "static bool"; emit=1; shared_owner++ } \ /^LockBufHdr\(/ { print "uint32"; emit=1; header_lock++ } \ emit { print } /^}/ { emit=0 } \ - END { if (begin != 1 || cancel != 1 || observe != 1 || wait_owner != 1 || begin_wait != 1 || result_type != 1 || rearm != 1 || header_lock != 1 || emit) exit 1 }' $< > $@.tmp + END { if (begin != 1 || cancel != 1 || observe != 1 || wait_owner != 1 || begin_wait != 1 || result_type != 1 || rearm != 1 || shared_owner != 1 || header_lock != 1 || emit) exit 1 }' $< > $@.tmp mv $@.tmp $@ test_cluster_r4_route_policy: test_cluster_r4_route_policy.c unit_test.h \ diff --git a/src/test/cluster_unit/data/r11-source-removal-census-v1.json b/src/test/cluster_unit/data/r11-source-removal-census-v1.json index a7f4ae94e3..32fa227e22 100644 --- a/src/test/cluster_unit/data/r11-source-removal-census-v1.json +++ b/src/test/cluster_unit/data/r11-source-removal-census-v1.json @@ -16,7 +16,7 @@ "current_product_snapshot": { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "d0cc88635549228f7ce0c39546edcc33fd11d8e53e657d705cd73ed92c8517a6" + "sha256": "f8e76f5b8d0e6c2daa2a1fcc2a756d59d0cf7a39369be2ec7e26002f74e2c274" }, "gates": { "L1": { diff --git a/src/test/cluster_unit/test_cluster_pcm_direct_init.c b/src/test/cluster_unit/test_cluster_pcm_direct_init.c index f65756102c..c3ab63cc8e 100644 --- a/src/test/cluster_unit/test_cluster_pcm_direct_init.c +++ b/src/test/cluster_unit/test_cluster_pcm_direct_init.c @@ -466,7 +466,8 @@ UT_TEST(test_bufmgr_consumes_proof_before_reservation_and_wire) char *source = read_source(BUFMGR_SOURCE_PATH); static const char *const order[] = { "cluster_bufmgr_pcm_gate_direct_init(", "cluster_pcm_direct_init_proof_consume", - "cluster_pcm_own_reservation_begin_exact", "cluster_pcm_lock_acquire_buffer" }; + "cluster_pcm_own_reservation_begin_exact", + "cluster_gcs_resource_x_target_direct_init_acquire_exact(" }; UT_ASSERT(source != NULL); if (source != NULL) { @@ -618,12 +619,23 @@ UT_TEST(test_valid_n_s_x_without_proof_uses_target_or_s_reservation) "cluster_bufmgr_pcm_x_writer_prepare_target(", "else", "cluster_bufmgr_pcm_begin_grant_reservation_wait(", - "cluster_pcm_lock_acquire_buffer(", - "buf, PCM_LOCK_MODE_S, &retry_denied" }; + "cluster_bufmgr_pcm_acquire_shared_owned(", + "buf, &pcm_pending_base, pcm_pending_token" }; + static const char *const owner[] + = { "cluster_bufmgr_pcm_acquire_shared_owned(", + "PG_TRY();", + "cluster_pcm_lock_acquire_buffer(buf, PCM_LOCK_MODE_S, retry_denied)", + "PG_CATCH();", + "LockBufHdr(buf)", + "BM_IO_IN_PROGRESS", + "cluster_pcm_own_reservation_abort_exact(", + "UnlockBufHdr(buf, state)", + "PG_RE_THROW();" }; UT_ASSERT(source != NULL); if (source != NULL) { assert_ordered(source, order, lengthof(order)); + assert_ordered(source, owner, lengthof(owner)); free(source); } } 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 998f89bf92..5e28134393 100644 --- a/src/test/cluster_unit/test_cluster_r4_route_policy.c +++ b/src/test/cluster_unit/test_cluster_r4_route_policy.c @@ -967,6 +967,19 @@ typedef struct RequesterSendCapture { } RequesterSendCapture; static RequesterSendCapture requester_send; +static bool capacity_acquire_success; + +bool +cluster_pcm_lock_acquire_buffer(BufferDesc *buf, PcmLockMode mode, bool *retry_denied) +{ + UT_ASSERT_EQ(mode, PCM_LOCK_MODE_S); + if (capacity_acquire_success) { + *retry_denied = false; + return true; + } + return cluster_gcs_send_block_request_and_wait(buf, PCM_TRANS_N_TO_S, UT_MASTER_NODE, false, + retry_denied); +} /* The real bufmgr rearm and header snapshots below are generated verbatim. * Only unrelated admission/lock runtime is external: no competing X head, @@ -4111,6 +4124,114 @@ UT_TEST(test_read_outbound_refusal_preserves_publication_boundary) cluster_node_id = saved_node; } +UT_TEST(test_shared_acquire_error_retires_only_its_quiescent_n_reservation) +{ + ClusterPcmOwnEntry *saved_own = ClusterPcmOwnArray; + int saved_buffers = NBuffers; + int saved_node = cluster_node_id; + int saved_retries = cluster_gcs_block_retransmit_max_retries; + int saved_timeout = cluster_gcs_reply_timeout_ms; + int mode; + + cluster_node_id = UT_REQUESTER_NODE; + cluster_gcs_block_retransmit_max_retries = 0; + cluster_gcs_reply_timeout_ms = 1; + NBuffers = 1; + for (mode = 0; mode < 15; mode++) { + ClusterPcmOwnEntry entry, before; + ClusterPcmOwnSnapshot base; + ClusterPcmGrantBeginWaitReason wait_reason; + BufferDesc buffer_before; + uint64 token = 0; + bool covered = false; + bool retry_denied = false; + volatile bool caught = false; + volatile bool granted = false; + sigjmp_buf jump; + sigjmp_buf *saved_stack = PG_exception_stack; + + route_test_reset_public_target_requester(); + requester_send.legacy_read = true; + requester_send.suppress_reply = true; + cluster_gcs_reply_timeout_ms = mode == 14 ? 10 : 1; + reply_cv_timed_sleep_raise = mode == 14; + capacity_acquire_success = mode == 12; + if (mode == 13) + requester_send.refuse_enqueue_calls = 1; + memset(&entry, 0, sizeof(entry)); + pg_atomic_init_u64(&entry.generation, 5); + pg_atomic_init_u64(&entry.reservation_token, 7); + ClusterPcmOwnArray = &entry; + memset(&capacity_buffer, 0, sizeof(capacity_buffer)); + capacity_buffer.tag = route_test_tag(); + capacity_buffer.pcm_state = PCM_STATE_N; + capacity_buffer.buffer_type = BUF_TYPE_CURRENT; + pg_atomic_init_u32(&capacity_buffer.state, BM_VALID | BM_TAG_VALID); + UT_ASSERT_EQ(cluster_pcm_own_begin_grant_reservation(&capacity_buffer, PCM_LOCK_MODE_S, + &base, &token, &covered, &wait_reason), + CLUSTER_PCM_OWN_OK); + UT_ASSERT(!covered); + if (mode == 1) + capacity_buffer.tag.blockNum++; + if (mode == 2) + pg_atomic_write_u64(&entry.generation, 6); + if (mode == 3) + pg_atomic_write_u64(&entry.reservation_token, 9); + if (mode == 4) + pg_atomic_write_u32(&entry.flags, PCM_OWN_FLAG_REVOKING); + if (mode == 5) + capacity_buffer.pcm_state = base.pcm_state = PCM_STATE_S; + if (mode == 6) + capacity_buffer.pcm_state = PCM_STATE_X; + if (mode == 7) + pg_atomic_fetch_or_u32(&capacity_buffer.state, BM_IO_IN_PROGRESS); + if (mode == 8) + pg_atomic_write_u64(&entry.writer_activation_token, 8); + if (mode == 9) + pg_atomic_write_u64(&entry.resource_x_activation_generation, 12); + if (mode == 10) + pg_atomic_write_u64(&entry.delivery_attempt, 3); + if (mode == 11) + capacity_buffer.buffer_type = BUF_TYPE_PI; + memcpy(&before, &entry, sizeof(before)); + memcpy(&buffer_before, &capacity_buffer, sizeof(buffer_before)); + route_ereport_armed = true; + if (sigsetjmp(jump, 1) == 0) { + PG_exception_stack = &jump; + granted = cluster_bufmgr_pcm_acquire_shared_owned(&capacity_buffer, &base, token, + &retry_denied); + } else + caught = true; + PG_exception_stack = saved_stack; + route_ereport_armed = false; + UT_ASSERT_EQ(caught, mode < 12 || mode == 14); + UT_ASSERT_EQ(granted, mode == 12); + UT_ASSERT_EQ(retry_denied, mode == 13); + UT_ASSERT_EQ(requester_send.done_calls, 0); + UT_ASSERT_EQ(memcmp(&capacity_buffer, &buffer_before, sizeof(buffer_before)), 0); + if (mode == 0 || mode == 14) { + uint64 next_token = 0; + ClusterPcmOwnSnapshot next_base; + + UT_ASSERT_EQ(cluster_pcm_own_flags_get(0), 0); + UT_ASSERT_EQ(cluster_pcm_own_begin_grant_reservation(&capacity_buffer, PCM_LOCK_MODE_S, + &next_base, &next_token, &covered, + &wait_reason), + CLUSTER_PCM_OWN_OK); + UT_ASSERT_EQ(next_token, token + 1); + } else + UT_ASSERT_EQ(memcmp(&entry, &before, sizeof(before)), 0); + route_test_assert_public_target_slot_is_canonical(); + } + capacity_acquire_success = false; + route_test_reset_public_target_requester(); + ClusterPcmOwnArray = saved_own; + NBuffers = saved_buffers; + cluster_node_id = saved_node; + cluster_gcs_block_retransmit_max_retries = saved_retries; + cluster_gcs_reply_timeout_ms = saved_timeout; +} + UT_TEST(test_real_capacity_refusal_to_exact_rearm_cancel_gate_and_successor) { ClusterPcmOwnEntry *saved_own = ClusterPcmOwnArray; @@ -6331,6 +6452,7 @@ main(void) UT_RUN(test_direct_s_capacity_refusal_returns_owned_retry_without_done); UT_RUN(test_capacity_retry_excludes_write_clean_forwarded_and_unverified_reply); UT_RUN(test_read_outbound_refusal_preserves_publication_boundary); + UT_RUN(test_shared_acquire_error_retires_only_its_quiescent_n_reservation); UT_RUN(test_real_capacity_refusal_to_exact_rearm_cancel_gate_and_successor); UT_RUN(test_target_wrapper_status25_beyond_old_limit_waits_for_full); UT_RUN(test_target_wrapper_lost_reply_and_backpressure_redrive_same_id); diff --git a/src/tools/check_r11_source_removal_census.py b/src/tools/check_r11_source_removal_census.py index a33ad1140a..e6dd076631 100644 --- a/src/tools/check_r11_source_removal_census.py +++ b/src/tools/check_r11_source_removal_census.py @@ -24,7 +24,7 @@ CURRENT_PRODUCT_SNAPSHOT = { "algorithm": "sha256-canonical-path-blob-v1", "path_count": 2225, - "sha256": "d0cc88635549228f7ce0c39546edcc33fd11d8e53e657d705cd73ed92c8517a6", + "sha256": "f8e76f5b8d0e6c2daa2a1fcc2a756d59d0cf7a39369be2ec7e26002f74e2c274", } LAYERS = { From 30a86aece4ada5f4ca60d3a8dc794da64b1d7a43 Mon Sep 17 00:00:00 2001 From: SqlRush Date: Fri, 18 Sep 2026 23:06:27 +0800 Subject: [PATCH 10/10] chore(release): prepare MVP correctness maintenance v0.130.1 --- PGRAC_VERSION | 2 +- README.md | 12 +- docs/mvp/v0.130.1/README.md | 32 ++++ .../v0.130.1/quickstart-linux-single-host.md | 147 ++++++++++++++++++ docs/release-notes/README.md | 14 +- docs/release-notes/v0.130.1.md | 93 +++++++++++ 6 files changed, 287 insertions(+), 13 deletions(-) create mode 100644 docs/mvp/v0.130.1/README.md create mode 100644 docs/mvp/v0.130.1/quickstart-linux-single-host.md create mode 100644 docs/release-notes/v0.130.1.md diff --git a/PGRAC_VERSION b/PGRAC_VERSION index 156fe0ca7d..ecc59810e8 100644 --- a/PGRAC_VERSION +++ b/PGRAC_VERSION @@ -1 +1 @@ -0.130.0 +0.130.1 diff --git a/README.md b/README.md index f3f9e35f86..1c12c1b9c1 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ PostgreSQL has never had a shared-disk, multi-active cluster (its HA is shared-nothing replication). pgrac brings the Oracle RAC model — many nodes, one shared database, Cache Fusion / SCN / GES — to PostgreSQL 16.13. -> **First stable MVP: [v0.130.0](docs/release-notes/v0.130.0.md).** +> **Current stable MVP: [v0.130.1](docs/release-notes/v0.130.1.md).** > -> The CI-qualified MVP now has four valid four-node point-update samples with +> This correctness maintenance release has four valid four-node point-update samples with > 32 clients per node, complete million-row data comparisons, health and > outstanding-work checks, and normal shutdown. It also retains the earlier > 8/16-client, soak, block-transfer and same-data normal-restart acceptance. @@ -63,8 +63,8 @@ More diagrams and deep-dives at **[pgrac.dev](https://pgrac.dev)**. ## Documentation -Start with the [stable MVP guide](docs/mvp/v0.130.0/README.md) and -[single-host Linux Quick Start](docs/mvp/v0.130.0/quickstart-linux-single-host.md). +Start with the [stable MVP guide](docs/mvp/v0.130.1/README.md) and +[single-host Linux Quick Start](docs/mvp/v0.130.1/quickstart-linux-single-host.md). The guide links the parameter, system-view and capability references, and distinguishes tested single-host operation from unqualified multi-host/failover deployment. The historical prerelease manual remains available unchanged. @@ -87,11 +87,11 @@ from the upstream tree. ## Quick start For the stable MVP, follow the -[single-host Linux guide](docs/mvp/v0.130.0/quickstart-linux-single-host.md). +[single-host Linux guide](docs/mvp/v0.130.1/quickstart-linux-single-host.md). This is a source release, not a turnkey production installer. ```bash -git clone --branch v0.130.0 --single-branch \ +git clone --branch v0.130.1 --single-branch \ https://github.com/sqlrush/pgrac.git pgrac-mvp1 cd pgrac-mvp1 git rev-parse HEAD diff --git a/docs/mvp/v0.130.1/README.md b/docs/mvp/v0.130.1/README.md new file mode 100644 index 0000000000..e51f5d82f1 --- /dev/null +++ b/docs/mvp/v0.130.1/README.md @@ -0,0 +1,32 @@ +# PGRAC MVP 维护稳定版使用入口 + +Author: SqlRush + +版本:`v0.130.1`;PostgreSQL 基线:16.13;发布日期:2026-09-18。 + +本版修复既有 MVP 范围内的事务等待、一致读及缓冲区清理缺陷;不是性能优化版。 +本候选重新通过四实例正确性 PRE、原数据正常启动及全体正常关闭,发布须通过精确提交的完整 MVP CI。 +不代表生产 HA、崩溃恢复、四台独立主机共享 LUN 或性能目标已认证。 +完整结果与限制见[发布说明](../../release-notes/v0.130.1.md)。 + +## 从这里开始 + +按[单机四实例 Quick Start(Linux)](quickstart-linux-single-host.md)拉取标签、编译、初始化及连接。 +使用本机文件系统上的四个独立 PGDATA,共享业务数据;不是四台主机,不需要容器或 GFS2。 +安装步骤沿用已演练示例,本轮没有重新运行 Quick Start 演练。 + +## 参考手册 + +详细接口参考保留原发布目录;旧版固定提交及验收声明只属于旧版,获取命令和本版范围以本目录为准。 + +| 内容 | 文档 | +|---|---| +| 共享存储准备、检查与四机边界 | [存储准备](../v0.130.0-mvp.1/storage-preparation.md)、[部署参考](../v0.130.0-mvp.1/01-linux-four-node-deployment.md) | +| 参数、默认值与配置组合 | [参数手册](../v0.130.0-mvp.1/02-parameters.md) | +| 系统视图与字段 | [系统视图](../v0.130.0-mvp.1/03-system-views.md) | +| 核心能力 | [功能与运行机制](../v0.130.0-mvp.1/04-core-capabilities.md) | +| 当前源码安装选项 | [安装指南](../../user-guide/install.md) | + +发布为源码版本,不提供生产认证二进制。保留标签、完整 commit、编译参数、配置与二进制 SHA-256; +不要仅用旧的 `pgrac_version()` 字符串识别版本。原稳定标签 `v0.130.0` 不变。 +使用独立安装目录评估新版本;本版没有认证滚动/混版本升级、通用数据迁移或降级。 diff --git a/docs/mvp/v0.130.1/quickstart-linux-single-host.md b/docs/mvp/v0.130.1/quickstart-linux-single-host.md new file mode 100644 index 0000000000..7844567620 --- /dev/null +++ b/docs/mvp/v0.130.1/quickstart-linux-single-host.md @@ -0,0 +1,147 @@ +# 单机四实例 Quick Start(Linux,MVP 稳定版) + +Author: SqlRush + +目标:一台 Linux 主机、四个 PGRAC 实例、同一份共享业务数据。版本固定为 `v0.130.1`。这是隔离环境中的源码安装示例,不是四机共享 LUN 或生产 HA 安装器。 + +沿用已演练的 Rocky Linux 9 / Btrfs 单机步骤与初始化示例;本次仅更新维护版标签及文档链接,不声称重新运行了 Quick Start 演练。稳定版 CI 与 PRE 范围见[发布说明](../../release-notes/v0.130.1.md)。 + +## 1. 准备主机 + +使用有 `sudo` 权限的普通账号,预留充足内存和至少 20 GiB 磁盘空间。在同一个 Bash 终端执行,不要用 root 运行数据库。 + +```bash +bash +set -euo pipefail +test "$(id -u)" -ne 0 +umask 077 + +sudo dnf install -y dnf-plugins-core +sudo dnf config-manager --set-enabled crb +sudo dnf install -y gcc make git pkgconf-pkg-config bison flex \ + perl perl-IPC-Run perl-Test-Simple perl-Time-HiRes \ + readline-devel zlib-devel libicu-devel lz4-devel libzstd-devel \ + util-linux procps-ng tar kmod +test -c /dev/loop-control || sudo modprobe loop +sudo losetup --find + +export PGRAC_QS_ROOT="$(mktemp -d /var/tmp/pgrac-quickstart.XXXXXX)" +findmnt -T "$PGRAC_QS_ROOT" +df -h "$PGRAC_QS_ROOT" +printf '本次安装目录:%s\n' "$PGRAC_QS_ROOT" +``` + +目录必须位于本机磁盘,不能使用 NFS 或主机共享映射目录。本机 ext4/XFS 不涉及跨主机挂载,但既有演练使用 Btrfs。示例仅创建自己的三个投票文件及对应 loop 设备,不格式化现有盘。 + +## 2. 拉取稳定版源码 + +```bash +git clone --depth 1 --branch v0.130.1 --single-branch \ + https://github.com/sqlrush/pgrac.git "$PGRAC_QS_ROOT/source" +git -C "$PGRAC_QS_ROOT/source" describe --exact-match --tags +git -C "$PGRAC_QS_ROOT/source" rev-parse HEAD +test "$(cat "$PGRAC_QS_ROOT/source/PGRAC_VERSION")" = 0.130.1 +``` + +## 3. 编译安装 + +示例继续使用不启用 OpenSSL 的本机构建;SQL 只使用 Unix socket,不开放外部 SQL 端口。这不是稳定版的 OpenSSL 构建限制。 + +```bash +mkdir "$PGRAC_QS_ROOT/build" +cd "$PGRAC_QS_ROOT/build" +../source/configure --prefix="$PGRAC_QS_ROOT/install" \ + --enable-cluster --enable-cassert --enable-debug --enable-tap-tests \ + --with-icu --with-lz4 --with-zstd +make -j4 +make install +make -C src/test/cluster_tap all +make -C src/test/regress pg_regress + +export PATH="$PGRAC_QS_ROOT/install/bin:$PATH" +pg_config --configure +``` + +## 4. 准备初始化示例 + +`run-quad.pl` 来自刚拉取的稳定标签中的文档示例,不是额外下载的未知脚本。目录名保留其首次发布版本;示例字节未修改。 + +```bash +cp "$PGRAC_QS_ROOT/source/docs/mvp/v0.130.0-mvp.1/quickstart-single-host.pl" \ + "$PGRAC_QS_ROOT/run-quad.pl" +printf '%s %s\n' \ + 0e5fe8670349c33485474f4757744507c9939a4149d2adef06a23f538ae9f4c9 \ + "$PGRAC_QS_ROOT/run-quad.pl" | sha256sum -c - + +mkdir "$PGRAC_QS_ROOT/data" "$PGRAC_QS_ROOT/log" +cat > "$PGRAC_QS_ROOT/seed.conf" <<'CONF' +fsync = on +full_page_writes = on +synchronous_commit = on +CONF + +export LC_ALL=C +export PERL5LIB="$PGRAC_QS_ROOT/source/src/test/perl" +export PG_REGRESS="$PGRAC_QS_ROOT/build/src/test/regress/pg_regress" +export PGRAC_DIRECT_IO_PROBE="$PGRAC_QS_ROOT/build/src/test/cluster_tap/pgrac_direct_io_probe" +export top_builddir="$PGRAC_QS_ROOT/build" +export TEMP_CONFIG="$PGRAC_QS_ROOT/seed.conf" +export TESTDATADIR="$PGRAC_QS_ROOT/data" +export TESTLOGDIR="$PGRAC_QS_ROOT/log" +export PG_TEST_NOCLEAN=1 PG_TEST_TIMEOUT_DEFAULT=180 +export PGRAC_STAGE8_HAPPY_PATH_ONLY=1 +unset PGRAC_TEST_TWO_STAGE_VOTING_LOOP +``` + +## 5. 初始化并启动四实例 + +脚本建立一个数据库身份及四个独立 PGDATA,共享业务数据,自动配置端口、互联与投票设备。`postgres` 数据库中的 `quickstart_demo` 表在 seed 阶段创建后克隆。本示例不启用共享系统目录,运行后不要单独建表、改表或执行 `CREATE DATABASE`。 + +```bash +sudo -v +perl "$PGRAC_QS_ROOT/run-quad.pl" > "$PGRAC_QS_ROOT/launcher.out" 2>&1 & +export PGRAC_QS_PID=$! + +for attempt in $(seq 1 360); do + test ! -f "$PGRAC_QS_ROOT/READY" || break + if ! kill -0 "$PGRAC_QS_PID" 2>/dev/null; then + tail -n 60 "$PGRAC_QS_ROOT/launcher.out" + tail -n 60 "$PGRAC_QS_ROOT/log/regress_log_run-quad" + exit 1 + fi + sleep 1 +done +test -f "$PGRAC_QS_ROOT/READY" +source "$PGRAC_QS_ROOT/connect.env" +``` + +`READY` 表示四实例已依次更新同一行并都读到 `value=4`。连接端口和四份 PGDATA 路径见 `connect.env`。 + +## 6. 验证共享读写 + +```bash +for port in "$PGPORT_0" "$PGPORT_1" "$PGPORT_2" "$PGPORT_3"; do + psql -X -v ON_ERROR_STOP=1 -p "$port" -c 'TABLE quickstart_demo' +done + +psql -X -v ON_ERROR_STOP=1 -p "$PGPORT_3" \ + -c 'UPDATE quickstart_demo SET value=value+10 WHERE id=1 RETURNING *' +psql -X -v ON_ERROR_STOP=1 -p "$PGPORT_0" -c 'TABLE quickstart_demo' +``` + +第一次四次查询均应为 `id=1, value=4`;最后 node0 应读到 `value=14`。 + +## 7. 全体正常关机 + +```bash +sudo -v +touch "$PGRAC_QS_ROOT/STOP" +wait "$PGRAC_QS_PID" +test -f "$PGRAC_QS_ROOT/STOPPED" + +for datadir in "$PGDATA_0" "$PGDATA_1" "$PGDATA_2" "$PGDATA_3"; do + pg_controldata "$datadir" | grep 'Database cluster state' +done +``` + +预期四行均为 `shut down`。脚本完成正常关机验证后仅释放自己创建的 loop 设备,不删除数据。不要强杀、执行 `losetup -D` 或在原目录重复初始化。该脚本只用于新建示例,不是原数据重启工具;失败时保留目录与日志,不能把失败现场当作干净关机数据。 diff --git a/docs/release-notes/README.md b/docs/release-notes/README.md index 373702464b..9f200f6fac 100644 --- a/docs/release-notes/README.md +++ b/docs/release-notes/README.md @@ -4,10 +4,11 @@ Author: SqlRush ## Current release -[v0.130.0 — first stable MVP](v0.130.0.md) is the current release. It combines -the MVP release CI gate with four valid four-node correctness samples on the -CI-fixed kernel. Stable means the documented MVP scope, not production HA, -independent-host shared-storage certification or a performance guarantee. +[v0.130.1 — correctness maintenance release](v0.130.1.md) is the current release. +It repairs transaction-wait, consistent-read and buffer-cleanup defects and has +four new valid four-node correctness samples. Stable means the documented MVP +scope, not production HA, independent-host shared-storage certification or a +performance guarantee. [v0.130.0 — first stable MVP](v0.130.0.md) is unchanged. [v0.130.0-mvp.1](v0.130.0-mvp.1.md) remains the immutable first evaluation snapshot. Its original CI limitation is retained in its historical notes; @@ -34,6 +35,7 @@ Versions use `MAJOR.MINOR.PATCH`, optionally followed by a prerelease label: |---|---| | `v0.130.0-mvp.1` | First frozen MVP baseline | | `v0.130.0` | First stable release within the documented MVP scope | +| `v0.130.1` | Correctness maintenance release within the same MVP scope | | `-mvp.N`, `-alpha.N`, `-beta.N` | Numbered evaluation prereleases | | `-rc.N` | Release candidates with their own published qualification scope | | No suffix | Stable release; only after its acceptance criteria pass | @@ -52,8 +54,8 @@ the superseded release's evidence and limitations. ## Selecting a version ```sh -git fetch origin tag v0.130.0 -git switch --detach v0.130.0 +git fetch origin tag v0.130.1 +git switch --detach v0.130.1 git rev-parse HEAD cat PGRAC_VERSION ``` diff --git a/docs/release-notes/v0.130.1.md b/docs/release-notes/v0.130.1.md new file mode 100644 index 0000000000..8a8a683484 --- /dev/null +++ b/docs/release-notes/v0.130.1.md @@ -0,0 +1,93 @@ +# PGRAC v0.130.1 — MVP correctness maintenance + +Author: SqlRush + +Release date: 2026-09-18. PostgreSQL base: 16.13. Distribution: source. + +This maintenance release follows `v0.130.0` and repairs defects within the same +qualified MVP scope. It is not a performance-optimization release or a broader +production-HA certification. The previous tag and its evidence remain unchanged. + +## Fixes + +- Deliver valid consistent-read refusal replies through the outbound validator, + and account for queue non-admission without reporting it as successful delivery. +- Retire abandoned remote-undo read builds through the existing retry path; + preserve identity checks and reject late replies to reused slots. +- Consume matching deadlock-victim cancellation in exact transaction waits. +- Preserve prepared update receipts across eligible waits and nested update + preparation; prevent an internal temporary-lock/transaction-slot dependency cycle. +- Honor the original lock-wait budget and complete exact cancellation bookkeeping. +- Correct statement visibility for local successor versions of foreign transactions. +- Wait for valid shared-read reservation contention, retry ordinary outbound + backpressure, and release only provably quiescent unpublished reservations on error. +- Reduce ordinary-path log noise while retaining error and refusal diagnostics. + +No disk format, protocol format, benchmark workload or configured timeout changes +are included. Experimental CR scheduling and worker-isolation optimizations are +not included. Malformed, stale or unauthenticated frames remain rejected. + +## Qualification + +The new correctness PRE used kernel commit +[`24e752cc0f619f56f2022e219848a437f0c47789`](https://github.com/sqlrush/pgrac/commit/24e752cc0f619f56f2022e219848a437f0c47789). +Release preparation changes only version metadata and user documentation from +that commit; kernel, build and test-runner sources are unchanged. + +- Four active instances on one Linux host, 32 clients per instance. +- Four valid point `UPDATE` + `COMMIT` samples; 5-second warmup and 35-second + measurement in each sample, with unchanged correctness rules and timeouts. +- Zero unexpected client/server errors and no forced cancellation. Every client + completed naturally. Raw late-completion codes remain in the evidence and are + accepted only by the existing natural-completion correctness policy. +- Complete ordered byte comparisons of all 1,000,000 rows from every node after + each sample; health, invariant and outstanding-work checks passed. +- Normal startup of retained clean data with full-row verification before work; + final coordinated normal shutdown, all four control files shut down, no database + process remaining and only the scene's own loop mappings released. Data retained. + +| PRE sample | Committed measurement transactions | Cluster TPS | +|---|---:|---:| +| 1 | 19,899 | 568.54 | +| 2 | 18,336 | 523.88 | +| 3 | 8,355 | 238.71 | +| 4 | 9,971 | 284.89 | + +Median TPS was **404.385**, peak **568.54**. Throughput varied substantially and +the median is below the previous release's recorded laboratory median. This is +not a controlled performance comparison or evidence of a speedup. Performance +analysis and optimization are separate follow-up work, not waived correctness gates. + +The assertion-enabled tested PostgreSQL binary SHA-256 is +`0a5bcbd8143ec8cf9af84331ce3d97e295b6dd5d224f89d3e7d1953cf8cc49d4`. +The full registered C unit suite contained 311 binaries. The release requires +successful **Fast CI and MVP Nightly CI on the exact tagged commit**, with every +required job executed; the GitHub Release carries that exact-commit CI evidence. + +Earlier C8/soak/micro/D7 acceptances keep their original candidate identities. +They were not rerun or relabeled as new executions on this maintenance candidate. +Historical out-of-MVP tests and their failure records remain available. + +## Limits and installation + +The [v0.130.0 scope limitations](v0.130.0.md#limits-and-upgrade-guidance) remain: +no certification of independent-host shared-LUN/GFS2 deployment, crash recovery, +automatic failover, production external fencing, backup restore, mixed-version +or rolling upgrades, general data migration/downgrade, or full SQL/2PC compatibility. +Clean shutdown/restart evidence must not be used as crash-recovery evidence. + +Use an independent installation for evaluation and preserve existing data. +This is a source release, not a signed portable binary package. Identify builds +by the immutable tag, commit, build flags and binary hash, not the legacy compiled +`pgrac_version()` string. + +```sh +git clone --branch v0.130.1 --single-branch \ + https://github.com/sqlrush/pgrac.git pgrac-mvp1 +cd pgrac-mvp1 +git rev-parse HEAD +cat PGRAC_VERSION +``` + +Follow the [maintenance MVP guide](../mvp/v0.130.1/README.md) and +[Linux single-host four-instance Quick Start](../mvp/v0.130.1/quickstart-linux-single-host.md).