Skip to main content

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}