zlayer_consensus/config.rs
1//! Consensus configuration with production-ready defaults.
2//!
3//! The default values are tuned for a typical 3-5 node cluster on a LAN
4//! with sub-millisecond latency. For WAN deployments, increase all timeouts.
5
6use std::time::Duration;
7
8use openraft::SnapshotPolicy;
9
10/// Configuration for a consensus node.
11///
12/// Wraps both openraft's `Config` and network-level timeout settings.
13#[derive(Debug, Clone)]
14pub struct ConsensusConfig {
15 /// Human-readable cluster name (used in logs).
16 pub cluster_name: String,
17
18 /// Minimum election timeout in milliseconds.
19 ///
20 /// A follower will start an election if it has not heard from the leader
21 /// in at least this many milliseconds. Setting this to 7-15x the heartbeat
22 /// interval prevents spurious elections while still detecting failures quickly.
23 ///
24 /// Default: 1500ms (7.5x default heartbeat).
25 pub election_timeout_min_ms: u64,
26
27 /// Maximum election timeout in milliseconds.
28 ///
29 /// The actual election timeout is randomized between min and max to prevent
30 /// split-vote scenarios.
31 ///
32 /// Default: 3000ms (15x default heartbeat).
33 pub election_timeout_max_ms: u64,
34
35 /// Heartbeat interval in milliseconds.
36 ///
37 /// The leader sends heartbeats at this interval to maintain authority.
38 ///
39 /// Default: 200ms.
40 pub heartbeat_interval_ms: u64,
41
42 /// Number of log entries since the last snapshot before triggering a new one.
43 ///
44 /// Default: 10,000 entries.
45 pub snapshot_logs_since_last: u64,
46
47 /// Maximum number of entries per `AppendEntries` RPC payload.
48 ///
49 /// Default: 300.
50 pub max_payload_entries: u64,
51
52 /// Timeout for vote and `append_entries` RPCs.
53 ///
54 /// Default: 5 seconds.
55 pub rpc_timeout: Duration,
56
57 /// Timeout for snapshot transfer RPCs.
58 ///
59 /// Snapshots can be large, so this should be significantly longer than
60 /// the normal RPC timeout.
61 ///
62 /// Default: 60 seconds.
63 pub snapshot_timeout: Duration,
64}
65
66impl Default for ConsensusConfig {
67 fn default() -> Self {
68 Self {
69 cluster_name: "zlayer".to_string(),
70 election_timeout_min_ms: 1500,
71 election_timeout_max_ms: 3000,
72 heartbeat_interval_ms: 200,
73 snapshot_logs_since_last: 10_000,
74 max_payload_entries: 300,
75 rpc_timeout: Duration::from_secs(5),
76 snapshot_timeout: Duration::from_secs(60),
77 }
78 }
79}
80
81impl ConsensusConfig {
82 /// Build an openraft `Config` from this consensus config.
83 ///
84 /// # Errors
85 ///
86 /// Returns an error if the resulting openraft config fails validation
87 /// (e.g., `election_timeout_min` > `election_timeout_max`).
88 #[allow(clippy::result_large_err)]
89 pub fn to_openraft_config(&self) -> Result<openraft::Config, openraft::ConfigError> {
90 let config = openraft::Config {
91 cluster_name: self.cluster_name.clone(),
92 election_timeout_min: self.election_timeout_min_ms,
93 election_timeout_max: self.election_timeout_max_ms,
94 heartbeat_interval: self.heartbeat_interval_ms,
95 max_payload_entries: self.max_payload_entries,
96 snapshot_policy: SnapshotPolicy::LogsSinceLast(self.snapshot_logs_since_last),
97 enable_tick: true,
98 enable_heartbeat: true,
99 enable_elect: true,
100 ..Default::default()
101 };
102
103 config.validate()
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn default_config_validates() {
113 let config = ConsensusConfig::default();
114 let raft_config = config.to_openraft_config();
115 assert!(
116 raft_config.is_ok(),
117 "Default config should validate: {raft_config:?}"
118 );
119 }
120
121 #[test]
122 fn invalid_config_detected() {
123 let config = ConsensusConfig {
124 election_timeout_min_ms: 5000,
125 election_timeout_max_ms: 1000, // min > max
126 ..Default::default()
127 };
128 assert!(config.to_openraft_config().is_err());
129 }
130}