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}