feat(ha-config-bootstrap): implement vote handlers and term persistence (task 4.2)
- ha_persist_term(): atomic write+rename of ha_term_file_t with CRC32 - handle_request_vote(): term/voted_for/freshness/catch-up gate chain - handle_request_vote_reply(): vote counting, quorum+all-replied signaling - ha_elect_evaluate_and_signal(): quorum+yield+zero-config gates (4.3 early) - am_i_primary_quorum(): single-node shortcircuit + 2-round retry (4.3 partial) - 24 Tests 0 Failures in test_ha_election suite
This commit is contained in:
parent
403dd17f8b
commit
0f88d5d32a
4 changed files with 538 additions and 1 deletions
|
|
@ -110,6 +110,11 @@ ctrl_role_t am_i_primary_quorum(ha_state_t *ha, int n_peers,
|
|||
xce_candidate_t **peers,
|
||||
uint64_t my_config_tag, uint64_t my_lsn);
|
||||
|
||||
void handle_request_vote_reply(ha_state_t *ha, ha_elect_ctx_t *ctx,
|
||||
int peer_idx, uint64_t term,
|
||||
int vote_granted,
|
||||
uint64_t peer_config_tag, uint64_t peer_lsn);
|
||||
|
||||
/* ── Minimal ha_config_t used across all election tests ─────────────────── */
|
||||
|
||||
static ha_config_t g_elect_cfg = {
|
||||
|
|
@ -803,6 +808,123 @@ static void test_vote_request_from_self_same_term_granted(void)
|
|||
printf("test_vote_request_from_self_same_term_granted: PASS\n");
|
||||
}
|
||||
|
||||
/* ── handle_request_vote_reply tests ────────────────────────────────────────
|
||||
*
|
||||
* These tests verify the reply handler's counting and signalling logic.
|
||||
* They use a 3-node cluster (n_peers=2 other peers) so quorum = ceil(3/2) = 2.
|
||||
* self-vote starts at 1; receiving one granted reply raises votes_granted to 2,
|
||||
* which meets quorum — the handler must post ctx->done at that point.
|
||||
*/
|
||||
|
||||
/* Req 4.2: one granted reply in a 3-node cluster reaches quorum → done posted */
|
||||
static void test_reply_handler_quorum_reached_posts_done(void)
|
||||
{
|
||||
/* 3-node cluster: n_peers=2. quorum=2. votes_granted starts at 1 (self). */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_elect_cfg, 0);
|
||||
atomic_store_explicit(&ha.term, 1ULL, memory_order_release);
|
||||
|
||||
ha_elect_ctx_t *ctx = alloc_elect_ctx(2);
|
||||
/* Record candidate's own tag/lsn so the evaluator can apply the yield gate */
|
||||
ctx->votes[0].my_tag = 100ULL;
|
||||
ctx->votes[0].my_lsn = 50ULL;
|
||||
|
||||
/* Peer 0 grants the vote with equal freshness → votes_granted becomes 2 = quorum */
|
||||
handle_request_vote_reply(&ha, ctx,
|
||||
0, /* peer_idx */
|
||||
1ULL, /* term */
|
||||
1, /* vote_granted */
|
||||
100ULL, /* peer_config_tag (equal to candidate) */
|
||||
50ULL /* peer_lsn */);
|
||||
|
||||
/* ctx->done should have been posted — sem_trywait must succeed */
|
||||
int rc = sem_trywait(&ctx->done);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
|
||||
"ctx->done must be posted when quorum is reached after first granted reply");
|
||||
|
||||
/* Tallies must be updated correctly */
|
||||
TEST_ASSERT_EQUAL_INT(2, ctx->votes_granted); /* self + peer 0 */
|
||||
TEST_ASSERT_EQUAL_INT(1, ctx->n_replied);
|
||||
TEST_ASSERT_EQUAL_INT(1, ctx->votes[0].replied);
|
||||
TEST_ASSERT_EQUAL_INT(1, ctx->votes[0].vote_granted);
|
||||
|
||||
free_elect_ctx(ctx);
|
||||
ha_fini(&ha);
|
||||
printf("test_reply_handler_quorum_reached_posts_done: PASS\n");
|
||||
}
|
||||
|
||||
/* Req 4.2: partial replies that do not yet reach quorum must NOT post done */
|
||||
static void test_reply_handler_partial_replies_do_not_post_done(void)
|
||||
{
|
||||
/* 5-node cluster: n_peers=4. quorum = ceil(5/2) = 3.
|
||||
* votes_granted starts at 1 (self). One granted reply → votes_granted=2 < 3.
|
||||
* n_replied=1 < 4. Neither threshold met → done must NOT be posted. */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_elect_cfg, 0);
|
||||
atomic_store_explicit(&ha.term, 2ULL, memory_order_release);
|
||||
|
||||
ha_elect_ctx_t *ctx = alloc_elect_ctx(4);
|
||||
ctx->votes[0].my_tag = 200ULL;
|
||||
ctx->votes[0].my_lsn = 100ULL;
|
||||
|
||||
/* Peer 0 grants; only 1 of 4 peers have replied → still below both thresholds */
|
||||
handle_request_vote_reply(&ha, ctx,
|
||||
0, /* peer_idx */
|
||||
2ULL, /* term */
|
||||
1, /* vote_granted */
|
||||
200ULL, /* peer_config_tag */
|
||||
100ULL /* peer_lsn */);
|
||||
|
||||
/* ctx->done must NOT have been posted — sem_trywait must fail with EAGAIN */
|
||||
int rc = sem_trywait(&ctx->done);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-1, rc,
|
||||
"ctx->done must NOT be posted when quorum is not yet reached");
|
||||
TEST_ASSERT_EQUAL_INT(EAGAIN, errno);
|
||||
|
||||
/* Tallies should still be updated */
|
||||
TEST_ASSERT_EQUAL_INT(2, ctx->votes_granted);
|
||||
TEST_ASSERT_EQUAL_INT(1, ctx->n_replied);
|
||||
|
||||
free_elect_ctx(ctx);
|
||||
ha_fini(&ha);
|
||||
printf("test_reply_handler_partial_replies_do_not_post_done: PASS\n");
|
||||
}
|
||||
|
||||
/* Req 4.2: all peers reply with denials → n_replied reaches n_peers → done posted */
|
||||
static void test_reply_handler_all_denied_posts_done_on_last_reply(void)
|
||||
{
|
||||
/* 3-node cluster: n_peers=2. Both peers deny → votes_granted stays 1 < quorum(2).
|
||||
* After second deny n_replied=2 = n_peers → done is posted (CTRL_STANDBY result). */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_elect_cfg, 0);
|
||||
atomic_store_explicit(&ha.term, 1ULL, memory_order_release);
|
||||
|
||||
ha_elect_ctx_t *ctx = alloc_elect_ctx(2);
|
||||
ctx->votes[0].my_tag = 100ULL;
|
||||
ctx->votes[0].my_lsn = 50ULL;
|
||||
|
||||
/* Peer 0 denies */
|
||||
handle_request_vote_reply(&ha, ctx, 0, 1ULL, 0, 100ULL, 50ULL);
|
||||
/* done must not be posted yet (1 of 2 replied, votes_granted=1 < quorum=2) */
|
||||
int rc = sem_trywait(&ctx->done);
|
||||
TEST_ASSERT_EQUAL_INT(-1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(EAGAIN, errno);
|
||||
|
||||
/* Peer 1 denies — now all 2 peers replied → done must be posted */
|
||||
handle_request_vote_reply(&ha, ctx, 1, 1ULL, 0, 100ULL, 50ULL);
|
||||
rc = sem_trywait(&ctx->done);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
|
||||
"ctx->done must be posted when all peers have replied (even if all deny)");
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, ctx->votes_granted); /* only self-vote */
|
||||
TEST_ASSERT_EQUAL_INT(2, ctx->n_replied);
|
||||
TEST_ASSERT_EQUAL_INT(0, ctx->result); /* CTRL_STANDBY */
|
||||
|
||||
free_elect_ctx(ctx);
|
||||
ha_fini(&ha);
|
||||
printf("test_reply_handler_all_denied_posts_done_on_last_reply: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test suite entry point ───────────────────────────────────────────────── */
|
||||
|
||||
void test_ha_election(void)
|
||||
|
|
@ -853,4 +975,9 @@ void test_ha_election(void)
|
|||
|
||||
/* Misc / self-vote */
|
||||
RUN_TEST(test_vote_request_from_self_same_term_granted);
|
||||
|
||||
/* handle_request_vote_reply — reply handler counting and signalling */
|
||||
RUN_TEST(test_reply_handler_quorum_reached_posts_done);
|
||||
RUN_TEST(test_reply_handler_partial_replies_do_not_post_done);
|
||||
RUN_TEST(test_reply_handler_all_denied_posts_done_on_last_reply);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue