feat(raft-commit-safety): raft_advance_commit Figure-8 current-term rule (tasks 3.1-3.3, 6.1)

AdvanceCommitIndex (raft.tla:259/270, closes G4): leader-only (-EPERM); Max(agreeIndexes)
= highest N with a quorum ((n_peers/2)+1) of match_index>=N, self counted as log_len;
scan log_len downward. CURRENT-TERM RULE: advance commit_index to N only when
on_get_entry_term(N)==s->term, else HOLD (Figure-8 safety — never commit a previous-term
entry on replication count alone; a later current-term commit indirectly commits it).
Monotonic; fires on_commit_advanced once per strict advance; commit_index<=log_len.

3.3: raft_on_append_entries_reply left untouched — verified it never advances commit
(separation, Req 2.6). 6.1: sim commit-driver helper raft_sim_advance_commit added.

Req 6.3 two-phase indirect-commit transition covered end-to-end by task 7.1 (sim).
ASAN green (230 tests).
This commit is contained in:
Rongsong Shen 2026-06-21 23:27:12 +00:00
parent 3ba6f03a9e
commit 195f660f9a
5 changed files with 350 additions and 4 deletions

View file

@ -687,6 +687,60 @@ int raft_on_append_entries_request(raft_state_t *s, int leader_idx,
}
}
int raft_advance_commit(raft_state_t *s)
{
/* Leader-only (raft.tla AdvanceCommitIndex is enabled only at the leader).
* Off the leader: -EPERM, no change, no callback. */
if (s->role != RAFT_LEADER)
return -EPERM;
/* Compute Max(agreeIndexes): the highest index N (1..log_len) for which a
* QUORUM of the cluster has match_index >= N. Self is treated as having all
* its own entries (match_index == log_len), so it counts toward every
* candidate N <= log_len. Scan from log_len downward and stop at the first N
* that reaches quorum (that N is the maximum, since lower indexes are
* implied by every node that agrees on a higher one). (raft.tla 259/262.) */
int quorum = (s->n_peers / 2) + 1;
uint64_t agreed = 0; /* 0 = no index reached quorum */
for (uint64_t n = s->log_len; n >= 1; n--) {
int count = 1; /* self always has index n (n <= log_len) */
for (int j = 0; j < s->n_peers; j++) {
if (j == s->self_idx)
continue;
if (s->match_index[j] >= n)
count++;
}
if (count >= quorum) {
agreed = n;
break;
}
}
/* No index reached quorum → nothing to commit. */
if (agreed == 0)
return RAFT_OK;
/* Current-term Figure-8 rule (raft.tla 270): advance commit ONLY when the
* entry at the agreed index is from the CURRENT term. A leader must never
* commit a previous-term entry on replication count alone — doing so risks
* the Figure-8 safety violation where a committed entry is later
* overwritten. */
if (s->cbs->on_get_entry_term(s->ctx, agreed) != s->term)
return RAFT_OK;
/* Monotonic: never lower commit_index; only a strict advance fires the
* callback (and exactly once). */
if (agreed <= s->commit_index)
return RAFT_OK;
s->commit_index = agreed;
s->cbs->on_commit_advanced(s->ctx, s->commit_index);
if (s->cbs->on_trace)
s->cbs->on_trace(s->ctx, "commit_advanced", NULL);
return RAFT_OK;
}
int raft_on_append_entries_reply(raft_state_t *s, int peer_idx, int success,
uint64_t match_index, uint64_t reply_term)
{

View file

@ -565,6 +565,18 @@ void raft_sim_partition(raft_sim_cluster_t *c, int node_idx, int isolated);
*/
int raft_sim_check_log_matching(const raft_sim_cluster_t *c);
/*
* raft_sim_advance_commit: drive raft_advance_commit on node_idx (task 6.1).
*
* Recomputes the node's commit index from its current match_index[] / log_len /
* term (the leader-only AdvanceCommitIndex + current-term Figure-8 rule). On a
* strict advance the node's on_commit_advanced fires, updating the per-node
* commit_index mirror and commit_advanced_count. No-op if node_idx is out of
* range. Off-leader nodes return -EPERM internally (the return is discarded;
* tests read the per-node mirror).
*/
void raft_sim_advance_commit(raft_sim_cluster_t *c, int node_idx);
/*
* raft_sim_restart_node: model a process restart of node_idx (Req 8.3).
*

View file

@ -389,6 +389,17 @@ void raft_sim_partition(raft_sim_cluster_t *c, int node_idx, int isolated)
c->nodes[node_idx].partition = isolated;
}
void raft_sim_advance_commit(raft_sim_cluster_t *c, int node_idx)
{
if (node_idx < 0 || node_idx >= c->n)
return;
/* Library decides whether the commit index advanced; on a strict advance the
* node's on_commit_advanced (sim_on_commit_advanced) updates commit_index /
* commit_advanced_count. The -EPERM off-leader return is intentionally
* discarded — tests read the per-node mirror, not this status. */
(void)raft_advance_commit(&c->nodes[node_idx].state);
}
/* Two log entries are identical iff their term, length, and payload bytes
* (over the recorded length) all match. */
static int sim_entry_identical(const raft_sim_entry_t *a,