d_engine_core/config/
cluster.rs

1use 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/// Cluster node configuration parameters
18///
19/// Encapsulates all essential settings for cluster node initialization and operation,
20/// including network settings, storage paths, and cluster topology.
21///
22/// # Defaults
23/// Configuration can be loaded from file with default values generated via `serde`'s
24/// default implementations. Field-level defaults use helper functions prefixed with `default_`.
25
26#[derive(Debug, Serialize, Deserialize, Clone)]
27pub struct ClusterConfig {
28    /// Unique node identifier in cluster
29    ///
30    /// Default: `default_node_id()` (typically 0 for single-node setup)
31    #[serde(default = "default_node_id")]
32    pub node_id: u32,
33
34    /// Network listening address (IP:PORT)
35    ///
36    /// Default: `default_listen_addr()` (127.0.0.1:8000)
37    #[serde(default = "default_listen_addr")]
38    pub listen_address: SocketAddr,
39
40    /// Seed nodes for cluster initialization
41    ///
42    /// Default: `default_initial_cluster()` (empty vector)
43    ///
44    /// # Note
45    /// Should contain at least 3 nodes for production deployment
46    #[serde(default = "default_initial_cluster")]
47    pub initial_cluster: Vec<NodeMeta>,
48
49    /// Database storage root directory
50    ///
51    /// Default: `default_db_dir()` (/tmp/db)
52    #[serde(default = "default_db_dir")]
53    pub db_root_dir: PathBuf,
54
55    /// Log files output directory
56    ///
57    /// Default: `default_log_dir()` (./logs)
58    #[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    /// Validates cluster configuration consistency
75    /// # Errors
76    /// Returns `Error::InvalidConfig` if any configuration rules are violated
77    pub fn validate(&self) -> Result<()> {
78        // Validate node identity
79        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        // Validate cluster membership
86        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        // Check node existence in cluster
93        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        // Check unique node IDs
102        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        // Validate network configuration
113        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        // Validate storage paths
120        // Check /tmp/db usage: strict in release, permissive in debug
121        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}