feat(raft-log-replication): raft_on_append_entries_request UpdateTerm+logOk+accept (tasks 5.1-5.2, 6.1-6.3)

Complete HandleAppendEntriesRequest (raft.tla:327) + UpdateTerm (raft.tla:406),
implemented as one function to avoid a misleading partial handler. Fixed step
order: UpdateTerm FIRST on every RPC incl. heartbeat (G5, strictly-greater,
clears voted_for, persists, leader step-down fires on_become_follower) ->
stale reject -> candidate step-down -> logOk (G2) -> reject if !logOk ->
accept (already-have / conflict-truncate-then-repersist (G3) / append).
Never advances commit, never does transport I/O.

Review-applied: logOk now includes the explicit prev_log_index <= log_len bound
(raft.tla:330 / Req 4.1) instead of relying solely on the 0-absent sentinel;
added a candidate-equal-term-logOk-fails step-ordering test. 15 tests, ASAN green.
This commit is contained in:
Rongsong Shen 2026-06-21 16:18:02 +00:00
parent a1aacc0389
commit 3c5185a5c3
3 changed files with 756 additions and 5 deletions

View file

@ -541,3 +541,139 @@ int raft_send_append_entries(raft_state_t *s, int peer_idx,
return RAFT_OK;
}
/* Fill *out as a rejection (success=0, match_index=0) at the current term and
* return RAFT_OK. Every reject branch shares this shape (raft.tla reject, 333). */
static int raft_ae_reject(raft_state_t *s, raft_append_result_t *out,
const char *trace_event)
{
out->reply_term = s->term;
out->success = 0;
out->match_index = 0;
if (trace_event && s->cbs->on_trace)
s->cbs->on_trace(s->ctx, trace_event, NULL);
return RAFT_OK;
}
int raft_on_append_entries_request(raft_state_t *s, int leader_idx,
uint64_t term, uint64_t prev_log_index,
uint64_t prev_log_term, uint64_t leader_commit,
const void *entry, size_t entry_len,
uint64_t entry_term, int entries_len,
raft_append_result_t *out)
{
/* leader_commit is accepted into the contract but NOT acted on in this spec
* — commit-index advance is owned by raft-commit-safety (Req 3.4 / 5.4). */
(void)leader_commit;
if (s->cbs->on_trace)
s->cbs->on_trace(s->ctx, "ae_recv", NULL);
/* ---- Step 1: UpdateTerm FIRST (raft.tla 406, Req 6.1–6.5 — closes G5) ----
* Applied on EVERY incoming AppendEntries/heartbeat, BEFORE logOk, so a
* strictly-higher-term RPC is never spuriously rejected for a log mismatch.
* Strictly-greater-than only: equal/lower term never alters term/voted_for. */
if (term > s->term) {
int was_leader = (s->role == RAFT_LEADER);
s->term = term;
s->voted_for = -1;
s->voted_for_term = 0;
s->role = RAFT_FOLLOWER;
/* Persist the adopted (term, voted_for) before processing the rest of
* the RPC (durability ordering, Req 6.2). */
s->cbs->on_persist(s->ctx, s->term, -1);
/* A node that was leader must notify the HA FSM so it steps down
* (Req 6.3), mirroring the vote-path higher-term step-down. */
if (was_leader)
s->cbs->on_become_follower(s->ctx, s->term, leader_idx);
if (s->cbs->on_trace)
s->cbs->on_trace(s->ctx, "term_adopt_ae", NULL);
}
/* ---- Step 2: stale-term reject (raft.tla 333) ----
* After UpdateTerm, term < s->term can only happen for a strictly-lower
* incoming term (the higher case adopted above). */
if (term < s->term)
return raft_ae_reject(s, out, "ae_reject_stale");
/* ---- Step 3: candidate step-down at equal term (raft.tla 346) ----
* A candidate that sees an AppendEntries at its own term concedes that a
* leader already exists for this term and returns to follower. */
if (s->role == RAFT_CANDIDATE)
s->role = RAFT_FOLLOWER;
/* ---- Step 4: logOk (raft.tla 328, Req 4.1 — closes G2) ----
* logOk = prevLogIndex == 0
* OR (prevLogIndex <= log_len AND on_get_entry_term(prevLogIndex) == prevLogTerm).
* The explicit prevLogIndex <= log_len bound mirrors raft.tla:330
* (prevLogIndex <= Len(log)). The on_get_entry_term 0 sentinel already
* rejects absent entries for any non-zero prevLogTerm, but the bound also
* defends against a malformed sender presenting prevLogTerm==0 with
* prevLogIndex past the local log (0==0 would otherwise spuriously pass). */
int log_ok = (prev_log_index == 0) ||
(prev_log_index <= s->log_len &&
s->cbs->on_get_entry_term(s->ctx, prev_log_index) == prev_log_term);
/* ---- Step 5: reject if !logOk (follower path, raft.tla 333, Req 4.2) ---- */
if (!log_ok)
return raft_ae_reject(s, out, "ae_reject_logmismatch");
/* ---- Step 6: accept (raft.tla 351), index = prevLogIndex + 1 ----
* match_index reported on success is prevLogIndex + entries_len for every
* accept sub-case (already-have / conflict-truncate / append). */
uint64_t index = prev_log_index + 1;
uint64_t match_index = prev_log_index + (uint64_t)entries_len;
/* already-have: a heartbeat (no entries), or the local entry at `index`
* already carries the incoming entry's term → success, no log rewrite. */
if (entries_len == 0 ||
s->cbs->on_get_entry_term(s->ctx, index) == entry_term) {
out->reply_term = s->term;
out->success = 1;
out->match_index = match_index;
/* must not shrink the log (already-have keeps the existing suffix) */
if (s->log_len < match_index)
s->log_len = match_index;
if (s->cbs->on_trace)
s->cbs->on_trace(s->ctx, "ae_already_have", NULL);
return RAFT_OK;
}
/* conflict-truncate (raft.tla 375, Req 4.5 — closes G3): a local entry at
* `index` exists with a DIFFERENT term. Delete the divergent suffix, then
* (intentional safe collapse) re-persist the leader's entry in the same
* handler invocation. The truncate is ALWAYS followed by the re-persist. */
if (s->cbs->on_get_entry_term(s->ctx, index) != 0) {
int rc = s->cbs->on_truncate_from(s->ctx, index);
if (rc != 0)
/* truncate failed → report a non-success so the leader retries; the
* follower's log is not corrupted (it simply did not converge). */
return raft_ae_reject(s, out, "ae_conflict_truncate_fail");
rc = s->cbs->on_persist_entry(s->ctx, index, entry_term, entry, entry_len);
if (rc != 0)
return raft_ae_reject(s, out, "ae_conflict_persist_fail");
out->reply_term = s->term;
out->success = 1;
out->match_index = match_index;
s->log_len = match_index;
if (s->cbs->on_trace)
s->cbs->on_trace(s->ctx, "ae_conflict_truncate", NULL);
return RAFT_OK;
}
/* append (raft.tla 383, Req 4.6): the local log ends exactly at
* prevLogIndex (no entry at `index`) → append the incoming entry. */
{
int rc = s->cbs->on_persist_entry(s->ctx, index, entry_term, entry, entry_len);
if (rc != 0)
return raft_ae_reject(s, out, "ae_append_persist_fail");
out->reply_term = s->term;
out->success = 1;
out->match_index = match_index;
if (s->log_len < match_index)
s->log_len = match_index;
if (s->cbs->on_trace)
s->cbs->on_trace(s->ctx, "ae_append", NULL);
return RAFT_OK;
}
}