forked from lei.chen/myxce
feat(ha-term-safety): retire term-0 repl_broadcast live send; carry leader term on replication (task 3.4)
Closes bug-990: the live replicator repl_broadcast wire-encoded term=0 (entry.term was never set), so standbys persisted hdr.term=0 and took the legacy direct-apply path instead of the library (term,lsn) intake. Now the live broadcast and catch-up streamer carry the leader's real term + AE prev tuple; ha_apply_wal_catchup_entry routes every term-carrying entry through raft_on_append_entries_request -> on_persist_entry (leader term stamped), retiring the term-0 direct-apply. Standbys now persist the leader's real term and reconcile by (term,lsn); the live commit chain is end-to-end. 848/849/850 + persist-before-ACK + commit-barrier + bug-960 preserved. Proven at integration level; the definitive live STRICT_TERM failover run is env-blocked here (bug-924 overlayfs) and recommended on a real fs.
This commit is contained in:
parent
87242aa8e9
commit
efe621c124
11 changed files with 307 additions and 45 deletions
|
|
@ -1278,6 +1278,83 @@ static void test_intake_logmismatch_rejected_no_false_convergence(void)
|
|||
printf("test_intake_logmismatch_rejected_no_false_convergence: PASS\n");
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* Task 3.4 — live broadcast/catch-up receive cutover (closes bug-990) */
|
||||
/* ================================================================== */
|
||||
|
||||
/* The live cutover's observable: a standby applying a LIVE-broadcast-shaped
|
||||
* AppendEntries (a single entry in the `entries` LIST carrying the leader's term
|
||||
* + prev tuple — exactly what repl_broadcast → xce_wal_batch_req_raw produces and
|
||||
* xce_decode yields) through the PRODUCTION receive entry point
|
||||
* ha_apply_wal_catchup_entry persists the LEADER's term (hdr.term == leader_term),
|
||||
* NOT 0 (the retired term-0 legacy direct-apply path) and NOT its local term.
|
||||
*
|
||||
* Before the cutover repl_broadcast sent entry->term == 0, so this entry took the
|
||||
* term-0 legacy direct-apply branch and persisted hdr.term = local p->term — the
|
||||
* bug-990 term-identity gap. With the cutover the term-carrying entry routes
|
||||
* through raft_on_append_entries_request → on_persist_entry → leader term stamped. */
|
||||
static void test_live_broadcast_receive_persists_leader_term(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_ts_live_bcast_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
cluster_t* c = make_cluster(tmp);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
/* The standby is a follower at a LOW local term (3) so we can prove the
|
||||
* persisted term is the LEADER's (7), not the local term. */
|
||||
make_follower(c, /*term*/3, /*n_peers*/3);
|
||||
|
||||
/* Seed LSN 1 at the leader's term 7 so the broadcast of LSN 2 satisfies the
|
||||
* library's logOk (prev=(1,7)). This mirrors the contiguous live stream. */
|
||||
uint8_t blob[64];
|
||||
size_t n = pack_entry(blob, WAL_JOB_SUBMIT, "a", 1);
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_cb_persist_entry_for_test(&c->ha, 1, 7, blob, n));
|
||||
c->ha.raft.log_len = 1;
|
||||
TEST_ASSERT_EQUAL_UINT64(7, persist_entry_term(&c->persist, 1));
|
||||
|
||||
/* Build the LIVE receive shape: a single entry in the `entries` list, each
|
||||
* sub-entry carrying its term + prev tuple (what wal_entry_to_wire now encodes
|
||||
* per element and xce_decode reproduces). repl_broadcast at term 7 of LSN 2
|
||||
* with prev=(1,7). */
|
||||
uint8_t lpay[2] = { 'b', 'c' };
|
||||
wal_catchup_entry_t sub;
|
||||
memset(&sub, 0, sizeof(sub));
|
||||
sub.lsn = 2;
|
||||
sub.entry_type = WAL_JOB_SUBMIT;
|
||||
sub.term = 7; /* the LEADER's term on the wire */
|
||||
sub.prev_log_index = 1;
|
||||
sub.prev_log_term = 7;
|
||||
sub.raw_payload = lpay;
|
||||
sub.raw_payload_len = sizeof(lpay);
|
||||
|
||||
wal_catchup_entry_t* sub_arr[1] = { &sub };
|
||||
wal_catchup_entry_t batch;
|
||||
memset(&batch, 0, sizeof(batch));
|
||||
batch.n_entries = 1;
|
||||
batch.entries = sub_arr;
|
||||
/* Top-level container also carries the AE metadata (the encoder mirrors it). */
|
||||
batch.term = 7;
|
||||
batch.prev_log_index = 1;
|
||||
batch.prev_log_term = 7;
|
||||
|
||||
int rc = ha_apply_wal_catchup_entry(&c->ha, &c->persist, &batch,
|
||||
/*ep*/NULL, c);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* The load-bearing assertion (bug-990): LSN 2 persists the LEADER's term 7,
|
||||
* NOT 0 and NOT the local term 3. Before the cutover this read back as the
|
||||
* standby's local term via the term-0 direct-apply path. */
|
||||
TEST_ASSERT_EQUAL_UINT64(7, persist_entry_term(&c->persist, 2));
|
||||
/* The agreed prefix (LSN 1@term7) is untouched. */
|
||||
TEST_ASSERT_EQUAL_UINT64(7, persist_entry_term(&c->persist, 1));
|
||||
/* The library adopted the leader's term on the RPC (G5). */
|
||||
TEST_ASSERT_EQUAL_UINT64(7, c->ha.raft.term);
|
||||
|
||||
teardown_cluster(c);
|
||||
printf("test_live_broadcast_receive_persists_leader_term: PASS\n");
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* Task 3.3 — drive raft_on_append_entries_reply + raft_advance_commit */
|
||||
/* on the replication ACK path */
|
||||
|
|
@ -1800,6 +1877,7 @@ void test_term_safety_adapter(void)
|
|||
RUN_TEST(test_intake_higher_term_heartbeat_adopts_term);
|
||||
RUN_TEST(test_intake_divergent_tail_truncated_and_reconverged);
|
||||
RUN_TEST(test_intake_logmismatch_rejected_no_false_convergence);
|
||||
RUN_TEST(test_live_broadcast_receive_persists_leader_term);
|
||||
RUN_TEST(test_repl_ack_currentterm_quorum_advances_commit);
|
||||
RUN_TEST(test_repl_ack_priorterm_quorum_holds_commit);
|
||||
RUN_TEST(test_repl_ack_staleterm_reply_dropped);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue