fix bug-1060 bug-1067

This commit is contained in:
Rongsong Shen 2026-06-24 17:05:43 +08:00
parent 6067ba6c94
commit 61e1391cd0
28 changed files with 7021 additions and 2926 deletions

View file

@ -1529,6 +1529,141 @@ static void test_catchup_applied_lsn_is_last_held_no_skip(void)
printf("test_catchup_applied_lsn_is_last_held_no_skip: PASS\n");
}
/* ================================================================== */
/* bug-1067 — cross-term catch-up: the AppendEntries header term is the */
/* leader's CURRENT term, NOT the streamed entry's stored term */
/* ================================================================== */
/* Root cause: ha_consume_append_entries used the entry's stored term (entry_term)
* as the AppendEntries header term. For LIVE appends that is the leader's current
* term (leader appends at its term), but for CATCH-UP streaming of HISTORICAL
* entries written by a PRIOR leader, entry_term < the current leader's term. The
* library's stale-term check (term < s->term → ae_reject_stale) then permanently
* rejected every historical entry, so the lagging standby could never catch up
* (standby_reported_lsn never advanced; the streamer retransmitted forever).
*
* The fix carries the leader's current term out-of-band per entry
* (wal_catchup_entry_t.leader_current_term, WalCatchupEntry.leaderCurrentTerm @8),
* set by the streamer to ha->term, and uses THAT as the AE header term while the
* entry's stored term remains the per-entry term for logOk / on_persist_entry.
*
* RED before the fix: the leader_current_term field does not exist (compile
* error); once added but before the receiver reads it, ae_term == entry_term (4) <
* s->term (5) → stale-reject → LSN 2 never persists. */
static void test_intake_crossterm_catchup_uses_leader_current_term(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_ts_xterm_catchup_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
cluster_t* c = make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
/* Standby has already adopted the current leader's term (5) from heartbeats. */
make_follower(c, /*term*/5, /*n_peers*/3);
/* Local log holds a HISTORICAL entry LSN1@term4 (written by the prior leader
* at term 4, replicated to this standby). */
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, 4, blob, n));
c->ha.raft.log_len = 1;
TEST_ASSERT_EQUAL_UINT64(4, persist_entry_term(&c->persist, 1));
/* The CURRENT leader (term 5) streams the historical entry LSN2@term4 as a
* catch-up sub-entry: the entry's STORED term is 4, but the AppendEntries
* header term is the leader's CURRENT term 5 (leader_current_term). prev tuple
* (1,4) matches the local log so logOk holds. */
uint8_t lpay[1] = { 'b' };
wal_catchup_entry_t sub;
memset(&sub, 0, sizeof(sub));
sub.lsn = 2;
sub.entry_type = WAL_JOB_SUBMIT;
sub.term = 4; /* the entry's STORED term (prior leader) */
sub.leader_current_term = 5; /* the streaming leader's CURRENT term */
sub.prev_log_index = 1;
sub.prev_log_term = 4;
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;
batch.term = 4;
batch.leader_current_term = 5;
batch.prev_log_index = 1;
batch.prev_log_term = 4;
int rc = ha_apply_wal_catchup_entry(&c->ha, &c->persist, &batch, /*ep*/NULL, c);
TEST_ASSERT_EQUAL_INT(0, rc);
/* THE bug-1067 assertion: the historical entry is ACCEPTED (not stale-rejected)
* and persisted at its stored term 4. Pre-fix it was rejected (absent). */
TEST_ASSERT_EQUAL_UINT64(4, persist_entry_term(&c->persist, 2));
/* The agreed prefix (LSN1@term4) is untouched. */
TEST_ASSERT_EQUAL_UINT64(4, persist_entry_term(&c->persist, 1));
/* The leader's current term (5) equals our term — no spurious term change. */
TEST_ASSERT_EQUAL_UINT64(5, c->ha.raft.term);
teardown_cluster(c);
printf("test_intake_crossterm_catchup_uses_leader_current_term: PASS\n");
}
/* Safety guard: a GENUINELY stale leader (leader_current_term < our term) must
* STILL be rejected — the fix must not open a hole letting a deposed leader's
* divergent entries through. */
static void test_intake_stale_leader_term_still_rejected(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_ts_stale_leader_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
cluster_t* c = make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
make_follower(c, /*term*/5, /*n_peers*/3);
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, 4, blob, n));
c->ha.raft.log_len = 1;
/* A deposed leader at term 3 (< our term 5) tries to stream. Even though the
* entry's stored term and prev tuple look locally plausible, the AE header
* term (leader_current_term=3) is stale → reject. */
uint8_t lpay[1] = { 'z' };
wal_catchup_entry_t sub;
memset(&sub, 0, sizeof(sub));
sub.lsn = 2;
sub.entry_type = WAL_JOB_SUBMIT;
sub.term = 3;
sub.leader_current_term = 3; /* STALE leader term < our term 5 */
sub.prev_log_index = 1;
sub.prev_log_term = 4;
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;
batch.term = 3;
batch.leader_current_term = 3;
batch.prev_log_index = 1;
batch.prev_log_term = 4;
int rc = ha_apply_wal_catchup_entry(&c->ha, &c->persist, &batch, /*ep*/NULL, c);
TEST_ASSERT_EQUAL_INT(0, rc);
/* Stale leader's entry was REJECTED — LSN 2 absent, our term unchanged. */
TEST_ASSERT_EQUAL_UINT64(0, persist_entry_term(&c->persist, 2));
TEST_ASSERT_EQUAL_UINT64(5, c->ha.raft.term);
teardown_cluster(c);
printf("test_intake_stale_leader_term_still_rejected: PASS\n");
}
/* ================================================================== */
/* Task 3.3 — drive raft_on_append_entries_reply + raft_advance_commit */
/* on the replication ACK path */
@ -2065,6 +2200,8 @@ void test_term_safety_adapter(void)
RUN_TEST(test_intake_logmismatch_rejected_no_false_convergence);
RUN_TEST(test_live_broadcast_receive_persists_leader_term);
RUN_TEST(test_catchup_applied_lsn_is_last_held_no_skip);
RUN_TEST(test_intake_crossterm_catchup_uses_leader_current_term);
RUN_TEST(test_intake_stale_leader_term_still_rejected);
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);