feat(ha-repl-hardening): set committed_lsn from vote-derived safe point (task 4.2)

- In raft_cb_become_leader, after persist_truncate_wal_above(safe_lsn),
  store safe_lsn to ha->last_known_committed_lsn via atomic release
- Test: 3-node voters [100,150,200] → safe_lsn=150; asserts
  ha->last_known_committed_lsn == 150 after raft_cb_become_leader_for_test
This commit is contained in:
Rongsong Shen 2026-06-05 03:28:59 +00:00
parent e7919304c3
commit 5915150c3b
3 changed files with 118 additions and 1 deletions

View file

@ -155,7 +155,7 @@
- _Requirements: 2.3_
- _Boundary: Controller FSM (ha.c)_
- [ ] 4.2 Replace stale-ctrlhello truncation with vote-derived safe commit point
- [x] 4.2 Replace stale-ctrlhello truncation with vote-derived safe commit point
- Remove `persist_truncate_wal_above(last_known_committed_lsn)` from the become-leader path
- Replace with `persist_truncate_wal_above(safe_committed_lsn)` using the value from task 4.1
- Set `committed_lsn = safe_committed_lsn` after truncation

View file

@ -468,6 +468,17 @@ static void raft_cb_become_leader(void *ctx, uint64_t term)
if (c) {
persist_truncate_wal_above(&c->persist, safe_lsn);
}
/* Task 4.2 (ha-repl-hardening): set committed_lsn = safe_lsn.
* persist_truncate_wal_above already sets c->persist.committed_lsn
* in the production path (c != NULL). Also update
* ha->last_known_committed_lsn so the test-mode decision loop
* (XCE_TESTING path in standby heartbeat reply handler) and any
* code reading ha->last_known_committed_lsn see the safe commit
* point immediately after promotion.
* Design §Flow 0 step 5: committed_lsn = safe_committed_lsn.
* Req 2.3 (ha-repl-hardening). */
atomic_store_explicit(&ha->last_known_committed_lsn, safe_lsn,
memory_order_release);
/* Update committed config tag baseline for the COMMITTING decision loop */
atomic_store_explicit(&ha->committed_config_tag, safe_tag,
memory_order_release);

View file

@ -608,6 +608,110 @@ static void test_safe_committed_lsn_one_peer_denied(void)
#endif
}
/* ── Task 4.2 (ha-repl-hardening): committed_lsn set to safe_committed_lsn ──
*
* raft_cb_become_leader must set ha->last_known_committed_lsn to the value
* returned by ha_compute_safe_committed_lsn after WAL truncation.
*
* This ensures that in test mode (c == NULL) the decision loop reads the
* correct committed_lsn from ha->last_known_committed_lsn (see decision-loop
* XCE_TESTING path in ha.c).
*
* Requirement 2.3 (ha-repl-hardening): safe_committed_lsn is derived from
* voter replies; entries at or below it survive truncation; entries above are
* removed.
*
* The test uses raft_cb_become_leader_for_test (XCE_TESTING shim) which calls
* raft_cb_become_leader with ha->raft_c = NULL (no real cluster, no WAL I/O).
* The WAL-level truncation behaviour (file entries above safe_lsn deleted) is
* covered by test_persist_truncate.c; this test focuses on the committed_lsn
* field assignment that task 4.2 adds.
*
* Design ref: design.md §Flow 0 step 5: committed_lsn = safe_committed_lsn
*/
#ifdef XCE_TESTING
extern void raft_cb_become_leader_for_test(ha_state_t *ha, uint64_t term);
#endif
/*
* Test E: raft_cb_become_leader sets ha->last_known_committed_lsn = safe_lsn.
*
* 3-node cluster (n_other=2, votes_granted=3):
* votes[0]: my_lsn=200, peer_lsn=100, vote_granted=1
* votes[1]: peer_lsn=150, vote_granted=1
*
* Sorted granted LSNs desc: [200 (self), 150 (peer1), 100 (peer0)]
* Majority threshold for 3 granted = ceil(3/2) = 2; element at index 1 = 150
* → safe_committed_lsn = 150
*
* After raft_cb_become_leader_for_test, ha->last_known_committed_lsn must == 150.
*
* Additionally verify via ha_compute_safe_committed_lsn that entry AT safe_lsn
* (lsn=150) is the boundary: safe_lsn=150 means truncate ABOVE 150 (entries
* 151+ removed); entry at 150 is preserved.
*/
static void test_become_leader_sets_committed_lsn_to_safe_lsn(void)
{
#ifdef XCE_TESTING
reset_counters();
ha_state_t ha;
ha_init(&ha, &g_cfg, 0);
ha.phase2_done = 1; /* skip phase2 in this test */
/* Build elect_ctx with 2 peers; safe_lsn will be 150 */
ha_elect_ctx_t *ctx = calloc(1, sizeof(*ctx) + 2 * sizeof(ctx->votes[0]));
TEST_ASSERT_NOT_NULL(ctx);
ctx->n_peers = 2;
ctx->votes_granted = 3; /* self + 2 peers */
ctx->n_replied = 2;
pthread_mutex_init(&ctx->lock, NULL);
sem_init(&ctx->done, 0, 0);
/* votes[0]: self data (my_lsn=200) + peer0 granted at lsn=100 */
ctx->votes[0].my_lsn = 200;
ctx->votes[0].my_tag = 5;
ctx->votes[0].vote_granted = 1;
ctx->votes[0].peer_lsn = 100;
ctx->votes[0].peer_tag = 5;
/* votes[1]: peer1 granted at lsn=150 */
ctx->votes[1].vote_granted = 1;
ctx->votes[1].peer_lsn = 150;
ctx->votes[1].peer_tag = 5;
/* Verify the safe_lsn computation independently */
uint64_t safe_lsn = 0, safe_tag = 0;
ha_compute_safe_committed_lsn(ctx, &safe_lsn, &safe_tag);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(150, safe_lsn,
"ha_compute_safe_committed_lsn: voters [100,150]+self=200 → safe_lsn must be 150");
/* Wire elect_ctx into ha_state so raft_cb_become_leader can read it */
atomic_store_explicit(&ha.elect_ctx, (void *)ctx, memory_order_release);
/* Drive the leader promotion path (c=NULL → no WAL I/O, no phase2) */
raft_cb_become_leader_for_test(&ha, /*term=*/1);
/* Task 4.2: ha->last_known_committed_lsn must equal safe_lsn (150) */
uint64_t committed_lsn =
atomic_load_explicit(&ha.last_known_committed_lsn, memory_order_acquire);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(150, committed_lsn,
"raft_cb_become_leader must set ha->last_known_committed_lsn = safe_lsn (150); "
"WAL entry AT safe_lsn=150 is preserved; entries above 150 are truncated");
sem_destroy(&ctx->done);
pthread_mutex_destroy(&ctx->lock);
free(ctx);
ha_fini(&ha);
clear_hook();
printf("test_become_leader_sets_committed_lsn_to_safe_lsn: PASS "
"(committed_lsn=%" PRIu64 ")\n", committed_lsn);
#else
printf("test_become_leader_sets_committed_lsn_to_safe_lsn: SKIP (non-test build)\n");
#endif
}
/* ── Suite entry point ───────────────────────────────────────────────────── */
void test_ha_evaluate_committing(void)
@ -627,4 +731,6 @@ void test_ha_evaluate_committing(void)
RUN_TEST(test_safe_committed_lsn_two_peers_at_50_and_51);
RUN_TEST(test_safe_committed_lsn_single_node);
RUN_TEST(test_safe_committed_lsn_one_peer_denied);
/* Task 4.2 (ha-repl-hardening): committed_lsn set to safe_committed_lsn */
RUN_TEST(test_become_leader_sets_committed_lsn_to_safe_lsn);
}