fix bug-1060 bug-1067

This commit is contained in:
Rongsong Shen 2026-06-24 17:05:43 +08:00
parent 6067ba6c94
commit 61e1391cd0
28 changed files with 7021 additions and 2926 deletions

View file

@ -1001,6 +1001,60 @@ static void test_safe_commit_reflects_granting_voter_lsn(void)
printf("test_safe_commit_reflects_granting_voter_lsn: PASS\n");
}
/* ── bug-1060: a new leader must NEVER truncate its own committed entries ─────
*
* Leader Completeness (Raft §5.4): a server that wins an election must already
* hold every entry committed by a prior leader, and it must never truncate its
* own log to "catch down" to a slower majority.
*
* Production root cause (bug-1060): raft_cb_become_leader truncated its WAL down
* to ha_compute_safe_committed_lsn()'s majority-threshold element. When the new
* leader durably holds entries ABOVE that element (it acked them as a standby
* under the prior primary — so they were committed), truncating to the majority
* element physically destroyed those committed entries and rewound commitIndex.
* e.g. granted voters {self=47, peer=44} → median safe_lsn=44, but self holds
* committed entries 45..47 → truncate(44) deletes 45..47. The leader then
* wrote its leadership NOP at LSN 45 (term+1) while the diverged peers still
* held the old job at LSN 45 → permanent prevote deadlock + committed-job loss.
*
* Invariant under test: the promotion truncation floor is clamped to the new
* leader's own durable tip, so persist_truncate_wal_above is a no-op on the
* leader's durable log. safe_lsn only governs removal of entries STRICTLY ABOVE
* the leader's durable tip (non-durable, divergent tail).
*
* RED before the fix: ha_promotion_truncate_floor does not exist (undefined
* reference).
*/
static void test_promotion_never_truncates_own_committed_entries(void)
{
/* The bug-1060 case: majority element (44) is BELOW the leader's own durable
* tip (47). The truncation floor must clamp UP to the durable tip so the
* leader's committed entries 45..47 are never cut. */
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
47ULL, ha_promotion_truncate_floor(/*safe_lsn=*/44ULL, /*durable_tip=*/47ULL),
"truncation floor must never fall below the leader's own durable tip "
"(Leader Completeness: a new leader never truncates its own committed log)");
/* Equal: floor stays at the durable tip (no truncation). */
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
47ULL, ha_promotion_truncate_floor(47ULL, 47ULL),
"equal safe_lsn and durable tip → floor at the tip (no-op truncation)");
/* safe_lsn ABOVE the durable tip: only a divergent tail strictly above the
* leader's durable log can be removed — safe_lsn governs there. */
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
50ULL, ha_promotion_truncate_floor(50ULL, 47ULL),
"when safe_lsn exceeds the durable tip, the floor follows safe_lsn "
"(removes only the non-durable divergent tail above the leader's log)");
/* Single-node / fresh boot: durable tip 0 → floor follows safe_lsn. */
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
0ULL, ha_promotion_truncate_floor(0ULL, 0ULL),
"empty log: floor 0 (nothing to truncate)");
printf("test_promotion_never_truncates_own_committed_entries: PASS\n");
}
/* ── Test suite entry point ───────────────────────────────────────────────── */
void test_ha_election(void)
@ -1059,4 +1113,7 @@ void test_ha_election(void)
/* bug-828 — safe-commit-from-votes must reflect the granting voter LSN */
RUN_TEST(test_safe_commit_reflects_granting_voter_lsn);
/* bug-1060 — a new leader must never truncate its own committed entries */
RUN_TEST(test_promotion_never_truncates_own_committed_entries);
}