Skip to main content

raft_rust/
config.rs

1//! 单节点进程配置(YAML)。
2//!
3//! 多节点集群:分别为每个节点准备一份配置(如 `config/node1.yaml`),
4//! 再各启动一个 `raft-node` 进程。
5
6// 读取 YAML 配置文件内容
7use std::fs;
8// 将 listen/peer 字符串解析为 socket 地址
9use std::net::SocketAddr;
10// 配置路径与数据目录路径
11use std::path::{Path, PathBuf};
12
13// 从 YAML 反序列化到结构体
14use serde::Deserialize;
15
16// 配置加载失败映射为库错误
17use crate::error::{Error, Result};
18// 节点 ID、Raft 运行参数与 tick 单位
19use crate::raft::{NodeID, Options, Ticks};
20
21/// 节点进程配置文件。
22// 整份 YAML 的根结构:本节点 + 同伴 + 可选调参
23#[derive(Clone, Debug, Deserialize)]
24// 一份 YAML 对应一个 raft-node 进程
25pub struct NodeFileConfig {
26    // 本节点身份与监听/数据目录
27    pub node: NodeSection,
28    /// 同伴列表。**为空表示单节点模式**(启动后立即成为领导者)。
29    // 缺省为空向量,便于单机快速启动
30    #[serde(default)]
31    // 出站连接目标;空列表即单节点自举
32    pub peers: Vec<PeerSection>,
33    // Raft 心跳/选举/快照等可选项,缺省用 OptionsSection::default
34    #[serde(default)]
35    // 未写 options 时走 Default,避免强制用户填满调参
36    pub options: OptionsSection,
37// 根配置结构结束
38}
39
40// YAML 中 `node:` 段:标识本进程在集群中的位置
41#[derive(Clone, Debug, Deserialize)]
42// 本进程在集群中的固定身份与本地资源路径
43pub struct NodeSection {
44    // Raft 节点 ID,集群内唯一
45    pub id: NodeID,
46    /// 监听地址,如 `127.0.0.1:7001`。
47    pub listen: String,
48    /// 数据目录(Raft 日志 + 快照)。
49    pub data_dir: PathBuf,
50// 本节点段结束
51}
52
53// YAML 中单个 peer:用于出站连接与路由
54#[derive(Clone, Debug, Deserialize)]
55// 集群中一个远程投票节点的可达信息
56pub struct PeerSection {
57    // 同伴节点 ID
58    pub id: NodeID,
59    // 同伴 TCP 地址字符串
60    pub addr: String,
61// 单个 peer 描述结束
62}
63
64// YAML 中 `options:` 段:映射到运行时 `Options`
65#[derive(Clone, Debug, Deserialize)]
66// 与 Node 内部 Options 一一对应的可序列化调参
67pub struct OptionsSection {
68    // 领导者心跳间隔(tick 数)
69    #[serde(default = "default_heartbeat")]
70    // 越小越快探测故障,越大越省带宽
71    pub heartbeat_interval: Ticks,
72    // 选举超时下限(含)
73    #[serde(default = "default_election_min")]
74    // 随机超时下界,应明显大于心跳间隔
75    pub election_timeout_min: Ticks,
76    // 选举超时上限(不含),节点在区间内随机,降低平票
77    #[serde(default = "default_election_max")]
78    // 随机超时上界(半开),与 min 构成合法 Range
79    pub election_timeout_max: Ticks,
80    // 单次 Append 最多携带的日志条数,控制 RPC 体积
81    #[serde(default = "default_max_append")]
82    // 批量复制上限,过大易阻塞小消息
83    pub max_append_entries: usize,
84    // 是否启用 Pre-vote,减少分区节点抬升任期
85    #[serde(default = "default_true")]
86    // 生产建议保持开启,避免无谓选举抬 term
87    pub pre_vote: bool,
88    // 是否启用 CheckQuorum,领导者失去多数时主动下台
89    #[serde(default = "default_true")]
90    // 防止网络分区后旧领导者继续服务写
91    pub check_quorum: bool,
92    /// 距上次快照 apply 了多少条后触发本地快照;0 表示关闭。
93    #[serde(default = "default_snapshot_threshold")]
94    // 日志压缩触发阈值;0 关闭自动快照
95    pub snapshot_threshold: u64,
96// options 段结束
97}
98
99// 未写 options 段时的默认调参
100impl Default for OptionsSection {
101    // 与各字段 serde default 函数对齐的整段默认值
102    fn default() -> Self {
103        // 与 serde default 函数保持一致,避免两处默认值漂移
104        Self {
105            // 默认心跳间隔
106            heartbeat_interval: default_heartbeat(),
107            // 默认选举超时下界
108            election_timeout_min: default_election_min(),
109            // 默认选举超时上界
110            election_timeout_max: default_election_max(),
111            // 默认单次 Append 批量上限
112            max_append_entries: default_max_append(),
113            // 默认开启 Pre-vote 与 CheckQuorum,偏向生产安全
114            pre_vote: true,
115            // 默认开启失去多数时主动下台
116            check_quorum: true,
117            // 默认快照阈值
118            snapshot_threshold: default_snapshot_threshold(),
119        // 默认结构体字面量结束
120        }
121    // Default::default 结束
122    }
123// OptionsSection::Default 结束
124}
125
126// 配置段 → 运行时 Options 的转换实现
127impl OptionsSection {
128    // 将配置段转换为 Node 构造所需的 Options
129    pub fn to_options(&self) -> Options {
130        // 选举超时区间必须合法,否则随机超时无意义
131        assert!(
132            // min 必须严格小于 max,否则 Range 为空
133            self.election_timeout_min < self.election_timeout_max,
134            // 断言失败时的诊断信息
135            "election_timeout_min must be < election_timeout_max"
136        // 断言调用结束
137        );
138        // 组装运行时 Options(Range 为半开区间)
139        Options {
140            // 透传心跳间隔
141            heartbeat_interval: self.heartbeat_interval,
142            // min..max 半开区间,与论文中随机选举超时一致
143            election_timeout_range: self.election_timeout_min..self.election_timeout_max,
144            // 透传批量 Append 上限
145            max_append_entries: self.max_append_entries,
146            // 透传 Pre-vote 开关
147            pre_vote: self.pre_vote,
148            // 透传 CheckQuorum 开关
149            check_quorum: self.check_quorum,
150            // 透传快照阈值
151            snapshot_threshold: self.snapshot_threshold,
152        // Options 字面量结束
153        }
154    // to_options 结束
155    }
156// OptionsSection impl 结束
157}
158
159// 默认心跳:每 2 个 tick(配合 TICK_INTERVAL 约 200ms)
160fn default_heartbeat() -> Ticks {
161    // 2 tick ≈ 常见 100ms tick 下的 200ms 心跳
162    2
163// default_heartbeat 结束
164}
165// 默认选举超时下限 5 tick
166fn default_election_min() -> Ticks {
167    // 略大于数倍心跳,减少无谓选举
168    5
169// default_election_min 结束
170}
171// 默认选举超时上限 10 tick
172fn default_election_max() -> Ticks {
173    // 与 min 拉开区间,降低同时超时概率
174    10
175// default_election_max 结束
176}
177// 默认单次 Append 最多 100 条,平衡吞吐与包大小
178fn default_max_append() -> usize {
179    // 100 条是教学实现的折中默认
180    100
181// default_max_append 结束
182}
183// serde 布尔字段默认 true
184fn default_true() -> bool {
185    // 供 pre_vote/check_quorum 等字段的 serde default 复用
186    true
187// default_true 结束
188}
189// 默认每 apply 1000 条触发一次本地快照
190fn default_snapshot_threshold() -> u64 {
191    // 1000 条后压缩,避免日志无限增长
192    1000
193// default_snapshot_threshold 结束
194}
195
196// 根配置的加载与地址解析
197impl NodeFileConfig {
198    // 从磁盘路径加载并校验整份节点配置
199    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
200        // 规范化为 Path 引用
201        let path = path.as_ref();
202        // 读取 YAML 文本;失败包装为带路径的 IO 错误
203        let text = fs::read_to_string(path)
204            // 保留路径信息,便于运维定位缺文件/权限问题
205            .map_err(|e| Error::IO(format!("read config {}: {e}", path.display())))?;
206        // 反序列化;YAML 语法/字段错误 → InvalidData
207        let cfg: Self = serde_yaml::from_str(&text)
208            // 解析失败同样带上路径
209            .map_err(|e| Error::InvalidData(format!("parse config {}: {e}", path.display())))?;
210        // 本节点 ID 不得出现在 peers,避免自连与成员集合重复
211        if cfg.peers.iter().any(|p| p.id == cfg.node.id) {
212            // 配置自相矛盾:自己既是 node 又是 peer
213            return Err(Error::InvalidInput("node id must not appear in peers".into()));
214        // ID 冲突检查结束
215        }
216        // 校验通过,返回配置
217        Ok(cfg)
218    // load 结束
219    }
220
221    // 解析本节点监听地址,供 TcpListener 绑定
222    pub fn listen_addr(&self) -> Result<SocketAddr> {
223        // 将 "host:port" 解析为 SocketAddr
224        self.node
225            // 取出 YAML 中的 listen 字符串
226            .listen
227            // 解析为标准 SocketAddr
228            .parse()
229            // 非法地址 → 用户输入错误
230            .map_err(|e| Error::InvalidInput(format!("bad listen addr: {e}")))
231    // listen_addr 结束
232    }
233
234    // 解析全部 peer 地址,供 PeerOutbox 出站路由表使用
235    pub fn peer_addrs(&self) -> Result<Vec<(NodeID, SocketAddr)>> {
236        // 逐个 peer 解析;任一失败则整体失败
237        self.peers
238            // 遍历配置中的每个同伴
239            .iter()
240            // 将 PeerSection 转为 (NodeID, SocketAddr)
241            .map(|p| {
242                // 将 peer 的地址字符串解析为 SocketAddr
243                let addr: SocketAddr = p
244                    // 取出该 peer 的地址字段
245                    .addr
246                    // 解析 host:port
247                    .parse()
248                    // 失败时标明是哪个 peer id
249                    .map_err(|e| Error::InvalidInput(format!("bad peer {} addr: {e}", p.id)))?;
250                // 返回 (节点 ID, 地址) 二元组
251                Ok((p.id, addr))
252            // 单个 peer 映射闭包结束
253            })
254            // 收集为 Vec,传播第一个错误
255            .collect()
256    // peer_addrs 结束
257    }
258// NodeFileConfig impl 结束
259}