raft_io/config.rs
1//! Node configuration.
2//!
3//! [`RaftConfig`] names a node, lists its peers, and sets the timing that drives
4//! elections and heartbeats. Timing is expressed in **logical ticks**, not
5//! wall-clock time: the core counts [`Event::Tick`](crate::Event::Tick)s, and
6//! the caller decides how often to tick (every 10 ms, say). This is what keeps
7//! the core free of any clock.
8//!
9//! The common case is one call — [`RaftConfig::new`] or [`RaftConfig::single`]
10//! — with sensible defaults. The builder methods ([`election_timeout`],
11//! [`heartbeat_interval`], [`seed`]) are there when you need to tune them.
12//!
13//! [`election_timeout`]: RaftConfig::election_timeout
14//! [`heartbeat_interval`]: RaftConfig::heartbeat_interval
15//! [`seed`]: RaftConfig::seed
16
17use crate::types::NodeId;
18
19/// Default lower bound of the randomised election timeout, in ticks.
20const DEFAULT_ELECTION_MIN: u32 = 10;
21/// Default upper bound of the randomised election timeout, in ticks.
22const DEFAULT_ELECTION_MAX: u32 = 20;
23/// Default heartbeat interval, in ticks. Must be well below the election
24/// timeout so a healthy leader is never replaced.
25const DEFAULT_HEARTBEAT: u32 = 3;
26/// Default cap on entries carried by a single `AppendEntries`. Bounds message
27/// size and per-RPC work so a far-behind follower is caught up in steady chunks
28/// rather than one unbounded payload.
29const DEFAULT_MAX_BATCH: usize = 64;
30/// Default snapshot threshold: `0` disables the policy hint, so snapshots are
31/// opt-in. A node never asks the application to snapshot unless this is set.
32const DEFAULT_SNAPSHOT_THRESHOLD: usize = 0;
33
34/// Configuration for a single [`RaftNode`](crate::RaftNode).
35///
36/// Build one with [`new`](RaftConfig::new) (or [`single`](RaftConfig::single)
37/// for a one-node cluster) and optionally tune it with the builder methods,
38/// which consume and return `self` so they chain.
39///
40/// # Examples
41///
42/// ```
43/// use raft_io::RaftConfig;
44///
45/// // Node 1 in a three-node cluster, with tuned timing.
46/// let cfg = RaftConfig::new(1, [2, 3])
47/// .with_election_timeout(15, 30)
48/// .with_heartbeat_interval(5);
49/// assert_eq!(cfg.id(), 1);
50/// assert_eq!(cfg.peers(), &[2, 3]);
51/// ```
52#[derive(Clone, Debug)]
53pub struct RaftConfig {
54 pub(crate) id: NodeId,
55 pub(crate) peers: Vec<NodeId>,
56 pub(crate) election_timeout_min: u32,
57 pub(crate) election_timeout_max: u32,
58 pub(crate) heartbeat_interval: u32,
59 pub(crate) max_batch: usize,
60 pub(crate) snapshot_threshold: usize,
61 pub(crate) seed: u64,
62}
63
64impl RaftConfig {
65 /// Creates a configuration for node `id` whose peers are `peers`.
66 ///
67 /// `peers` is every *other* node in the cluster; do not include `id`. The
68 /// quorum the node needs to win an election or commit an entry is derived
69 /// from the total size (`peers.len() + 1`). Timing defaults to a `10..=20`
70 /// tick election timeout and a `3` tick heartbeat, and the RNG seed defaults
71 /// to `id` so distinct nodes jitter differently out of the box.
72 ///
73 /// # Examples
74 ///
75 /// ```
76 /// use raft_io::RaftConfig;
77 ///
78 /// let cfg = RaftConfig::new(1, [2, 3, 4, 5]);
79 /// assert_eq!(cfg.peers().len(), 4);
80 /// ```
81 #[must_use]
82 pub fn new(id: NodeId, peers: impl IntoIterator<Item = NodeId>) -> Self {
83 let peers = peers.into_iter().filter(|&p| p != id).collect();
84 Self {
85 id,
86 peers,
87 election_timeout_min: DEFAULT_ELECTION_MIN,
88 election_timeout_max: DEFAULT_ELECTION_MAX,
89 heartbeat_interval: DEFAULT_HEARTBEAT,
90 max_batch: DEFAULT_MAX_BATCH,
91 snapshot_threshold: DEFAULT_SNAPSHOT_THRESHOLD,
92 seed: id,
93 }
94 }
95
96 /// Creates a configuration for a single-node cluster.
97 ///
98 /// A single node has no peers and a quorum of one, so it elects itself and
99 /// commits its own proposals immediately. This is the trivial path for
100 /// tests and local development.
101 ///
102 /// # Examples
103 ///
104 /// ```
105 /// use raft_io::RaftConfig;
106 ///
107 /// let cfg = RaftConfig::single(1);
108 /// assert!(cfg.peers().is_empty());
109 /// ```
110 #[must_use]
111 pub fn single(id: NodeId) -> Self {
112 Self::new(id, [])
113 }
114
115 /// Sets the randomised election timeout bounds, in ticks.
116 ///
117 /// A follower that hears nothing from a leader for a randomly chosen number
118 /// of ticks in `[min, max]` starts an election. The spread is what breaks
119 /// split votes. The bounds are normalised so `min >= 1` and `max >= min`,
120 /// so out-of-order or zero arguments cannot wedge the node.
121 ///
122 /// # Examples
123 ///
124 /// ```
125 /// use raft_io::RaftConfig;
126 ///
127 /// let cfg = RaftConfig::single(1).with_election_timeout(150, 300);
128 /// assert_eq!(cfg.election_timeout(), (150, 300));
129 ///
130 /// // Arguments are normalised rather than rejected.
131 /// let fixed = RaftConfig::single(1).with_election_timeout(0, 0);
132 /// assert_eq!(fixed.election_timeout(), (1, 1));
133 /// ```
134 #[must_use]
135 pub fn with_election_timeout(mut self, min: u32, max: u32) -> Self {
136 let min = min.max(1);
137 self.election_timeout_min = min;
138 self.election_timeout_max = max.max(min);
139 self
140 }
141
142 /// Sets the heartbeat interval, in ticks.
143 ///
144 /// A leader broadcasts a heartbeat every `interval` ticks to suppress
145 /// elections. Keep it well below the election-timeout lower bound — a few
146 /// times smaller is typical — so a single dropped heartbeat does not unseat
147 /// a healthy leader. The value is normalised to at least `1`.
148 ///
149 /// # Examples
150 ///
151 /// ```
152 /// use raft_io::RaftConfig;
153 ///
154 /// let cfg = RaftConfig::single(1).with_heartbeat_interval(5);
155 /// assert_eq!(cfg.heartbeat_interval(), 5);
156 /// ```
157 #[must_use]
158 pub fn with_heartbeat_interval(mut self, interval: u32) -> Self {
159 self.heartbeat_interval = interval.max(1);
160 self
161 }
162
163 /// Sets the maximum number of log entries a single `AppendEntries` carries.
164 ///
165 /// This bounds message size and the work done per replication RPC: a
166 /// follower that has fallen far behind is caught up in batches of at most
167 /// this many entries rather than in one unbounded payload. The value is
168 /// normalised to at least `1` so replication can always make progress.
169 ///
170 /// # Examples
171 ///
172 /// ```
173 /// use raft_io::RaftConfig;
174 ///
175 /// let cfg = RaftConfig::new(1, [2, 3]).with_max_batch(256);
176 /// assert_eq!(cfg.max_batch(), 256);
177 /// ```
178 #[must_use]
179 pub fn with_max_batch(mut self, max_batch: usize) -> Self {
180 self.max_batch = max_batch.max(1);
181 self
182 }
183
184 /// Sets the snapshot threshold: how many applied entries may accumulate
185 /// beyond the last snapshot before the node asks the application to take a
186 /// new one.
187 ///
188 /// When the gap between the last applied index and the snapshot index reaches
189 /// `threshold`, the node emits an [`Action::Snapshot`](crate::Action::Snapshot)
190 /// hint; the application snapshots its state machine and feeds the bytes back
191 /// via [`Event::Snapshot`](crate::Event::Snapshot), and the log is compacted.
192 /// `0` (the default) disables the hint entirely, so snapshots are opt-in.
193 ///
194 /// # Examples
195 ///
196 /// ```
197 /// use raft_io::RaftConfig;
198 ///
199 /// let cfg = RaftConfig::new(1, [2, 3]).with_snapshot_threshold(1024);
200 /// assert_eq!(cfg.snapshot_threshold(), 1024);
201 /// ```
202 #[must_use]
203 pub fn with_snapshot_threshold(mut self, threshold: usize) -> Self {
204 self.snapshot_threshold = threshold;
205 self
206 }
207
208 /// Sets the seed for the node's election-timeout RNG.
209 ///
210 /// Determinism is the point of the core, so the jitter source is seeded
211 /// rather than drawn from the OS. Equal seeds reproduce equal timeout
212 /// sequences; give peers distinct seeds (the default is the node id) so they
213 /// do not jitter in lockstep.
214 ///
215 /// # Examples
216 ///
217 /// ```
218 /// use raft_io::RaftConfig;
219 ///
220 /// let cfg = RaftConfig::single(1).with_seed(0xDEAD_BEEF);
221 /// assert_eq!(cfg.seed(), 0xDEAD_BEEF);
222 /// ```
223 #[must_use]
224 pub fn with_seed(mut self, seed: u64) -> Self {
225 self.seed = seed;
226 self
227 }
228
229 /// Returns this node's id.
230 #[inline]
231 #[must_use]
232 pub fn id(&self) -> NodeId {
233 self.id
234 }
235
236 /// Returns this node's peers (every other node in the cluster).
237 #[inline]
238 #[must_use]
239 pub fn peers(&self) -> &[NodeId] {
240 &self.peers
241 }
242
243 /// Returns the election-timeout bounds as `(min, max)` ticks.
244 #[inline]
245 #[must_use]
246 pub fn election_timeout(&self) -> (u32, u32) {
247 (self.election_timeout_min, self.election_timeout_max)
248 }
249
250 /// Returns the heartbeat interval in ticks.
251 #[inline]
252 #[must_use]
253 pub fn heartbeat_interval(&self) -> u32 {
254 self.heartbeat_interval
255 }
256
257 /// Returns the maximum entries carried by a single `AppendEntries`.
258 #[inline]
259 #[must_use]
260 pub fn max_batch(&self) -> usize {
261 self.max_batch
262 }
263
264 /// Returns the snapshot threshold (`0` if snapshots are disabled).
265 #[inline]
266 #[must_use]
267 pub fn snapshot_threshold(&self) -> usize {
268 self.snapshot_threshold
269 }
270
271 /// Returns the election-timeout RNG seed.
272 #[inline]
273 #[must_use]
274 pub fn seed(&self) -> u64 {
275 self.seed
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_new_filters_self_from_peers() {
285 let cfg = RaftConfig::new(1, [1, 2, 3]);
286 assert_eq!(cfg.peers(), &[2, 3]);
287 }
288
289 #[test]
290 fn test_defaults_are_applied() {
291 let cfg = RaftConfig::new(2, [1]);
292 assert_eq!(
293 cfg.election_timeout(),
294 (DEFAULT_ELECTION_MIN, DEFAULT_ELECTION_MAX)
295 );
296 assert_eq!(cfg.heartbeat_interval(), DEFAULT_HEARTBEAT);
297 assert_eq!(cfg.max_batch(), DEFAULT_MAX_BATCH);
298 assert_eq!(cfg.seed(), 2);
299 }
300
301 #[test]
302 fn test_max_batch_is_at_least_one() {
303 assert_eq!(RaftConfig::single(1).with_max_batch(0).max_batch(), 1);
304 assert_eq!(RaftConfig::single(1).with_max_batch(128).max_batch(), 128);
305 }
306
307 #[test]
308 fn test_single_has_no_peers() {
309 assert!(RaftConfig::single(9).peers().is_empty());
310 }
311
312 #[test]
313 fn test_election_timeout_normalises_bounds() {
314 let cfg = RaftConfig::single(1).with_election_timeout(0, 0);
315 assert_eq!(cfg.election_timeout(), (1, 1));
316
317 let swapped = RaftConfig::single(1).with_election_timeout(30, 10);
318 assert_eq!(swapped.election_timeout(), (30, 30));
319 }
320
321 #[test]
322 fn test_heartbeat_interval_is_at_least_one() {
323 assert_eq!(
324 RaftConfig::single(1)
325 .with_heartbeat_interval(0)
326 .heartbeat_interval(),
327 1
328 );
329 }
330
331 #[test]
332 fn test_builder_chains() {
333 let cfg = RaftConfig::new(1, [2, 3])
334 .with_election_timeout(15, 30)
335 .with_heartbeat_interval(5)
336 .with_seed(7);
337 assert_eq!(cfg.election_timeout(), (15, 30));
338 assert_eq!(cfg.heartbeat_interval(), 5);
339 assert_eq!(cfg.seed(), 7);
340 }
341}