diff --git a/src/lib/raft/raft.c b/src/lib/raft/raft.c index 9b8d69b7..f3fad327 100644 --- a/src/lib/raft/raft.c +++ b/src/lib/raft/raft.c @@ -15,6 +15,11 @@ int raft_init(raft_state_t *s, const raft_config_t *cfg) /* Validate required callbacks and n_peers before touching *s */ if (cfg->n_peers < 1) return -EINVAL; + /* Even cluster sizes (>1) have no fault-tolerance advantage over the next + * smaller odd size and can mislead operators into thinking split-brain is + * impossible. Reject to prevent misconfiguration. */ + if (cfg->n_peers > 1 && cfg->n_peers % 2 == 0) + return -EINVAL; if (!cfg->cbs) return -EINVAL; if (!cfg->cbs->on_send_vote_request) diff --git a/src/lib/testsuite/test_raft.c b/src/lib/testsuite/test_raft.c index ab6bf4f9..e29f78d6 100644 --- a/src/lib/testsuite/test_raft.c +++ b/src/lib/testsuite/test_raft.c @@ -258,7 +258,7 @@ static void test_raft_init_null_become_leader_returns_einval(void) cbs.on_become_leader = NULL; memset(&cfg, 0, sizeof(cfg)); - cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; + cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } @@ -273,7 +273,7 @@ static void test_raft_init_null_become_follower_returns_einval(void) cbs.on_become_follower = NULL; memset(&cfg, 0, sizeof(cfg)); - cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; + cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } @@ -288,7 +288,7 @@ static void test_raft_init_null_persist_returns_einval(void) cbs.on_persist = NULL; memset(&cfg, 0, sizeof(cfg)); - cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; + cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } @@ -303,7 +303,7 @@ static void test_raft_init_null_schedule_timeout_returns_einval(void) cbs.on_schedule_timeout = NULL; memset(&cfg, 0, sizeof(cfg)); - cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; + cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } @@ -318,7 +318,7 @@ static void test_raft_init_null_cancel_timeout_returns_einval(void) cbs.on_cancel_timeout = NULL; memset(&cfg, 0, sizeof(cfg)); - cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; + cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } @@ -333,11 +333,47 @@ static void test_raft_init_null_start_election_after_delay_returns_einval(void) cbs.on_start_election_after_delay = NULL; memset(&cfg, 0, sizeof(cfg)); - cfg.n_peers = 2; cfg.self_idx = 0; cfg.cbs = &cbs; + cfg.n_peers = 3; cfg.self_idx = 0; cfg.cbs = &cbs; TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); } +/* n_peers even (>1) → -EINVAL: even-sized clusters have no fault-tolerance + * advantage over the next-smaller odd cluster; reject to prevent misconfiguration. */ +static void test_raft_init_even_peers_returns_einval(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_full_cbs(); + raft_config_t cfg; + + memset(&s, 0xAB, sizeof(s)); + + /* n_peers=2: even, >1 → must fail */ + memset(&cfg, 0, sizeof(cfg)); + cfg.n_peers = 2; + cfg.self_idx = 0; + cfg.cbs = &cbs; + TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); + + /* n_peers=4: even, >1 → must fail */ + cfg.n_peers = 4; + TEST_ASSERT_EQUAL_INT(-EINVAL, raft_init(&s, &cfg)); + + /* n_peers=1: single-node is always valid (odd by convention) */ + cfg.n_peers = 1; + TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg)); + + /* n_peers=3: odd, >1 → must succeed */ + memset(&cfg, 0, sizeof(cfg)); + cfg.n_peers = 3; + cfg.self_idx = 0; + cfg.cbs = &cbs; + TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg)); + + /* s must NOT be unchanged after valid inits above */ + TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); +} + /* n_peers == 0 → -EINVAL */ static void test_raft_init_zero_peers_returns_einval(void) { @@ -2849,6 +2885,7 @@ void test_raft(void) RUN_TEST(test_raft_init_null_schedule_timeout_returns_einval); RUN_TEST(test_raft_init_null_cancel_timeout_returns_einval); RUN_TEST(test_raft_init_null_start_election_after_delay_returns_einval); + RUN_TEST(test_raft_init_even_peers_returns_einval); RUN_TEST(test_raft_init_zero_peers_returns_einval); RUN_TEST(test_raft_init_success_fields); RUN_TEST(test_raft_init_default_election_timeout);