feat(ha-repl-hardening): audit safe commit point and snapshot transfer tests (tasks 6.3, 6.4)

6.3: Fix single-node bug — ha_compute_safe_committed_lsn now returns
self_lsn instead of 0 for n_peers=0 (prevents WAL truncation on
single-node re-election). Add self_lsn/self_tag to ha_elect_ctx_t,
populated unconditionally in ha_start_election_async. All 3 test
scenarios pass under ASAN.

6.4: Add test_snapshot_recv_128kb_two_chunks (128KB snapshot in
2x64KB chunks, verifies size and applied_lsn); 4 other test cases
already existed from tasks 3.5c/3.5d. All ASAN-clean.
This commit is contained in:
Rongsong Shen 2026-06-05 06:21:00 +00:00
parent d41cfc5813
commit 345d51dd61
5 changed files with 160 additions and 23 deletions

View file

@ -348,14 +348,15 @@ static void raft_cb_start_election_after_delay(void *ctx, uint32_t delay_ms)
*
* Algorithm (design §Flow 0: Post-Election — Determine Safe Commit Point):
* 1. Collect LSNs from all granted voters:
* - Self always granted: self_lsn = votes[0].my_lsn (when n_peers > 0)
* - Self always granted: self_lsn = ctx->self_lsn (stored unconditionally)
* - Each peer with vote_granted=1: lsn = votes[i].peer_lsn
* 2. Sort granted LSNs descending.
* 3. safe_committed_lsn = sorted[floor(n_granted / 2)]
* (the majority-threshold element — the highest LSN held by a majority)
* 4. Same logic for config tags (votes[i].my_tag / votes[i].peer_tag).
* 4. Same logic for config tags (ctx->self_tag / votes[i].peer_tag).
*
* Single-node (n_peers == 0): votes[] is empty, outputs 0 for both fields.
* Single-node (n_peers == 0): votes[] is empty; safe_committed_lsn = ctx->self_lsn.
* This prevents WAL truncation to 0 on re-election in a single-node cluster.
*
* Requirements: 2.3 (ha-repl-hardening spec)
* Boundary: Controller FSM (ha.c)
@ -375,10 +376,11 @@ void ha_compute_safe_committed_lsn(ha_elect_ctx_t *ctx,
{
if (!ctx || !safe_lsn_out || !safe_tag_out) return;
/* Single-node: no votes[] entries — self is the only majority. */
/* Single-node: no votes[] entries — self is the only majority.
* Return self_lsn so WAL is not truncated to 0 on re-election. */
if (ctx->n_peers == 0) {
*safe_lsn_out = 0;
*safe_tag_out = 0;
*safe_lsn_out = ctx->self_lsn;
*safe_tag_out = ctx->self_tag;
return;
}
@ -397,9 +399,9 @@ void ha_compute_safe_committed_lsn(ha_elect_ctx_t *ctx,
int n_granted = 0;
/* Self always granted: self's lsn/tag lives in votes[0].my_lsn / my_tag */
lsns[n_granted] = ctx->votes[0].my_lsn;
tags[n_granted] = ctx->votes[0].my_tag;
/* Self always granted: self's lsn/tag from the dedicated self_lsn/self_tag fields */
lsns[n_granted] = ctx->self_lsn;
tags[n_granted] = ctx->self_tag;
n_granted++;
/* Each peer with vote_granted=1 */
@ -3073,7 +3075,13 @@ void ha_start_election_async(ha_state_t *ha, cluster_t *c,
pthread_mutex_init(&ctx->lock, NULL);
sem_init(&ctx->done, 0, 0);
/* Store candidate's own tag/lsn; votes[0] is read by raft_cb_send_vote_request */
/* Store candidate's own tag/lsn unconditionally in self_lsn/self_tag (task 6.3):
* ha_compute_safe_committed_lsn uses these for the majority computation,
* including the single-node case where votes[] is empty. */
ctx->self_lsn = my_lsn;
ctx->self_tag = my_config_tag;
/* Also store in votes[0] for raft_cb_send_vote_request (multi-node only) */
if (n_other > 0) {
ctx->votes[0].my_tag = my_config_tag;
ctx->votes[0].my_lsn = my_lsn;