Skip to main content

raft_io/
types.rs

1//! Core value types shared across the protocol.
2//!
3//! These are deliberately plain: a [`NodeId`], the monotonic [`Term`] and
4//! [`Index`] counters, the [`Role`] a node currently plays, a single
5//! [`LogEntry`], and the [`HardState`] that Raft requires to survive a restart.
6//! They carry no behaviour beyond construction and small accessors, which keeps
7//! them cheap to copy and trivial to serialize once framing lands.
8
9/// Identifier for a node in the cluster.
10///
11/// Identifiers are opaque to the protocol; any scheme is fine as long as each
12/// node in a cluster has a distinct, stable value. A plain integer keeps the
13/// common case allocation-free and `Copy`.
14pub type NodeId = u64;
15
16/// A Raft term: a monotonically increasing logical clock.
17///
18/// Terms partition time into epochs, each beginning with an election. Every
19/// message carries the sender's term; a node that sees a higher term steps down
20/// and adopts it. Term `0` is the initial value before any election.
21pub type Term = u64;
22
23/// Position of an entry in the replicated log.
24///
25/// Indices are 1-based: the first appended entry has index `1`, and index `0`
26/// is the sentinel meaning "before the first entry" (with term `0`). Using `0`
27/// as a sentinel lets the `prev_log_index` consistency check at the head of the
28/// log fall out without a special case.
29pub type Index = u64;
30
31/// The role a node currently plays in the consensus protocol.
32///
33/// A node is always in exactly one role. It starts as a [`Follower`], may
34/// become a [`Candidate`] when it stops hearing from a leader, and becomes a
35/// [`Leader`] if it wins an election.
36///
37/// [`Follower`]: Role::Follower
38/// [`Candidate`]: Role::Candidate
39/// [`Leader`]: Role::Leader
40///
41/// # Examples
42///
43/// ```
44/// use raft_io::{RaftConfig, RaftNode, Role};
45///
46/// let node = RaftNode::new(RaftConfig::single(1));
47/// assert_eq!(node.role(), Role::Follower);
48/// ```
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50pub enum Role {
51    /// Passive replica: serves the leader and votes in elections.
52    Follower,
53    /// Standing for election in the current term, collecting votes.
54    Candidate,
55    /// Won the election for the current term; drives replication.
56    Leader,
57}
58
59/// What a [`LogEntry`] carries.
60///
61/// Most entries are [`Normal`](EntryKind::Normal) application commands. A
62/// [`Config`](EntryKind::Config) entry instead carries a cluster configuration —
63/// the voting membership — and drives a membership change; its
64/// [`command`](LogEntry::command) bytes encode the new member set rather than an
65/// application command, so the protocol interprets them and the application does
66/// not apply them.
67///
68/// # Examples
69///
70/// ```
71/// use raft_io::{EntryKind, LogEntry};
72///
73/// assert_eq!(LogEntry::new(1, 1, vec![]).kind, EntryKind::Normal);
74/// assert_eq!(LogEntry::config(1, 2, &[1, 2, 3]).kind, EntryKind::Config);
75/// ```
76#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
77#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
78pub enum EntryKind {
79    /// An ordinary application command, applied to the state machine.
80    #[default]
81    Normal,
82    /// A cluster-configuration change. The command bytes encode the new voting
83    /// membership; the protocol adopts it and never applies it to the state
84    /// machine.
85    Config,
86}
87
88/// A single entry in the replicated log.
89///
90/// The [`command`](LogEntry::command) is opaque bytes: for a
91/// [`Normal`](EntryKind::Normal) entry the protocol replicates and orders it but
92/// never interprets it, and the application's state machine decodes it on apply;
93/// for a [`Config`](EntryKind::Config) entry the bytes encode the new voting
94/// membership. Each entry records the [`term`](LogEntry::term) in which the
95/// leader created it and its [`index`](LogEntry::index) in the log, which
96/// together identify it uniquely.
97///
98/// # Examples
99///
100/// ```
101/// use raft_io::LogEntry;
102///
103/// let entry = LogEntry::new(2, 7, b"put k v".to_vec());
104/// assert_eq!(entry.term, 2);
105/// assert_eq!(entry.index, 7);
106/// assert_eq!(entry.command, b"put k v");
107/// ```
108#[derive(Clone, Debug, PartialEq, Eq)]
109#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
110pub struct LogEntry {
111    /// Term in which the leader created this entry.
112    pub term: Term,
113    /// 1-based position of this entry in the log.
114    pub index: Index,
115    /// Whether this is a normal command or a configuration change.
116    pub kind: EntryKind,
117    /// Opaque bytes: an application command, or an encoded member set for a
118    /// [`Config`](EntryKind::Config) entry.
119    pub command: Vec<u8>,
120}
121
122impl LogEntry {
123    /// Creates a [`Normal`](EntryKind::Normal) log entry at `index` in `term`
124    /// carrying `command`.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use raft_io::LogEntry;
130    ///
131    /// let e = LogEntry::new(1, 1, vec![0xAB]);
132    /// assert_eq!(e.command, vec![0xAB]);
133    /// ```
134    #[inline]
135    #[must_use]
136    pub fn new(term: Term, index: Index, command: Vec<u8>) -> Self {
137        Self {
138            term,
139            index,
140            kind: EntryKind::Normal,
141            command,
142        }
143    }
144
145    /// Creates a [`Config`](EntryKind::Config) log entry carrying the voting
146    /// membership `members`.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use raft_io::LogEntry;
152    ///
153    /// let e = LogEntry::config(3, 9, &[1, 2, 3]);
154    /// assert_eq!(e.members(), Some(vec![1, 2, 3]));
155    /// ```
156    #[inline]
157    #[must_use]
158    pub fn config(term: Term, index: Index, members: &[NodeId]) -> Self {
159        Self {
160            term,
161            index,
162            kind: EntryKind::Config,
163            command: encode_members(members),
164        }
165    }
166
167    /// Returns the voting membership a [`Config`](EntryKind::Config) entry
168    /// carries, or `None` for a [`Normal`](EntryKind::Normal) entry.
169    ///
170    /// # Examples
171    ///
172    /// ```
173    /// use raft_io::LogEntry;
174    ///
175    /// assert_eq!(LogEntry::new(1, 1, vec![]).members(), None);
176    /// assert_eq!(LogEntry::config(1, 2, &[7, 8]).members(), Some(vec![7, 8]));
177    /// ```
178    #[must_use]
179    pub fn members(&self) -> Option<Vec<NodeId>> {
180        match self.kind {
181            EntryKind::Normal => None,
182            EntryKind::Config => Some(decode_members(&self.command)),
183        }
184    }
185}
186
187/// Encodes a voting membership as little-endian `u64`s.
188#[must_use]
189pub(crate) fn encode_members(members: &[NodeId]) -> Vec<u8> {
190    let mut buf = Vec::with_capacity(members.len() * 8);
191    for &id in members {
192        buf.extend_from_slice(&id.to_le_bytes());
193    }
194    buf
195}
196
197/// Decodes a voting membership written by [`encode_members`]. A trailing partial
198/// chunk (only possible from corruption) is ignored.
199#[must_use]
200pub(crate) fn decode_members(bytes: &[u8]) -> Vec<NodeId> {
201    bytes
202        .chunks_exact(8)
203        .map(|c| {
204            let mut id = [0u8; 8];
205            id.copy_from_slice(c);
206            NodeId::from_le_bytes(id)
207        })
208        .collect()
209}
210
211/// The state Raft must persist before responding to any RPC.
212///
213/// Safety depends on `current_term` and `voted_for` surviving a crash: a node
214/// that forgot it had already voted in a term could vote twice and help elect
215/// two leaders. The [`RaftLog`](crate::RaftLog) stores this alongside the log
216/// entries; the in-memory [`MemoryLog`](crate::MemoryLog) keeps it in a field.
217///
218/// # Examples
219///
220/// ```
221/// use raft_io::HardState;
222///
223/// let hs = HardState::default();
224/// assert_eq!(hs.term, 0);
225/// assert_eq!(hs.voted_for, None);
226/// ```
227#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
228pub struct HardState {
229    /// The latest term the node has seen.
230    pub term: Term,
231    /// The candidate this node voted for in `term`, if any.
232    pub voted_for: Option<NodeId>,
233}
234
235/// A point-in-time capture of the application's state machine, with the log
236/// position it covers.
237///
238/// A snapshot lets the log discard the entries it subsumes (compaction) and lets
239/// a leader catch up a follower that has fallen too far behind to replicate
240/// entry by entry. [`index`](Snapshot::index) and [`term`](Snapshot::term) are
241/// the last log entry the snapshot includes — its replacement "sentinel" once
242/// the entries up to there are gone — and [`data`](Snapshot::data) is the opaque
243/// serialized state the application produces and restores. The protocol moves
244/// the bytes but never interprets them.
245///
246/// # Examples
247///
248/// ```
249/// use raft_io::Snapshot;
250///
251/// let snap = Snapshot::new(10, 3, b"serialized state".to_vec());
252/// assert_eq!(snap.index, 10);
253/// assert_eq!(snap.term, 3);
254/// ```
255#[derive(Clone, Debug, PartialEq, Eq)]
256#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
257pub struct Snapshot {
258    /// Index of the last log entry the snapshot includes.
259    pub index: Index,
260    /// Term of the last log entry the snapshot includes.
261    pub term: Term,
262    /// Voting membership in effect at [`index`](Snapshot::index).
263    ///
264    /// Carried so a node that catches up from this snapshot — its configuration
265    /// log entries having been compacted away — still knows who is in the
266    /// cluster. The node fills this in when it takes a snapshot; an application
267    /// constructing a snapshot directly with [`new`](Snapshot::new) leaves it
268    /// empty.
269    pub config: Vec<NodeId>,
270    /// Opaque serialized state machine state. The protocol never inspects it.
271    pub data: Vec<u8>,
272}
273
274impl Snapshot {
275    /// Creates a snapshot covering the log through `index` (created in `term`),
276    /// carrying serialized state `data` and an empty configuration.
277    ///
278    /// The node fills the configuration in when it takes a snapshot; use this
279    /// constructor for snapshots that do not track membership.
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// use raft_io::Snapshot;
285    ///
286    /// let snap = Snapshot::new(5, 2, vec![1, 2, 3]);
287    /// assert_eq!(snap.data, vec![1, 2, 3]);
288    /// assert!(snap.config.is_empty());
289    /// ```
290    #[inline]
291    #[must_use]
292    pub fn new(index: Index, term: Term, data: Vec<u8>) -> Self {
293        Self {
294            index,
295            term,
296            config: Vec::new(),
297            data,
298        }
299    }
300
301    /// Creates a snapshot that also records the voting membership `config` in
302    /// effect at `index`.
303    ///
304    /// # Examples
305    ///
306    /// ```
307    /// use raft_io::Snapshot;
308    ///
309    /// let snap = Snapshot::with_config(5, 2, vec![1, 2, 3], vec![0xAB]);
310    /// assert_eq!(snap.config, vec![1, 2, 3]);
311    /// ```
312    #[inline]
313    #[must_use]
314    pub fn with_config(index: Index, term: Term, config: Vec<NodeId>, data: Vec<u8>) -> Self {
315        Self {
316            index,
317            term,
318            config,
319            data,
320        }
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_log_entry_new_sets_all_fields() {
330        let e = LogEntry::new(3, 9, vec![1, 2, 3]);
331        assert_eq!(e.term, 3);
332        assert_eq!(e.index, 9);
333        assert_eq!(e.command, vec![1, 2, 3]);
334    }
335
336    #[test]
337    fn test_hard_state_default_is_term_zero_no_vote() {
338        let hs = HardState::default();
339        assert_eq!(
340            hs,
341            HardState {
342                term: 0,
343                voted_for: None
344            }
345        );
346    }
347
348    #[test]
349    fn test_role_is_copy_and_comparable() {
350        let r = Role::Leader;
351        let copy = r;
352        assert_eq!(r, copy);
353        assert_ne!(Role::Follower, Role::Candidate);
354    }
355
356    #[test]
357    fn test_normal_entry_has_no_members() {
358        let e = LogEntry::new(1, 1, b"cmd".to_vec());
359        assert_eq!(e.kind, EntryKind::Normal);
360        assert_eq!(e.members(), None);
361    }
362
363    #[test]
364    fn test_config_entry_round_trips_members() {
365        let e = LogEntry::config(3, 9, &[1, 2, 3, 99]);
366        assert_eq!(e.kind, EntryKind::Config);
367        assert_eq!(e.members(), Some(vec![1, 2, 3, 99]));
368    }
369
370    #[test]
371    fn test_empty_config_entry() {
372        assert_eq!(LogEntry::config(1, 1, &[]).members(), Some(vec![]));
373    }
374
375    #[test]
376    fn test_member_codec_round_trips() {
377        for members in [vec![], vec![0], vec![1, 2, 3], vec![u64::MAX, 0, 7]] {
378            assert_eq!(decode_members(&encode_members(&members)), members);
379        }
380    }
381
382    #[test]
383    fn test_decode_members_ignores_trailing_partial_chunk() {
384        let mut bytes = encode_members(&[5, 6]);
385        bytes.push(0xFF); // a stray byte from corruption
386        assert_eq!(decode_members(&bytes), vec![5, 6]);
387    }
388
389    #[test]
390    fn test_snapshot_with_config_carries_membership() {
391        let snap = Snapshot::with_config(5, 2, vec![1, 2, 3], vec![0xAB]);
392        assert_eq!(snap.config, vec![1, 2, 3]);
393        assert!(Snapshot::new(5, 2, vec![]).config.is_empty());
394    }
395}