Skip to main content

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;
10use tracing::warn;
11
12use super::validate_directory;
13use crate::Error;
14use crate::Result;
15
16/// Cluster node configuration parameters
17///
18/// Encapsulates all essential settings for cluster node initialization and operation,
19/// including network settings, storage paths, and cluster topology.
20///
21/// # Defaults
22/// Configuration can be loaded from file with default values generated via `serde`'s
23/// default implementations. Field-level defaults use helper functions prefixed with `default_`.
24
25#[derive(Debug, Serialize, Deserialize, Clone)]
26pub struct ClusterConfig {
27    /// Unique node identifier in cluster
28    ///
29    /// Default: `default_node_id()` (typically 0 for single-node setup)
30    #[serde(default = "default_node_id")]
31    pub node_id: u32,
32
33    /// Network listening address (IP:PORT)
34    ///
35    /// Default: `default_listen_addr()` (127.0.0.1:8000)
36    #[serde(default = "default_listen_addr")]
37    pub listen_address: SocketAddr,
38
39    /// Seed nodes for cluster initialization
40    ///
41    /// Default: `default_initial_cluster()` (empty vector)
42    ///
43    /// # Note
44    /// Should contain at least 3 nodes for production deployment
45    #[serde(default = "default_initial_cluster")]
46    pub initial_cluster: Vec<NodeMeta>,
47
48    /// Database storage root directory
49    ///
50    /// Default: `default_db_dir()` (/tmp/db)
51    #[serde(default = "default_db_dir")]
52    pub db_root_dir: PathBuf,
53
54    /// Log files output directory
55    ///
56    /// Default: `default_log_dir()` (./logs)
57    #[serde(default = "default_log_dir")]
58    pub log_dir: PathBuf,
59}
60impl Default for ClusterConfig {
61    fn default() -> Self {
62        Self {
63            node_id: default_node_id(),
64            listen_address: default_listen_addr(),
65            initial_cluster: vec![],
66            db_root_dir: default_db_dir(),
67            log_dir: default_log_dir(),
68        }
69    }
70}
71
72impl ClusterConfig {
73    /// Validates cluster configuration consistency
74    /// # Errors
75    /// Returns `Error::InvalidConfig` if any configuration rules are violated
76    pub fn validate(&self) -> Result<()> {
77        // Validate node identity
78        if self.node_id == 0 {
79            return Err(Error::Config(ConfigError::Message(
80                "node_id cannot be 0 (reserved for invalid nodes)".into(),
81            )));
82        }
83
84        // Validate cluster membership
85        if self.initial_cluster.is_empty() {
86            return Err(Error::Config(ConfigError::Message(
87                "initial_cluster must contain at least one node".into(),
88            )));
89        }
90
91        // Check node existence in cluster
92        let self_in_cluster = self.initial_cluster.iter().any(|n| n.id == self.node_id);
93        if !self_in_cluster {
94            return Err(Error::Config(ConfigError::Message(format!(
95                "Current node {} not found in initial_cluster",
96                self.node_id
97            ))));
98        }
99
100        // Check unique node IDs
101        let mut ids = std::collections::HashSet::new();
102        for node in &self.initial_cluster {
103            if !ids.insert(node.id) {
104                return Err(Error::Config(ConfigError::Message(format!(
105                    "Duplicate node_id {} in initial_cluster",
106                    node.id
107                ))));
108            }
109        }
110
111        // Validate network configuration
112        if self.listen_address.port() == 0 {
113            return Err(Error::Config(ConfigError::Message(
114                "listen_address must specify a non-zero port".into(),
115            )));
116        }
117
118        // Warn if data path is under /tmp — caller's responsibility to choose a persistent path
119        if self.db_root_dir.starts_with("/tmp") {
120            warn!(
121                "db_root_dir {:?} is a temporary path — data will be lost on reboot. \
122                 Use a persistent path for production.",
123                self.db_root_dir
124            );
125        }
126
127        validate_directory(&self.db_root_dir, "db_root_dir")?;
128        validate_directory(&self.log_dir, "log_dir")?;
129
130        Ok(())
131    }
132}
133
134fn default_node_id() -> u32 {
135    1
136}
137fn default_initial_cluster() -> Vec<NodeMeta> {
138    vec![NodeMeta {
139        id: 1,
140        address: "127.0.0.1:8080".to_string(),
141        role: Follower as i32,
142        status: NodeStatus::Active.into(),
143    }]
144}
145fn default_listen_addr() -> SocketAddr {
146    "127.0.0.1:9081".parse().unwrap()
147}
148fn default_db_dir() -> PathBuf {
149    PathBuf::from("/tmp/db")
150}
151fn default_log_dir() -> PathBuf {
152    PathBuf::from("/tmp/logs")
153}