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}