1use crate::errors::{PolarisError, PolarisResult};
4use serde::{Deserialize, Serialize};
5use std::net::SocketAddr;
6use std::path::PathBuf;
7use std::time::Duration;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ClusterConfig {
12 pub name: String,
14
15 pub seed_nodes: Vec<String>,
17
18 pub network: NetworkConfig,
20
21 pub security: SecurityConfig,
23
24 pub storage: StorageConfig,
26
27 pub scheduler: SchedulerConfig,
29}
30
31impl Default for ClusterConfig {
32 fn default() -> Self {
33 Self {
34 name: "polaris-cluster".to_string(),
35 seed_nodes: vec![],
36 network: NetworkConfig::default(),
37 security: SecurityConfig::default(),
38 storage: StorageConfig::default(),
39 scheduler: SchedulerConfig::default(),
40 }
41 }
42}
43
44impl ClusterConfig {
45 pub fn validate(&self) -> PolarisResult<()> {
47 if self.name.is_empty() {
48 return Err(PolarisError::InvalidConfig("cluster name cannot be empty".into()));
49 }
50 self.network.validate()?;
51 self.security.validate()?;
52 self.storage.validate()?;
53 Ok(())
54 }
55
56 pub fn from_file(path: impl Into<PathBuf>) -> PolarisResult<Self> {
58 let contents = std::fs::read_to_string(path.into())
59 .map_err(|e| PolarisError::InvalidConfig(format!("Failed to read config: {}", e)))?;
60 toml::from_str(&contents)
61 .map_err(|e| PolarisError::InvalidConfig(format!("Failed to parse config: {}", e)))
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct NodeConfig {
68 pub name: Option<String>,
70
71 pub bind_addr: SocketAddr,
73
74 pub advertise_addr: Option<SocketAddr>,
76
77 pub resources: ResourceLimits,
79
80 #[serde(with = "humantime_serde")]
82 pub heartbeat_interval: Duration,
83
84 #[serde(with = "humantime_serde")]
86 pub heartbeat_timeout: Duration,
87}
88
89impl Default for NodeConfig {
90 fn default() -> Self {
91 Self {
92 name: None,
93 bind_addr: "127.0.0.1:7001".parse().unwrap(),
94 advertise_addr: None,
95 resources: ResourceLimits::default(),
96 heartbeat_interval: Duration::from_secs(5),
97 heartbeat_timeout: Duration::from_secs(15),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct NetworkConfig {
105 pub transport: TransportType,
107
108 #[serde(with = "humantime_serde")]
110 pub connect_timeout: Duration,
111
112 #[serde(with = "humantime_serde")]
114 pub request_timeout: Duration,
115
116 pub max_connections: usize,
118
119 pub compression: bool,
121}
122
123impl Default for NetworkConfig {
124 fn default() -> Self {
125 Self {
126 transport: TransportType::Quic,
127 connect_timeout: Duration::from_secs(10),
128 request_timeout: Duration::from_secs(30),
129 max_connections: 1000,
130 compression: false,
131 }
132 }
133}
134
135impl NetworkConfig {
136 fn validate(&self) -> PolarisResult<()> {
137 if self.max_connections == 0 {
138 return Err(PolarisError::InvalidConfig(
139 "max_connections must be > 0".into(),
140 ));
141 }
142 Ok(())
143 }
144}
145
146#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
148#[serde(rename_all = "lowercase")]
149pub enum TransportType {
150 Quic,
152 Tls,
154 Grpc,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct SecurityConfig {
161 pub mtls_enabled: bool,
163
164 pub cert_path: Option<PathBuf>,
166
167 pub key_path: Option<PathBuf>,
169
170 pub ca_cert_path: Option<PathBuf>,
172
173 pub jwt_enabled: bool,
175
176 pub jwt_secret: Option<String>,
178}
179
180impl Default for SecurityConfig {
181 fn default() -> Self {
182 Self {
183 mtls_enabled: false, cert_path: None,
185 key_path: None,
186 ca_cert_path: None,
187 jwt_enabled: false,
188 jwt_secret: None,
189 }
190 }
191}
192
193impl SecurityConfig {
194 fn validate(&self) -> PolarisResult<()> {
195 if self.mtls_enabled {
196 if self.cert_path.is_none() || self.key_path.is_none() {
197 return Err(PolarisError::InvalidConfig(
198 "mTLS enabled but cert/key paths not provided".into(),
199 ));
200 }
201 }
202 Ok(())
203 }
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct StorageConfig {
209 pub backend: StorageBackend,
211
212 pub path: Option<PathBuf>,
214
215 pub persistence_enabled: bool,
217}
218
219impl Default for StorageConfig {
220 fn default() -> Self {
221 Self {
222 backend: StorageBackend::InMemory,
223 path: None,
224 persistence_enabled: false,
225 }
226 }
227}
228
229impl StorageConfig {
230 fn validate(&self) -> PolarisResult<()> {
231 if self.persistence_enabled && self.path.is_none() {
232 return Err(PolarisError::InvalidConfig(
233 "persistence enabled but path not provided".into(),
234 ));
235 }
236 Ok(())
237 }
238}
239
240#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
242#[serde(rename_all = "lowercase")]
243pub enum StorageBackend {
244 InMemory,
246 RocksDb,
248 Sled,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct SchedulerConfig {
255 pub scheduler_type: SchedulerType,
257
258 pub max_retries: u32,
260
261 pub retry_backoff_multiplier: f64,
263
264 #[serde(with = "humantime_serde")]
266 pub initial_retry_delay: Duration,
267
268 #[serde(with = "humantime_serde")]
270 pub max_retry_delay: Duration,
271}
272
273impl Default for SchedulerConfig {
274 fn default() -> Self {
275 Self {
276 scheduler_type: SchedulerType::RoundRobin,
277 max_retries: 3,
278 retry_backoff_multiplier: 2.0,
279 initial_retry_delay: Duration::from_millis(100),
280 max_retry_delay: Duration::from_secs(60),
281 }
282 }
283}
284
285#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
287#[serde(rename_all = "snake_case")]
288pub enum SchedulerType {
289 RoundRobin,
291 LoadAware,
293 AiScheduler,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct ResourceLimits {
300 pub max_cpu_cores: usize,
302
303 pub max_memory_bytes: u64,
305
306 pub max_concurrent_tasks: usize,
308
309 pub max_network_bandwidth: u64,
311}
312
313impl Default for ResourceLimits {
314 fn default() -> Self {
315 Self {
316 max_cpu_cores: num_cpus::get(),
317 max_memory_bytes: 0, max_concurrent_tasks: 100,
319 max_network_bandwidth: 0, }
321 }
322}
323
324mod num_cpus {
326 pub(super) fn get() -> usize {
327 std::thread::available_parallelism()
328 .map(|n| n.get())
329 .unwrap_or(4)
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn test_cluster_config_default() {
339 let config = ClusterConfig::default();
340 assert_eq!(config.name, "polaris-cluster");
341 assert!(config.seed_nodes.is_empty());
342 }
343
344 #[test]
345 fn test_cluster_config_validation() {
346 let mut config = ClusterConfig::default();
347 assert!(config.validate().is_ok());
348
349 config.name = String::new();
350 assert!(config.validate().is_err());
351 }
352
353 #[test]
354 fn test_node_config_default() {
355 let config = NodeConfig::default();
356 assert!(config.name.is_none());
357 assert_eq!(config.heartbeat_interval, Duration::from_secs(5));
358 }
359}