feat(ha-term-safety): drive raft_log_append on primary append path (task 3.1)

This commit is contained in:
Rongsong Shen 2026-06-22 08:09:47 +00:00
parent 3dd8493fff
commit 6a519a928b
5 changed files with 299 additions and 11 deletions

View file

@ -961,6 +961,147 @@ static void test_init_empty_wal_zero_floor(void)
printf("test_init_empty_wal_zero_floor: PASS\n");
}
/* ================================================================== */
/* Task 3.1 — drive raft_log_append on the primary append path */
/* ================================================================== */
/* Bring a make_cluster()'d node up as the raft LEADER so raft_log_append is
* enabled (it returns -EPERM off the leader). We init the library from the
* recovered floor (task 2.5) and set the live election term + raft role
* directly — modelling a node that has already won an election at `term`
* (the cerebrum raft-sim convention: set state.term after a clean election;
* raft_log_append stamps entries from s->term). */
static void make_leader(cluster_t* c, uint64_t term, int n_peers)
{
atomic_store(&c->ha.role, CTRL_PRIMARY);
atomic_store(&c->ha.term, term);
ha_init_raft(&c->ha, c, n_peers);
c->ha.raft.term = term;
c->ha.raft.role = RAFT_LEADER;
}
/* Test: a primary job-state append driven through ha_primary_log_append goes
* through the library — the library assigns the LSN (== s->log_len post-append),
* stamps the live election term into the WAL header, and keeps the persist
* write cursor co-tracked with the library index (current_lsn == log_len + 1).
* Against the pre-3.1 hand-rolled path this entry point does not exist
* (compile RED) — and once it exists the library MUST be the LSN/term source
* (R1.1, R1.2, R2.1). */
static void test_primary_log_append_stamps_term_and_assigns_lsn(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_ts_plap_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
const uint64_t T = 7;
cluster_t* c = make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
make_leader(c, T, /*n_peers*/1);
/* Fresh leader, empty log: first append takes library index 1. */
TEST_ASSERT_EQUAL_UINT64(0, c->ha.raft.log_len);
wal_jobstate_t js = { .jobid = 42, .state = 3 };
uint64_t lsn = 0;
TEST_ASSERT_EQUAL_INT(0,
ha_primary_log_append(c, WAL_JOB_UPDATE, &js, sizeof(js), &lsn));
/* (a) The LIBRARY assigned the LSN and grew its log to it. */
TEST_ASSERT_EQUAL_UINT64(1, lsn);
TEST_ASSERT_EQUAL_UINT64(1, c->ha.raft.log_len);
/* (b) The WAL header recorded the live election term for that LSN. */
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(&c->persist, 1));
/* (d) Co-tracking invariant: the persist write cursor advanced exactly in
* lockstep with the library index — current_lsn == log_len + 1 (next LSN). */
TEST_ASSERT_EQUAL_UINT64(c->ha.raft.log_len + 1, c->persist.current_lsn);
/* A second append keeps the two counters co-tracked. */
js.jobid = 43;
TEST_ASSERT_EQUAL_INT(0,
ha_primary_log_append(c, WAL_JOB_UPDATE, &js, sizeof(js), &lsn));
TEST_ASSERT_EQUAL_UINT64(2, lsn);
TEST_ASSERT_EQUAL_UINT64(2, c->ha.raft.log_len);
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(&c->persist, 2));
TEST_ASSERT_EQUAL_UINT64(c->ha.raft.log_len + 1, c->persist.current_lsn);
teardown_cluster(c);
printf("test_primary_log_append_stamps_term_and_assigns_lsn: PASS\n");
}
/* Test: the own-term promotion no-op (ha_arm_own_term_gate) is routed through
* the library, so the WAL_NOOP entry records the newly established term (not 0)
* and the library index tracks it (R1.2). Pre-3.1 the noop went through a
* hand-rolled persist_wal_async with a best-effort p->term and did NOT advance
* s->log_len. */
static void test_own_term_noop_records_live_term_via_library(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_ts_noop_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
const uint64_t T = 5;
cluster_t* c = make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
make_leader(c, T, /*n_peers*/1);
/* Register the persist hooks then arm the own-term gate exactly as the
* promotion tail does (ha_promote_to_primary / raft_cb_become_leader). */
ha_arm_own_term_gate_for_test(c);
TEST_ASSERT_EQUAL_INT(0, persist_flush(&c->persist));
/* The no-op landed at library index 1 with the live term recorded. */
TEST_ASSERT_EQUAL_UINT64(1, c->ha.raft.log_len);
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(&c->persist, 1));
TEST_ASSERT_EQUAL_UINT64(c->ha.raft.log_len + 1, c->persist.current_lsn);
teardown_cluster(c);
printf("test_own_term_noop_records_live_term_via_library: PASS\n");
}
/* Test: when raft leadership is NOT active (the library is not RAFT_LEADER —
* the init-time non-raft promotion road and engine-less test contexts),
* ha_primary_log_append still appends locally through the legacy path so the
* non-raft promotion tail keeps working, WITHOUT growing the library log (the
* library is not tracking the log on that road, so no co-tracking hazard).
* The entry still records the live election term via persist_set_term. */
static void test_primary_log_append_non_leader_legacy_fallback(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_ts_plapfb_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
const uint64_t T = 6;
cluster_t* c = make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
/* Non-raft promotion road: controller role PRIMARY + live term, but the
* raft library was never driven to LEADER (raft.role stays FOLLOWER). */
atomic_store(&c->ha.role, CTRL_PRIMARY);
atomic_store(&c->ha.term, T);
ha_init_raft(&c->ha, c, /*n_peers*/1);
TEST_ASSERT_NOT_EQUAL_INT(RAFT_LEADER, c->ha.raft.role);
wal_jobstate_t js = { .jobid = 7, .state = 1 };
uint64_t lsn = 0;
TEST_ASSERT_EQUAL_INT(0,
ha_primary_log_append(c, WAL_JOB_UPDATE, &js, sizeof(js), &lsn));
/* Appended at the contiguous next LSN (current_lsn started at 1). */
TEST_ASSERT_EQUAL_UINT64(1, lsn);
TEST_ASSERT_EQUAL_UINT64(2, c->persist.current_lsn);
/* Live term still recorded for the entry. */
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(&c->persist, 1));
/* The library log was NOT grown by the legacy fallback (no raft leadership). */
TEST_ASSERT_EQUAL_UINT64(0, c->ha.raft.log_len);
teardown_cluster(c);
printf("test_primary_log_append_non_leader_legacy_fallback: PASS\n");
}
/* ------------------------------------------------------------------ */
/* Suite entry point */
/* ------------------------------------------------------------------ */
@ -987,4 +1128,7 @@ void test_term_safety_adapter(void)
RUN_TEST(test_on_commit_advanced_wired_feeds_barrier);
RUN_TEST(test_init_restores_recovered_floor);
RUN_TEST(test_init_empty_wal_zero_floor);
RUN_TEST(test_primary_log_append_stamps_term_and_assigns_lsn);
RUN_TEST(test_own_term_noop_records_live_term_via_library);
RUN_TEST(test_primary_log_append_non_leader_legacy_fallback);
}