raft_io/message.rs
1//! The RPC messages nodes exchange.
2//!
3//! [`RequestVote`] and its [pre-vote](PreVote) variant drive elections;
4//! [`AppendEntries`] replicates the log and doubles as the leader's heartbeat;
5//! [`InstallSnapshot`] catches up a far-behind follower; [`TimeoutNow`] hands off
6//! leadership; and [`ReadProbe`] confirms leadership for a linearizable read.
7//! Each has a reply. The protocol core never sends these itself — it emits
8//! [`Action::Send`](crate::Action::Send) carrying a [`Message`], and the caller
9//! delivers it through a [`RaftTransport`](crate::RaftTransport). Keeping the
10//! messages as plain data is what lets a test harness route them in memory and a
11//! framing layer put them on a wire.
12
13use crate::types::{Index, LogEntry, NodeId, Snapshot, Term};
14
15/// A candidate's request for a vote in an election.
16///
17/// Sent by a [`Candidate`](crate::Role::Candidate) to every peer when it starts
18/// an election. A recipient grants its vote only if it has not already voted in
19/// this term and the candidate's log is at least as up to date as its own — the
20/// election restriction that keeps a node missing committed entries from
21/// becoming leader.
22///
23/// # Examples
24///
25/// ```
26/// use raft_io::RequestVote;
27///
28/// let rv = RequestVote {
29/// term: 4, candidate: 2, last_log_index: 9, last_log_term: 3, force: false,
30/// };
31/// assert_eq!(rv.candidate, 2);
32/// ```
33#[derive(Clone, Debug, PartialEq, Eq)]
34#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
35pub struct RequestVote {
36 /// The candidate's term.
37 pub term: Term,
38 /// The candidate requesting the vote.
39 pub candidate: NodeId,
40 /// Index of the candidate's last log entry.
41 pub last_log_index: Index,
42 /// Term of the candidate's last log entry.
43 pub last_log_term: Term,
44 /// A forced election, requested by the current leader as part of a
45 /// [leadership transfer](crate::Event::TransferLeadership). A recipient
46 /// honours it even within the leader-stickiness window, so the hand-off is
47 /// not blocked by its own loyalty to the departing leader.
48 pub force: bool,
49}
50
51/// A candidate's *pre-vote* probe, sent before it commits to a real election.
52///
53/// Pre-voting (Raft thesis §9.6) is a disruption guard. Before a node increments
54/// its term and campaigns for real, it asks its peers whether they *would* vote
55/// for it at the next term — without bumping anyone's term. A peer grants only if
56/// it has no active leader and the candidate's log is up to date (the same
57/// election restriction a real vote applies). The candidate runs a real
58/// [`RequestVote`] election only once a quorum of pre-votes says yes.
59///
60/// The point is that a node partitioned away from the cluster never collects a
61/// pre-vote majority, so it never inflates its term. When it rejoins it does not
62/// force the established leader to step down, which is the disruption a plain
63/// election would cause. Unlike [`RequestVote`], a pre-vote changes no persistent
64/// state on either side.
65///
66/// # Examples
67///
68/// ```
69/// use raft_io::PreVote;
70///
71/// // The `term` is the *hypothetical* term the candidate would campaign at —
72/// // one past its current term — not a term it has adopted.
73/// let pv = PreVote { term: 5, candidate: 2, last_log_index: 9, last_log_term: 3 };
74/// assert_eq!(pv.candidate, 2);
75/// ```
76#[derive(Clone, Debug, PartialEq, Eq)]
77#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
78pub struct PreVote {
79 /// The hypothetical term the candidate would campaign at — one past its
80 /// current term. It is *not* a term the candidate has adopted; a recipient
81 /// neither stores it nor steps down for it.
82 pub term: Term,
83 /// The candidate seeking the pre-vote.
84 pub candidate: NodeId,
85 /// Index of the candidate's last log entry.
86 pub last_log_index: Index,
87 /// Term of the candidate's last log entry.
88 pub last_log_term: Term,
89}
90
91/// A peer's response to a [`PreVote`].
92///
93/// `term` is the responder's *current* term, unchanged by the pre-vote. If it
94/// exceeds the pre-candidate's term, the pre-candidate has fallen behind and
95/// abandons the round; otherwise `vote_granted` tells it whether this peer would
96/// support a real election. None of this touches persistent state.
97///
98/// # Examples
99///
100/// ```
101/// use raft_io::PreVoteReply;
102///
103/// let reply = PreVoteReply { term: 4, vote_granted: true, from: 3 };
104/// assert!(reply.vote_granted);
105/// ```
106#[derive(Clone, Debug, PartialEq, Eq)]
107#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
108pub struct PreVoteReply {
109 /// The responder's current term, unchanged by the pre-vote.
110 pub term: Term,
111 /// Whether the responder would grant a real vote under these conditions.
112 pub vote_granted: bool,
113 /// The node that produced this reply.
114 pub from: NodeId,
115}
116
117/// A peer's response to a [`RequestVote`].
118///
119/// `from` names the responder so the candidate can count distinct votes without
120/// depending on transport-level addressing. If `term` exceeds the candidate's
121/// term, the candidate steps down instead of counting the vote.
122///
123/// # Examples
124///
125/// ```
126/// use raft_io::RequestVoteReply;
127///
128/// let reply = RequestVoteReply { term: 4, vote_granted: true, from: 3 };
129/// assert!(reply.vote_granted);
130/// ```
131#[derive(Clone, Debug, PartialEq, Eq)]
132#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
133pub struct RequestVoteReply {
134 /// The responder's current term, for the candidate to update itself.
135 pub term: Term,
136 /// Whether the responder granted its vote.
137 pub vote_granted: bool,
138 /// The node that produced this reply.
139 pub from: NodeId,
140}
141
142/// The leader's replicate-and-heartbeat RPC.
143///
144/// The leader sends this to each follower. With an empty
145/// [`entries`](AppendEntries::entries) list it is a pure heartbeat that asserts
146/// leadership and resets the follower's election timer; with entries it
147/// replicates the log. The `prev_log_index` / `prev_log_term`
148/// pair lets the follower verify its log matches the leader's up to that point
149/// before accepting anything new.
150///
151/// # Examples
152///
153/// ```
154/// use raft_io::AppendEntries;
155///
156/// // An empty heartbeat for term 4 from node 1.
157/// let hb = AppendEntries {
158/// term: 4,
159/// leader: 1,
160/// prev_log_index: 9,
161/// prev_log_term: 3,
162/// entries: Vec::new(),
163/// leader_commit: 7,
164/// };
165/// assert!(hb.entries.is_empty());
166/// ```
167#[derive(Clone, Debug, PartialEq, Eq)]
168#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
169pub struct AppendEntries {
170 /// The leader's term.
171 pub term: Term,
172 /// The leader sending the RPC, so followers can record it.
173 pub leader: NodeId,
174 /// Index of the log entry immediately preceding the new ones.
175 pub prev_log_index: Index,
176 /// Term of the entry at `prev_log_index`.
177 pub prev_log_term: Term,
178 /// Entries to store, in order; empty for a pure heartbeat.
179 pub entries: Vec<LogEntry>,
180 /// The leader's commit index, so followers can advance their own.
181 pub leader_commit: Index,
182}
183
184/// A follower's response to an [`AppendEntries`].
185///
186/// `success` is `true` when the follower's log matched at `prev_log_index` and
187/// it accepted the RPC. `match_index` reports the highest log index the
188/// follower now agrees on, which the leader uses to track replication progress.
189///
190/// On a rejection, the `conflict_*` fields let the leader skip the follower's
191/// `next_index` back by a whole term in one round trip instead of decrementing
192/// one entry at a time (the fast-backtracking optimisation from the Raft thesis,
193/// §5.3). They are `0` on success and ignored.
194///
195/// # Examples
196///
197/// ```
198/// use raft_io::AppendEntriesReply;
199///
200/// let ok = AppendEntriesReply {
201/// term: 4,
202/// success: true,
203/// from: 2,
204/// match_index: 9,
205/// conflict_index: 0,
206/// conflict_term: 0,
207/// };
208/// assert!(ok.success);
209/// ```
210#[derive(Clone, Debug, PartialEq, Eq)]
211#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
212pub struct AppendEntriesReply {
213 /// The follower's current term, for the leader to update itself.
214 pub term: Term,
215 /// Whether the follower accepted the RPC.
216 pub success: bool,
217 /// The node that produced this reply.
218 pub from: NodeId,
219 /// Highest log index the follower now matches with the leader.
220 pub match_index: Index,
221 /// On rejection, the index the leader should probe next (the follower's
222 /// first index of `conflict_term`, or its log length plus one when the log
223 /// is simply too short). `0` on success.
224 pub conflict_index: Index,
225 /// On rejection, the term of the follower's entry at `prev_log_index`, or
226 /// `0` when the follower has no entry there. `0` on success.
227 pub conflict_term: Term,
228}
229
230/// A leader's transfer of a [`Snapshot`] to a follower too far behind to
231/// replicate entry by entry.
232///
233/// When a follower's next required entry has already been compacted out of the
234/// leader's log, the leader sends this instead of an [`AppendEntries`]. The
235/// follower installs the snapshot — replacing its state through
236/// `snapshot.index` — then resumes normal replication from the tail.
237///
238/// # Examples
239///
240/// ```
241/// use raft_io::{InstallSnapshot, Snapshot};
242///
243/// let rpc = InstallSnapshot { term: 5, leader: 1, snapshot: Snapshot::new(10, 3, vec![]) };
244/// assert_eq!(rpc.snapshot.index, 10);
245/// ```
246#[derive(Clone, Debug, PartialEq, Eq)]
247#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
248pub struct InstallSnapshot {
249 /// The leader's term.
250 pub term: Term,
251 /// The leader sending the snapshot.
252 pub leader: NodeId,
253 /// The snapshot to install.
254 pub snapshot: Snapshot,
255}
256
257/// A follower's response to an [`InstallSnapshot`].
258///
259/// `last_index` is the snapshot's index the follower has now installed, which
260/// the leader uses to advance that follower's replication progress.
261///
262/// # Examples
263///
264/// ```
265/// use raft_io::InstallSnapshotReply;
266///
267/// let reply = InstallSnapshotReply { term: 5, from: 2, last_index: 10 };
268/// assert_eq!(reply.last_index, 10);
269/// ```
270#[derive(Clone, Debug, PartialEq, Eq)]
271#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
272pub struct InstallSnapshotReply {
273 /// The follower's current term, for the leader to update itself.
274 pub term: Term,
275 /// The node that produced this reply.
276 pub from: NodeId,
277 /// The snapshot index the follower has installed.
278 pub last_index: Index,
279}
280
281/// A leader's signal telling `target` to start an election immediately.
282///
283/// Sent during a [leadership transfer](crate::Event::TransferLeadership): once the
284/// target is fully caught up, the leader sends this so the target campaigns at
285/// once instead of waiting out its election timeout, taking over with minimal
286/// disruption.
287///
288/// # Examples
289///
290/// ```
291/// use raft_io::TimeoutNow;
292///
293/// let rpc = TimeoutNow { term: 5, leader: 1 };
294/// assert_eq!(rpc.leader, 1);
295/// ```
296#[derive(Clone, Debug, PartialEq, Eq)]
297#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
298pub struct TimeoutNow {
299 /// The leader's term.
300 pub term: Term,
301 /// The leader handing off leadership.
302 pub leader: NodeId,
303}
304
305/// A leader's read-confirmation probe, the network half of a linearizable read.
306///
307/// To serve a read that reflects every previously-committed write, a leader must
308/// confirm it is *still* the leader at the moment of the read — a deposed leader
309/// that has not yet learned of its fall could otherwise answer with stale state
310/// (the ReadIndex protocol, Raft thesis §6.4). The leader sends `ReadProbe` to
311/// every other voter and waits for a quorum of [`ReadProbeReply`]s carrying the
312/// matching `seq` before releasing the read. The probe carries no log data and
313/// changes no state; it is a pure leadership check.
314///
315/// # Examples
316///
317/// ```
318/// use raft_io::ReadProbe;
319///
320/// let probe = ReadProbe { term: 5, leader: 1, seq: 42 };
321/// assert_eq!(probe.seq, 42);
322/// ```
323#[derive(Clone, Debug, PartialEq, Eq)]
324#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
325pub struct ReadProbe {
326 /// The leader's term.
327 pub term: Term,
328 /// The leader issuing the probe.
329 pub leader: NodeId,
330 /// A monotonic confirmation-round number, echoed back in the reply so the
331 /// leader can tell which round a reply confirms.
332 pub seq: u64,
333}
334
335/// A follower's acknowledgement of a [`ReadProbe`].
336///
337/// A follower replies iff it still recognises the prober as leader for its term.
338/// `seq` echoes the probe so the leader counts replies for the right round; if
339/// `term` exceeds the leader's, the leader has been deposed and steps down rather
340/// than counting the reply.
341///
342/// # Examples
343///
344/// ```
345/// use raft_io::ReadProbeReply;
346///
347/// let reply = ReadProbeReply { term: 5, from: 2, seq: 42 };
348/// assert_eq!(reply.from, 2);
349/// ```
350#[derive(Clone, Debug, PartialEq, Eq)]
351#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
352pub struct ReadProbeReply {
353 /// The responder's current term, for the leader to update itself.
354 pub term: Term,
355 /// The node that produced this reply.
356 pub from: NodeId,
357 /// The probe round this reply confirms (echoed from the [`ReadProbe`]).
358 pub seq: u64,
359}
360
361/// Any message a node can send or receive.
362///
363/// Wraps the RPCs and their replies. The enum is
364/// [`#[non_exhaustive]`](https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute):
365/// future versions may add variants, so a `match` over a `Message` must include
366/// a wildcard arm.
367///
368/// # Examples
369///
370/// ```
371/// use raft_io::{Message, RequestVote};
372///
373/// let msg = Message::RequestVote(RequestVote {
374/// term: 1,
375/// candidate: 1,
376/// last_log_index: 0,
377/// last_log_term: 0,
378/// force: false,
379/// });
380/// match msg {
381/// Message::RequestVote(rv) => assert_eq!(rv.term, 1),
382/// _ => unreachable!(),
383/// }
384/// ```
385#[non_exhaustive]
386#[derive(Clone, Debug, PartialEq, Eq)]
387#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
388pub enum Message {
389 /// A candidate is probing for support before a real election.
390 PreVote(PreVote),
391 /// A peer is answering a pre-vote probe.
392 PreVoteReply(PreVoteReply),
393 /// A candidate is asking for a vote.
394 RequestVote(RequestVote),
395 /// A peer is answering a vote request.
396 RequestVoteReply(RequestVoteReply),
397 /// A leader is replicating entries or sending a heartbeat.
398 AppendEntries(AppendEntries),
399 /// A follower is answering an append.
400 AppendEntriesReply(AppendEntriesReply),
401 /// A leader is shipping a snapshot to a far-behind follower.
402 InstallSnapshot(InstallSnapshot),
403 /// A follower is acknowledging an installed snapshot.
404 InstallSnapshotReply(InstallSnapshotReply),
405 /// A leader is handing off leadership, telling the target to campaign now.
406 TimeoutNow(TimeoutNow),
407 /// A leader is confirming its leadership to serve a linearizable read.
408 ReadProbe(ReadProbe),
409 /// A follower is acknowledging a read-confirmation probe.
410 ReadProbeReply(ReadProbeReply),
411}
412
413impl Message {
414 /// Returns the term carried by the message, whatever its variant.
415 ///
416 /// The protocol checks the term of every inbound message first — a higher
417 /// term forces the node to step down — so a single accessor avoids matching
418 /// at each call site.
419 ///
420 /// # Examples
421 ///
422 /// ```
423 /// use raft_io::{AppendEntriesReply, Message};
424 ///
425 /// let m = Message::AppendEntriesReply(AppendEntriesReply {
426 /// term: 5,
427 /// success: false,
428 /// from: 2,
429 /// match_index: 0,
430 /// conflict_index: 1,
431 /// conflict_term: 0,
432 /// });
433 /// assert_eq!(m.term(), 5);
434 /// ```
435 #[inline]
436 #[must_use]
437 pub fn term(&self) -> Term {
438 match self {
439 Self::PreVote(m) => m.term,
440 Self::PreVoteReply(m) => m.term,
441 Self::RequestVote(m) => m.term,
442 Self::RequestVoteReply(m) => m.term,
443 Self::AppendEntries(m) => m.term,
444 Self::AppendEntriesReply(m) => m.term,
445 Self::InstallSnapshot(m) => m.term,
446 Self::InstallSnapshotReply(m) => m.term,
447 Self::TimeoutNow(m) => m.term,
448 Self::ReadProbe(m) => m.term,
449 Self::ReadProbeReply(m) => m.term,
450 }
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn test_message_term_reads_each_variant() {
460 assert_eq!(
461 Message::RequestVote(RequestVote {
462 term: 1,
463 candidate: 1,
464 last_log_index: 0,
465 last_log_term: 0,
466 force: false,
467 })
468 .term(),
469 1
470 );
471 assert_eq!(
472 Message::RequestVoteReply(RequestVoteReply {
473 term: 2,
474 vote_granted: true,
475 from: 1
476 })
477 .term(),
478 2
479 );
480 assert_eq!(
481 Message::AppendEntries(AppendEntries {
482 term: 3,
483 leader: 1,
484 prev_log_index: 0,
485 prev_log_term: 0,
486 entries: Vec::new(),
487 leader_commit: 0,
488 })
489 .term(),
490 3
491 );
492 assert_eq!(
493 Message::AppendEntriesReply(AppendEntriesReply {
494 term: 4,
495 success: true,
496 from: 1,
497 match_index: 0,
498 conflict_index: 0,
499 conflict_term: 0,
500 })
501 .term(),
502 4
503 );
504 }
505}