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:
parent
3ba6f03a9e
commit
195f660f9a
5 changed files with 350 additions and 4 deletions
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue