fix(raft-ha-core-safety): (term,lsn,payload-crc)-aware persist guard (task 2.2)
Payload-aware repair: identical payload = idempotent no-op; non-empty differing payload at the TIP = repair (truncate-to-lsn-1 + re-append, committed_lsn snapshot/restored under lock); empty-over-stored = keep (never degrade); BELOW-tip conflict = loud non-promotable fence (recovery_unrecoverable), never truncate the tail or lower committed_lsn. read_entry_meta_locked computes payload-crc identically to walcompare. Turns 2.1 case (a) green; new below-tip regression test pins the committed-boundary safety. Review round 1 caught + fixed a committed-data- loss bug (tail-truncate on below-tip conflict). 8/8 target suite, sibling persist/repl/catchup/durability suites green; persist-before-ACK intact.
This commit is contained in:
parent
3b5eda1429
commit
056675c8b3
3 changed files with 335 additions and 14 deletions
|
|
@ -388,6 +388,89 @@ static void test_2_1_identical_rewrite_is_idempotent_noop(void)
|
|||
printf("test_2_1_identical_rewrite_is_idempotent_noop: PASS (pin)\n");
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* TASK 2.2 REMEDIATION (round 1) — below-tip payload conflict must NOT drop
|
||||
* committed entries above the conflict, and must NOT regress committed_lsn.
|
||||
*
|
||||
* The round-1 repair fired truncate-and-reappend for ANY lsn < current_lsn,
|
||||
* which (when the conflict sits BELOW the stored tip — an empty body at a
|
||||
* committed LSN with committed entries ABOVE it) deleted those committed
|
||||
* entries and rolled committed_lsn backward — a State Machine Safety /
|
||||
* commit-monotonicity violation on the still-live term-blind apply route.
|
||||
*
|
||||
* Construct exactly that shape: entries [1..N] (N=4) with an empty (PLEN=0)
|
||||
* body at k=2 (k < N, so entries 3,4 sit ABOVE the conflict), committed_lsn=N.
|
||||
* Stream the CORRECT full body at lsn=k (the corrective replicated write).
|
||||
* The fix must NOT truncate the tail here: assert (a) entries k+1..N survive,
|
||||
* and (b) committed_lsn is unchanged. This FAILs against the round-1 version
|
||||
* (entries 3,4 dropped, committed_lsn rolled to k-1) and PASSes after the fix
|
||||
* (below-tip conflict → loud non-promotable fence, stored entry kept).
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_2_2_below_tip_conflict_keeps_committed_entries(void)
|
||||
{
|
||||
hcs_member_t m;
|
||||
TEST_ASSERT_EQUAL_INT(0, hcs_member_up(&m, "btip"));
|
||||
|
||||
enum { N = 4, K = 2 };
|
||||
uint8_t full[HCS_FULL_LEN];
|
||||
hcs_fill_body(full);
|
||||
|
||||
/* Build entries [1..N] contiguously through the real replicated guard, with
|
||||
* the entry at K written EMPTY (PLEN=0) — the live cross-term catch-up
|
||||
* payload-loss signature, with committed entries K+1..N stored ABOVE it. */
|
||||
for (uint64_t lsn = 1; lsn <= N; lsn++) {
|
||||
const void* body = (lsn == K) ? NULL : (const void*)full;
|
||||
uint32_t blen = (lsn == K) ? 0u : HCS_FULL_LEN;
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
|
||||
hcs_wal_put(&m, lsn, /*term=*/3, WAL_JOB_SUBMIT, body, blen),
|
||||
"each contiguous build write must persist (real fs required — bug-924)");
|
||||
}
|
||||
|
||||
/* The whole prefix [1..N] is committed. */
|
||||
atomic_store_explicit(&m.p->committed_lsn, (uint64_t)N, memory_order_release);
|
||||
|
||||
/* Sanity: entries K+1..N are present with the full payload before the repair. */
|
||||
for (uint64_t lsn = K + 1; lsn <= N; lsn++) {
|
||||
hcs_probe_t pr;
|
||||
TEST_ASSERT_EQUAL_INT(0, hcs_wal_probe(&m, lsn, &pr));
|
||||
TEST_ASSERT_TRUE_MESSAGE(pr.present, "above-conflict entry must exist pre-repair");
|
||||
TEST_ASSERT_EQUAL_UINT32(HCS_FULL_LEN, pr.len);
|
||||
}
|
||||
|
||||
/* The corrective FULL body is streamed for the SAME (term, lsn=K) — a
|
||||
* non-empty payload that differs from the stored empty body, BELOW the tip. */
|
||||
int rc = hcs_wal_put(&m, /*lsn=*/K, /*term=*/3, WAL_JOB_SUBMIT, full, HCS_FULL_LEN);
|
||||
TEST_ASSERT_TRUE_MESSAGE(rc == 0,
|
||||
"the below-tip conflict write must be a loud no-op (rc==0), never an error "
|
||||
"that re-streams destructively");
|
||||
|
||||
/* (a) Entries K+1..N must STILL be present — the round-1 truncate dropped
|
||||
* them; the fix must keep every committed entry above the conflict. */
|
||||
for (uint64_t lsn = K + 1; lsn <= N; lsn++) {
|
||||
hcs_probe_t pr;
|
||||
TEST_ASSERT_EQUAL_INT(0, hcs_wal_probe(&m, lsn, &pr));
|
||||
TEST_ASSERT_TRUE_MESSAGE(pr.present,
|
||||
"REGRESSION GUARD: a below-tip payload repair must NOT drop committed "
|
||||
"entries stored ABOVE the conflict LSN (round-1 truncate deleted them)");
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(HCS_FULL_LEN, pr.len,
|
||||
"above-conflict committed entry must keep its full payload");
|
||||
}
|
||||
|
||||
/* (b) committed_lsn must NOT be lowered — round-1 rolled it back to K-1. */
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE((uint64_t)N,
|
||||
atomic_load_explicit(&m.p->committed_lsn, memory_order_acquire),
|
||||
"REGRESSION GUARD: a below-tip payload repair must NOT regress committed_lsn "
|
||||
"(commit monotonicity / State Machine Safety)");
|
||||
|
||||
/* The below-tip conflict raises the loud non-promotable fence (stays ALIVE,
|
||||
* self-heals via catch-up) rather than corrupting committed state. */
|
||||
TEST_ASSERT_TRUE_MESSAGE(persist_recovery_unrecoverable(m.p),
|
||||
"a below-tip payload conflict must raise the non-promotable fence");
|
||||
|
||||
hcs_member_down(&m);
|
||||
printf("test_2_2_below_tip_conflict_keeps_committed_entries: PASS\n");
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* SCAFFOLDING (PASS) — prove each shared seam is usable by later phases.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
|
@ -519,6 +602,9 @@ void test_ha_core_safety(void)
|
|||
RUN_TEST(test_smoke_red_payload_blind_guard_drops_corrective_write);
|
||||
RUN_TEST(test_2_1_full_then_empty_must_not_degrade);
|
||||
RUN_TEST(test_2_1_identical_rewrite_is_idempotent_noop);
|
||||
/* Task 2.2 remediation: a below-tip payload conflict must not drop
|
||||
* committed entries above it nor regress committed_lsn. */
|
||||
RUN_TEST(test_2_2_below_tip_conflict_keeps_committed_entries);
|
||||
/* Scaffolding: each shared seam is usable by 2.3 / 4.1 / 5.1. */
|
||||
RUN_TEST(test_seam_wal_construct_and_probe);
|
||||
RUN_TEST(test_seam_divergence_is_observable);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue