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:
parent
a1aacc0389
commit
3c5185a5c3
3 changed files with 756 additions and 5 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue