Skip to main content

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}