d_engine_core/config/
cluster.rs1use std::net::SocketAddr;
2use std::path::PathBuf;
3
4use config::ConfigError;
5use d_engine_proto::common::NodeRole::Follower;
6use d_engine_proto::common::NodeStatus;
7use d_engine_proto::server::cluster::NodeMeta;
8use serde::Deserialize;
9use serde::Serialize;
10#[cfg(debug_assertions)]
11use tracing::warn;
12
13use super::validate_directory;
14use crate::Error;
15use crate::Result;
16
17#[derive(Debug, Serialize, Deserialize, Clone)]
27pub struct ClusterConfig {
28 #[serde(default = "default_node_id")]
32 pub node_id: u32,
33
34 #[serde(default = "default_listen_addr")]
38 pub listen_address: SocketAddr,
39
40 #[serde(default = "default_initial_cluster")]
47 pub initial_cluster: Vec<NodeMeta>,
48
49 #[serde(default = "default_db_dir")]
53 pub db_root_dir: PathBuf,
54
55 #[serde(default = "default_log_dir")]
59 pub log_dir: PathBuf,
60}
61impl Default for ClusterConfig {
62 fn default() -> Self {
63 Self {
64 node_id: default_node_id(),
65 listen_address: default_listen_addr(),
66 initial_cluster: vec![],
67 db_root_dir: default_db_dir(),
68 log_dir: default_log_dir(),
69 }
70 }
71}
72
73impl ClusterConfig {
74 pub fn validate(&self) -> Result<()> {
78 if self.node_id == 0 {
80 return Err(Error::Config(ConfigError::Message(
81 "node_id cannot be 0 (reserved for invalid nodes)".into(),
82 )));
83 }
84
85 if self.initial_cluster.is_empty() {
87 return Err(Error::Config(ConfigError::Message(
88 "initial_cluster must contain at least one node".into(),
89 )));
90 }
91
92 let self_in_cluster = self.initial_cluster.iter().any(|n| n.id == self.node_id);
94 if !self_in_cluster {
95 return Err(Error::Config(ConfigError::Message(format!(
96 "Current node {} not found in initial_cluster",
97 self.node_id
98 ))));
99 }
100
101 let mut ids = std::collections::HashSet::new();
103 for node in &self.initial_cluster {
104 if !ids.insert(node.id) {
105 return Err(Error::Config(ConfigError::Message(format!(
106 "Duplicate node_id {} in initial_cluster",
107 node.id
108 ))));
109 }
110 }
111
112 if self.listen_address.port() == 0 {
114 return Err(Error::Config(ConfigError::Message(
115 "listen_address must specify a non-zero port".into(),
116 )));
117 }
118
119 if self.db_root_dir == PathBuf::from("/tmp/db") {
122 #[cfg(not(debug_assertions))]
123 {
124 return Err(Error::Config(ConfigError::Message(
125 "db_root_dir not configured. Using /tmp/db is not allowed in release builds. \
126 Please set CONFIG_PATH environment variable or configure [cluster.db_root_dir] \
127 in your config file.".into()
128 )));
129 }
130
131 #[cfg(debug_assertions)]
132 {
133 warn!(
134 "⚠️ Using default /tmp/db (data will be lost on reboot). \
135 Set CONFIG_PATH or configure [cluster.db_root_dir] for production."
136 );
137 }
138 }
139
140 validate_directory(&self.db_root_dir, "db_root_dir")?;
141 validate_directory(&self.log_dir, "log_dir")?;
142
143 Ok(())
144 }
145}
146
147fn default_node_id() -> u32 {
148 1
149}
150fn default_initial_cluster() -> Vec<NodeMeta> {
151 vec![NodeMeta {
152 id: 1,
153 address: "127.0.0.1:8080".to_string(),
154 role: Follower as i32,
155 status: NodeStatus::Active.into(),
156 }]
157}
158fn default_listen_addr() -> SocketAddr {
159 "127.0.0.1:9081".parse().unwrap()
160}
161fn default_db_dir() -> PathBuf {
162 PathBuf::from("/tmp/db")
163}
164fn default_log_dir() -> PathBuf {
165 PathBuf::from("/tmp/logs")
166}