Skip to main content

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}