From 6b5b201a64b88de4b0393d096021f80e1cdf46cc Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sun, 21 Jun 2026 14:15:15 +0000 Subject: [PATCH] =?UTF-8?q?feat(raft-log-replication):=20header=20contract?= =?UTF-8?q?=20=E2=80=94=20log=20model,=20callbacks,=20entry=20points=20(ta?= =?UTF-8?q?sks=201.1-1.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend src/lib/raft/raft.h for the TLA+ log half (raft.tla logVars/leaderVars): - raft_state_t: log_len, next_index[], match_index[] (no entry bytes stored — terms via on_get_entry_term, bytes in the adapter); raft_config_t.initial_log_len. - raft_callbacks_t: 4 REQUIRED log callbacks on_persist_entry, on_get_entry_term, on_send_append_entries, on_truncate_from. - Entry points + raft_append_result_t: raft_log_append (ClientRequest), raft_send_append_entries (AppendEntries emit), raft_on_append_entries_request (Handle + UpdateTerm-first), raft_on_append_entries_reply. Concretized on_send_append_entries's varargs "entries handle" placeholder to an explicit n_entries (0/1); design.md updated to match. Header compiles clean with and without XCE_TESTING (-Wall -Wextra -fsyntax-only). --- .kiro/specs/raft-log-replication/design.md | 7 +- .kiro/specs/raft-log-replication/tasks.md | 7 +- src/lib/raft/raft.h | 103 ++++++++++++++++++++- 3 files changed, 111 insertions(+), 6 deletions(-) diff --git a/.kiro/specs/raft-log-replication/design.md b/.kiro/specs/raft-log-replication/design.md index 3259c2ee..ee78fbf1 100644 --- a/.kiro/specs/raft-log-replication/design.md +++ b/.kiro/specs/raft-log-replication/design.md @@ -338,10 +338,13 @@ int on_persist_entry(void *ctx, uint64_t index, uint64_t term, uint64_t on_get_entry_term(void *ctx, uint64_t index); /* REQUIRED: transmit one AppendEntries to peer_idx with prevLog{Index,Term}, - * leaderCommit, and an entries handle. */ + * leaderCommit, and n_entries (0 = heartbeat, 1 = one entry). The library holds + * no entry bytes, so when n_entries==1 the adapter sources the single entry at + * prev_log_index+1 from its own log. [Concretizes the original varargs "entries + * handle" placeholder; impl 2026-06-21.] */ int on_send_append_entries(void *ctx, int peer_idx, uint64_t prev_log_index, uint64_t prev_log_term, - uint64_t leader_commit /*, entries handle */ ...); + uint64_t leader_commit, int n_entries); /* REQUIRED: delete local entries at index..end. */ int on_truncate_from(void *ctx, uint64_t index); diff --git a/.kiro/specs/raft-log-replication/tasks.md b/.kiro/specs/raft-log-replication/tasks.md index 66c9fcfe..09ffa2eb 100644 --- a/.kiro/specs/raft-log-replication/tasks.md +++ b/.kiro/specs/raft-log-replication/tasks.md @@ -6,14 +6,14 @@ ## 1. Header contract: log model, callbacks, entry points -- [ ] 1.1 Extend `raft.h` with the log model state and config restore field +- [x] 1.1 Extend `raft.h` with the log model state and config restore field - Add `uint64_t log_len;`, `uint64_t next_index[RAFT_MAX_PEERS];`, `uint64_t match_index[RAFT_MAX_PEERS];` to `raft_state_t` - Add `uint64_t initial_log_len;` to `raft_config_t` (restored WAL length at boot, default 0; library never reads storage) - Observable completion: `raft.h` compiles; `sizeof(raft_state_t)` grows by the new fixed fields; no entry-byte storage added - _Requirements: 1.1, 1.2, 3.1_ - _Boundary: raft.h_ -- [ ] 1.2 Add the four new REQUIRED callbacks to `raft_callbacks_t` (verbatim contract) +- [x] 1.2 Add the four new REQUIRED callbacks to `raft_callbacks_t` (verbatim contract) - Declare `on_persist_entry`, `on_get_entry_term`, `on_send_append_entries`, `on_truncate_from` with the exact signatures from design.md / brief - Document each as REQUIRED and the `on_get_entry_term` → `0`-means-absent convention - Observable completion: the four function-pointer members exist in `raft_callbacks_t` with the verbatim signatures; header doc comments match the Authoritative shared contract @@ -21,7 +21,8 @@ - _Boundary: raft.h_ - _Depends: 1.1_ -- [ ] 1.3 Declare the three new entry points and `raft_append_result_t` +- [x] 1.3 Declare the three new entry points and `raft_append_result_t` + - NOTE (impl 2026-06-21): `on_send_append_entries`'s design "entries handle `...`" varargs placeholder was concretized to an explicit `int n_entries` (0=heartbeat, 1=one entry at `prev_log_index+1`; adapter sources the bytes since the library holds none). design.md §"Seam — raft_callbacks_t additions" updated to match. Downstream `raft-commit-safety` / `raft-sim-conformance` consume this concrete shape. - Declare `raft_append_result_t`; declare `raft_log_append`, `raft_send_append_entries`, `raft_on_append_entries_request`, `raft_on_append_entries_reply` with the design.md signatures - Observable completion: header declares all new entry points; a stub TU including `raft.h` compiles - _Requirements: 2.1, 3.3, 4.7, 5.1_ diff --git a/src/lib/raft/raft.h b/src/lib/raft/raft.h index e020d840..8a86b70a 100644 --- a/src/lib/raft/raft.h +++ b/src/lib/raft/raft.h @@ -44,6 +44,15 @@ typedef struct { int pre_vote_in_progress; /* 1 while a pre-vote round is active */ int pre_votes_granted; /* grants collected this round (incl. self) */ int pre_vote_replies[RAFT_MAX_PEERS]; /* per-peer replied flag; avoids double-count */ + /* ---- Log replication state (raft-log-replication, raft.tla logVars/leaderVars) ---- + * The library stores NO entry bytes and NO per-entry term array — entry terms + * are obtained via on_get_entry_term; bytes live in the adapter/simulator. This + * keeps raft_state_t fixed-size and the core pure (same discipline as not + * storing (term, voted_for) bytes). Index is 1-based (raft.tla) and maps 1:1 + * to the WAL LSN. */ + uint64_t log_len; /* highest local log index (0 = empty) */ + uint64_t next_index[RAFT_MAX_PEERS]; /* leader: next index to send each peer */ + uint64_t match_index[RAFT_MAX_PEERS]; /* leader: highest index known replicated to each peer */ /* set by raft_init — needed by the state machine to fire callbacks */ const raft_callbacks_t *cbs; void *ctx; @@ -69,7 +78,8 @@ typedef struct { * REQUIRED callbacks (must be non-NULL at raft_init): * on_send_vote_request, on_become_leader, on_become_follower, * on_persist, on_schedule_timeout, on_cancel_timeout, - * on_start_election_after_delay + * on_start_election_after_delay, + * on_persist_entry, on_get_entry_term, on_send_append_entries, on_truncate_from * * OPTIONAL callbacks (may be NULL): * on_vote_eligible — if NULL, all Raft-eligible candidates are granted @@ -109,6 +119,34 @@ typedef struct raft_callbacks_t { void (*on_cancel_timeout)(void *ctx); /* REQUIRED: Request delayed election start after RDHUP (priority-based jitter) */ void (*on_start_election_after_delay)(void *ctx, uint32_t delay_ms); + + /* ---- Log replication callbacks (raft-log-replication) ---- + * REQUIRED (all four non-NULL at raft_init). The library owns the protocol + * decisions; these perform the I/O. The library stores no entry bytes, so it + * reads entry terms only via on_get_entry_term and writes/truncates only via + * these callbacks. */ + + /* REQUIRED: durably append/overwrite the log entry at 1-based `index` with + * `term` (persist-before-act: called before an append/accept is reported). */ + int (*on_persist_entry)(void *ctx, uint64_t index, uint64_t term, + const void *entry, size_t len); + + /* REQUIRED: term of the local entry at 1-based `index` (0 if absent). */ + uint64_t (*on_get_entry_term)(void *ctx, uint64_t index); + + /* REQUIRED: transmit one AppendEntries to peer_idx with prevLog{Index,Term} + * and leader_commit. `n_entries` is 0 (heartbeat) or 1 (this spec, like + * raft.tla); when 1 the adapter sources the single entry at prev_log_index+1 + * from its own log (the library holds no entry bytes). [Concretizes the + * design's varargs "entries handle" placeholder into an explicit count.] */ + int (*on_send_append_entries)(void *ctx, int peer_idx, + uint64_t prev_log_index, + uint64_t prev_log_term, + uint64_t leader_commit, int n_entries); + + /* REQUIRED: delete local entries at `index`..end. */ + int (*on_truncate_from)(void *ctx, uint64_t index); + /* OPTIONAL: Trace callback for observability. * event: short string literal, e.g. "vote_granted", "became_leader" * detail: formatted human-readable context string @@ -136,6 +174,7 @@ typedef struct { int my_priority; /* priority index for RDHUP jitter (0 = highest) */ uint64_t initial_term; /* term restored from disk; 0 on first boot */ int initial_voted_for; /* voted_for restored from disk; -1 if none */ + uint64_t initial_log_len; /* WAL length restored from disk; 0 on first boot (library never reads storage) */ const raft_callbacks_t *cbs; /* all callbacks; required callbacks must be non-NULL */ void *ctx; /* opaque caller context passed to every callback */ } raft_config_t; @@ -258,6 +297,68 @@ int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term); */ int raft_on_pre_vote_reply(raft_state_t *s, int peer_idx, int granted); +/* ---------- Log replication (raft-log-replication) ---------- + * + * The library owns the log protocol decisions (append, AppendEntries logOk + + * conflict truncation, UpdateTerm-on-any-RPC) and drives I/O through the four + * log callbacks. Index is 1-based (raft.tla) == WAL LSN. Commit-index advance + * and (lastLogTerm,lastLogIndex) vote freshness are NOT here — they are + * raft-commit-safety. */ + +/* Result of handling an AppendEntries request — the adapter replies with this. */ +typedef struct { + uint64_t reply_term; /* current term to return to the leader */ + int success; /* 1 = accepted, 0 = rejected */ + uint64_t match_index; /* on success: prev_log_index + entries_len; else 0 */ +} raft_append_result_t; + +/* + * raft_log_append: leader appends one client entry (raft.tla ClientRequest, 246). + * Stamps the entry with s->term, assigns index log_len+1, persists it via + * on_persist_entry BEFORE returning success (persist-before-act). Does NOT + * advance any commit index (owned by raft-commit-safety). + * out_index receives the assigned 1-based index (== WAL LSN) on success. + * Returns RAFT_OK; -EPERM if role != RAFT_LEADER; propagates a non-zero + * on_persist_entry return on persist failure (no log mutation on failure). + */ +int raft_log_append(raft_state_t *s, const void *entry, size_t len, + uint64_t *out_index); + +/* + * raft_send_append_entries: emit one AppendEntries to peer_idx (raft.tla + * AppendEntries, 204). Computes prev_log_index = next_index[peer]-1 and + * prev_log_term via on_get_entry_term, sends at most one entry (n_entries 0/1), + * via on_send_append_entries. Leader-only (R3.5); leader_commit is opaque. + * Returns RAFT_OK. + */ +int raft_send_append_entries(raft_state_t *s, int peer_idx, + uint64_t leader_commit); + +/* + * raft_on_append_entries_request: handle an incoming AppendEntries / heartbeat + * (raft.tla HandleAppendEntriesRequest 327 + UpdateTerm 406). Step order is + * fixed: UpdateTerm first (G5, every RPC incl. heartbeat) → logOk (G2) → + * reject / candidate-step-down / accept (already-have / conflict-truncate (G3) / + * append). entries_len is 0 (heartbeat) or 1. Fills *out. leader_commit is + * accepted but NOT acted on here (raft-commit-safety). Returns 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); + +/* + * raft_on_append_entries_reply: process a follower's AppendEntries response + * (raft.tla HandleAppendEntriesResponse 393). Applied only when + * reply_term == s->term (stale-term drop, 415). success: next_index[peer] = + * match_index+1, match_index[peer] = match_index. failure: next_index[peer] = + * Max(next_index[peer]-1, 1). Does NOT advance commit. Returns 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); + /* Fills snap with current state. Caller must hold lock. */ void raft_snapshot(const raft_state_t *s, raft_snapshot_t *snap);