forked from lei.chen/myxce
test(ha-term-safety): end-to-end adapter callback + marker-path integration test (task 5.1)
This commit is contained in:
parent
be4c1f0721
commit
d39500ba48
2 changed files with 186 additions and 1 deletions
|
|
@ -11,6 +11,7 @@
|
|||
#include <unity/unity.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "config_repl.h"
|
||||
#include "persist.h"
|
||||
#include "wal_types.h"
|
||||
#include "repl.h"
|
||||
|
|
@ -1588,6 +1589,185 @@ static void test_repl_ack_out_of_range_peer_rejected(void)
|
|||
printf("test_repl_ack_out_of_range_peer_rejected: PASS\n");
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* Task 5.1 — every-callback + marker-path end-to-end integration */
|
||||
/* ================================================================== */
|
||||
|
||||
/*
|
||||
* The per-task tests above each exercise ONE callback (or one leg of the
|
||||
* marker path) in isolation. This integration test drives a single realistic
|
||||
* leader-lifecycle flow in which ALL FIVE adapter callbacks AND the config
|
||||
* marker path interoperate end-to-end, so a regression that breaks the
|
||||
* COMPOSITION of the callbacks (not just one in isolation) is caught here:
|
||||
*
|
||||
* on_persist_entry — primary job append + config-marker append both
|
||||
* land in the WAL with the live term stamped
|
||||
* on_get_entry_term — the leader's last-log term is read back
|
||||
* on_send_append_entries— the leader emits the marker entry to a peer with the
|
||||
* correct (prev tuple, leader_commit) and the entry's
|
||||
* recorded term/type/payload
|
||||
* on_commit_advanced — a current-term quorum ACK advances commit; the
|
||||
* marker enters the committed prefix and the DERIVED
|
||||
* committed config (config_committed_marker) equals it
|
||||
* on_truncate_from — a divergent same-LSN tail is truncated then the
|
||||
* leader's entry re-persisted, the committed prefix
|
||||
* (including the committed marker) left intact
|
||||
*
|
||||
* Against any single broken callback the corresponding assertion below fails
|
||||
* (e.g. neutering on_commit_advanced leaves committed_lsn behind the durable
|
||||
* majority so config_committed_marker returns -ENOENT; neutering on_truncate_from
|
||||
* leaves the stale divergent term at the conflicting LSN).
|
||||
*/
|
||||
static void test_every_callback_and_marker_path_end_to_end(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_ts_e2e_all_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
const uint64_t T = 9;
|
||||
|
||||
cluster_t *c = make_cluster(tmp);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
|
||||
/* Bring the node up as the raft LEADER at term T (init-restore floor path,
|
||||
* task 2.5). A 3-peer cluster so a majority is self + 1 standby. */
|
||||
make_leader(c, T, /*n_peers*/3);
|
||||
TEST_ASSERT_EQUAL_INT(0, c->ha.my_priority); /* self_idx 0 */
|
||||
TEST_ASSERT_EQUAL_UINT64(0, c->ha.raft.log_len);
|
||||
|
||||
/* The cluster's own commit engine over the same persist (3 peers). */
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
commit_engine_init(&c->commit, &c->persist, /*n_peers*/3, /*n_vcpus*/8));
|
||||
|
||||
/* ── on_persist_entry (job append) ──────────────────────────────────────
|
||||
* A primary job-state append routes through the library → on_persist_entry
|
||||
* stamps the live term at LSN 1. */
|
||||
wal_jobstate_t js = { .jobid = 77, .state = 2 };
|
||||
uint64_t lsn_job = 0;
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
ha_primary_log_append(c, WAL_JOB_UPDATE, &js, sizeof(js), &lsn_job));
|
||||
TEST_ASSERT_EQUAL_UINT64(1, lsn_job);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, c->ha.raft.log_len);
|
||||
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(&c->persist, 1));
|
||||
|
||||
/* ── on_persist_entry (config marker — the marker path) ─────────────────
|
||||
* A config marker rides the SAME unified append path (R7.1/R7.3): it lands
|
||||
* at LSN 2 with the live term stamped, exactly like a job entry. */
|
||||
uint8_t sha[32];
|
||||
for (int i = 0; i < 32; i++) sha[i] = (uint8_t)(0xA0 + i);
|
||||
uint64_t tag = 0xA0A1A2A3A4A5A6A7ULL; /* be64 of the first 8 sha bytes */
|
||||
uint64_t lsn_marker = 0;
|
||||
TEST_ASSERT_EQUAL_INT(0, config_append_marker(c, tag, sha, &lsn_marker));
|
||||
TEST_ASSERT_EQUAL_UINT64(2, lsn_marker);
|
||||
TEST_ASSERT_EQUAL_UINT64(2, c->ha.raft.log_len);
|
||||
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(&c->persist, 2));
|
||||
|
||||
/* A third, NON-committed job entry at LSN 3 (current term). It is appended
|
||||
* BEFORE the standby is registered so the primary's repl_broadcast is a
|
||||
* no-op (registering the dummy int ep first would fan the append out to a
|
||||
* non-endpoint → ASAN SEGV — the same ordering constraint the send-AE tests
|
||||
* above document). LSN 3 is the divergent suffix the on_truncate_from leg
|
||||
* removes once the committed prefix (LSN 1,2) is established below. */
|
||||
wal_jobstate_t js2 = { .jobid = 78, .state = 1 };
|
||||
uint64_t lsn3 = 0;
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
ha_primary_log_append(c, WAL_JOB_UPDATE, &js2, sizeof(js2), &lsn3));
|
||||
TEST_ASSERT_EQUAL_UINT64(3, lsn3);
|
||||
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(&c->persist, 3));
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_flush(&c->persist));
|
||||
|
||||
/* ── on_get_entry_term ──────────────────────────────────────────────────
|
||||
* The leader's last-log term (LSN 3) reads back as T via the wired callback. */
|
||||
TEST_ASSERT_EQUAL_UINT64(T,
|
||||
ha_cb_get_entry_term_for_test(&c->ha, c->ha.raft.log_len));
|
||||
|
||||
/* ── on_send_append_entries ─────────────────────────────────────────────
|
||||
* The leader emits the marker entry (LSN 2) to peer 0, carrying the prev
|
||||
* tuple (LSN 1, term T), leader_commit, and the marker's own recorded term +
|
||||
* WAL_CONFIG_MARKER type + 40-byte reference payload (read from its own WAL). */
|
||||
register_one_standby(c);
|
||||
repl_ae_capture_t cap;
|
||||
memset(&cap, 0, sizeof(cap));
|
||||
repl_send_ae_capture_hook = ∩
|
||||
int rc = ha_cb_send_append_entries_for_test(&c->ha, /*peer*/0,
|
||||
/*prev_idx*/1, /*prev_term*/T,
|
||||
/*leader_commit*/0, /*n*/1);
|
||||
repl_send_ae_capture_hook = NULL;
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, cap.called);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, cap.prev_log_index);
|
||||
TEST_ASSERT_EQUAL_UINT64(T, cap.prev_log_term);
|
||||
TEST_ASSERT_EQUAL_UINT64(2, cap.entry_lsn);
|
||||
TEST_ASSERT_EQUAL_UINT64(T, cap.entry_term);
|
||||
TEST_ASSERT_EQUAL_UINT8(WAL_CONFIG_MARKER, cap.entry_type);
|
||||
TEST_ASSERT_EQUAL_UINT32(WAL_CONFIG_MARKER_PAYLOAD_LEN, cap.payload_len);
|
||||
|
||||
/* ── on_commit_advanced (+ committed marker derive) ─────────────────────
|
||||
* Before commit advances, the marker is appended but NOT in the committed
|
||||
* prefix, so the derived committed config is absent. */
|
||||
uint64_t got_tag = 0;
|
||||
uint8_t got_sha[32];
|
||||
TEST_ASSERT_EQUAL_INT(-ENOENT,
|
||||
config_committed_marker(c, &got_tag, got_sha));
|
||||
|
||||
/* on_commit_advanced is LOAD-BEARING (production order: the ACK path fires
|
||||
* on_commit_advanced, which records the library ceiling, BEFORE the barrier
|
||||
* re-evaluates the durable majority). Fire the WIRED callback at
|
||||
* commit_index=1 FIRST to set the ceiling at 1, THEN supply a durable
|
||||
* majority covering LSN 2. committed_lsn caps at the library ceiling (1),
|
||||
* BELOW the durable majority (2). This can only hold if on_commit_advanced
|
||||
* fed the library current-term commit point as the barrier ceiling (the
|
||||
* term-blind safe-commit is no longer the source); a no-op callback would
|
||||
* let the barrier release committed_lsn to the durable majority (2). */
|
||||
ha_cb_commit_advanced_for_test(&c->ha, 1);
|
||||
atomic_store(&c->persist.repl.standbys[0].acked_lsn, 2);
|
||||
commit_on_ack(&c->commit, /*peer_idx*/0, /*acked_lsn*/2);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, atomic_load(&c->persist.committed_lsn));
|
||||
|
||||
/* Now route the real current-term quorum ACK through the library
|
||||
* (raft_on_append_entries_reply → raft_advance_commit → on_commit_advanced).
|
||||
* The agreed entries are current-term (T) so the library commit advances to
|
||||
* 2 and the callback raises the ceiling, releasing committed_lsn to 2. */
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
ha_raft_on_repl_ack(c, /*abs_peer_idx*/1, /*acked_lsn*/2, /*reply_term*/T));
|
||||
TEST_ASSERT_EQUAL_UINT64(2, c->ha.raft.commit_index);
|
||||
TEST_ASSERT_EQUAL_UINT64(2, atomic_load(&c->persist.committed_lsn));
|
||||
|
||||
/* The marker (LSN 2) is now in the committed prefix → the DERIVED committed
|
||||
* config equals the appended marker's {tag, sha} (R7.7). */
|
||||
TEST_ASSERT_EQUAL_INT(0, config_committed_marker(c, &got_tag, got_sha));
|
||||
TEST_ASSERT_EQUAL_UINT64(tag, got_tag);
|
||||
TEST_ASSERT_EQUAL_MEMORY(sha, got_sha, 32);
|
||||
|
||||
/* ── on_truncate_from ───────────────────────────────────────────────────
|
||||
* The uncommitted suffix (the LSN 3 job entry appended above) is removed by
|
||||
* truncate-from LSN 3, while the committed prefix (job LSN 1 + committed
|
||||
* marker LSN 2) stays byte-stable (R3.2/R3.3). on_truncate_from is the
|
||||
* callback under test; the committed marker derive must still hold after. */
|
||||
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(&c->persist, 3)); /* present */
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_cb_truncate_from_for_test(&c->ha, 3));
|
||||
TEST_ASSERT_EQUAL_UINT64(0, persist_entry_term(&c->persist, 3)); /* gone */
|
||||
|
||||
/* The committed prefix is intact: LSN 1 (job) + LSN 2 (marker) keep their
|
||||
* terms, and the committed marker still derives to the same {tag, sha}. */
|
||||
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(&c->persist, 1));
|
||||
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(&c->persist, 2));
|
||||
TEST_ASSERT_EQUAL_INT(0, config_committed_marker(c, &got_tag, got_sha));
|
||||
TEST_ASSERT_EQUAL_UINT64(tag, got_tag);
|
||||
TEST_ASSERT_EQUAL_MEMORY(sha, got_sha, 32);
|
||||
|
||||
/* All five term-safety callbacks remain wired (non-NULL) through the flow. */
|
||||
TEST_ASSERT_NOT_NULL(c->ha.raft.cbs->on_persist_entry);
|
||||
TEST_ASSERT_NOT_NULL(c->ha.raft.cbs->on_get_entry_term);
|
||||
TEST_ASSERT_NOT_NULL(c->ha.raft.cbs->on_send_append_entries);
|
||||
TEST_ASSERT_NOT_NULL(c->ha.raft.cbs->on_truncate_from);
|
||||
TEST_ASSERT_NOT_NULL(c->ha.raft.cbs->on_commit_advanced);
|
||||
|
||||
commit_engine_fini(&c->commit);
|
||||
teardown_cluster(c);
|
||||
printf("test_every_callback_and_marker_path_end_to_end: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Suite entry point */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -1626,4 +1806,6 @@ void test_term_safety_adapter(void)
|
|||
RUN_TEST(test_repl_ack_abs_peer_idx_endpoint_type_roundtrip);
|
||||
RUN_TEST(test_repl_ack_heartbeat_standby_absolute_slot);
|
||||
RUN_TEST(test_repl_ack_out_of_range_peer_rejected);
|
||||
/* Task 5.1: every-callback + marker-path end-to-end integration. */
|
||||
RUN_TEST(test_every_callback_and_marker_path_end_to_end);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue