d_engine_core/config/
mod.rs

1//! Configuration management module for distributed Raft consensus engine.
2//!
3//! Provides hierarchical configuration loading and validation with:
4//! - Default values as code base
5//! - Environment variable overrides
6//! - Configuration file support
7//! - Component-wise validation
8mod cluster;
9use std::fmt::Debug;
10use std::path::Path;
11mod lease;
12mod network;
13mod raft;
14mod retry;
15mod tls;
16pub use cluster::*;
17use config::ConfigError;
18pub use lease::*;
19pub use network::*;
20pub use raft::*;
21pub use retry::*;
22pub use tls::*;
23#[cfg(test)]
24mod config_test;
25#[cfg(test)]
26mod lease_test;
27#[cfg(test)]
28mod network_test;
29#[cfg(test)]
30mod raft_test;
31#[cfg(test)]
32mod tls_test;
33use std::env;
34
35use config::Config;
36use config::Environment;
37use config::File;
38use serde::Deserialize;
39use serde::Serialize;
40
41use crate::Error;
42use crate::Result;
43
44/// Main configuration container for Raft consensus engine components
45///
46/// Combines all subsystem configurations with hierarchical override support:
47/// 1. Default values from code implementation
48/// 2. Configuration file specified by `CONFIG_PATH`
49/// 3. Environment variables (highest priority)
50#[derive(Serialize, Deserialize, Clone, Default)]
51pub struct RaftNodeConfig {
52    /// Cluster topology and node configuration
53    pub cluster: ClusterConfig,
54    /// Network communication parameters
55    pub network: NetworkConfig,
56    /// Core Raft algorithm parameters
57    pub raft: RaftConfig,
58    /// Retry policies for distributed operations
59    pub retry: RetryPolicies,
60    /// TLS/SSL security configuration
61    pub tls: TlsConfig,
62}
63impl Debug for RaftNodeConfig {
64    fn fmt(
65        &self,
66        f: &mut std::fmt::Formatter<'_>,
67    ) -> std::fmt::Result {
68        f.debug_struct("RaftNodeConfig").field("cluster", &self.cluster).finish()
69    }
70}
71impl RaftNodeConfig {
72    /// Creates a new configuration with hierarchical override support:
73    ///
74    /// Configuration sources are merged in the following order (later sources
75    /// override earlier ones):
76    /// 1. Type defaults (lowest priority)
77    /// 2. Configuration file from `CONFIG_PATH` environment variable
78    /// 3. Environment variables with `RAFT__` prefix (highest priority)
79    ///
80    /// # Returns
81    /// Merged configuration instance or error if:
82    /// - Config file parsing fails
83    /// - Validation rules are violated
84    ///
85    /// # Example
86    /// ```ignore
87    /// // Load with default values only
88    /// let cfg = RaftNodeConfig::new()?;
89    ///
90    /// // Load with config file and environment variables
91    /// std::env::set_var("CONFIG_PATH", "config/cluster.toml");
92    /// std::env::set_var("RAFT__CLUSTER__NODE_ID", "100");
93    /// let cfg = RaftNodeConfig::new()?;
94    /// ```
95    pub fn new() -> Result<Self> {
96        // Create a basic configuration builder
97        // 1. Default values ​​as the base layer
98        let mut builder = Config::builder().add_source(Config::try_from(&Self::default())?);
99
100        // 2. Conditionally add configuration files
101        if let Ok(config_path) = env::var("CONFIG_PATH") {
102            builder = builder.add_source(File::with_name(&config_path).required(true));
103        }
104
105        // 3. Add environment variable source
106        builder = builder.add_source(
107            Environment::with_prefix("RAFT")
108                .separator("__")
109                .ignore_empty(true)
110                .try_parsing(true),
111        );
112
113        // Build and deserialize
114        let config: Self = builder.build()?.try_deserialize()?;
115        config.validate()?;
116        Ok(config)
117    }
118
119    /// Creates a new configuration with additional overrides:
120    ///
121    /// Merging order (later sources override earlier ones):
122    /// 1. Current configuration values
123    /// 2. New configuration file
124    /// 3. Latest environment variables (highest priority)
125    ///
126    /// # Example
127    /// ```ignore
128    /// // Initial configuration
129    /// let base = RaftNodeConfig::new()?;
130    ///
131    /// // Apply runtime overrides
132    /// let final_cfg = base.with_override_config("runtime_overrides.toml")?;
133    /// ```
134    pub fn with_override_config(
135        &self,
136        path: &str,
137    ) -> Result<Self> {
138        let config: Self = Config::builder()
139            .add_source(Config::try_from(self)?) // Current config
140            .add_source(File::with_name(path)) // New overrides
141            .add_source(
142                // Fresh environment
143                Environment::with_prefix("RAFT")
144                    .separator("__")
145                    .ignore_empty(true)
146                    .try_parsing(true),
147            )
148            .build()?
149            .try_deserialize()?;
150        config.validate()?;
151        Ok(config)
152    }
153
154    /// Validates cross-component configuration rules
155    ///
156    /// # Returns
157    /// `Ok(())` if all configurations are valid, or
158    /// `Err(Error)` containing validation failure details
159    ///
160    /// # Errors
161    /// Returns validation errors from any subsystem:
162    /// - Invalid port bindings
163    /// - Conflicting node IDs
164    /// - Expired certificates
165    /// - Retry policy conflicts
166    pub fn validate(&self) -> Result<()> {
167        self.cluster.validate()?;
168        self.raft.validate()?;
169        self.network.validate()?;
170        self.tls.validate()?;
171        self.retry.validate()?;
172        Ok(())
173    }
174
175    /// Checks if this node is configured as a learner (not a voter)
176    ///
177    /// A learner node has role=Learner. Only learners can join via JoinCluster RPC.
178    /// Voters have role=Follower/Candidate/Leader and are added via config change (AddNode).
179    ///
180    /// Note: Status (Promotable/ReadOnly/Active) is separate from role.
181    /// This method checks **role only**.
182    pub fn is_learner(&self) -> bool {
183        use d_engine_proto::common::NodeRole;
184
185        self.cluster
186            .initial_cluster
187            .iter()
188            .find(|n| n.id == self.cluster.node_id)
189            .map(|n| n.role == NodeRole::Learner as i32)
190            .unwrap_or(false)
191    }
192}
193
194/// Ensures directory path is valid and writable
195pub(super) fn validate_directory(
196    path: &Path,
197    name: &str,
198) -> Result<()> {
199    if path.as_os_str().is_empty() {
200        return Err(Error::Config(ConfigError::Message(format!(
201            "{name} path cannot be empty"
202        ))));
203    }
204
205    #[cfg(not(test))]
206    {
207        use std::fs;
208        // Check directory existence or create ability
209        if !path.exists() {
210            fs::create_dir_all(path).map_err(|e| {
211                Error::Config(ConfigError::Message(format!(
212                    "Failed to create {} directory at {}: {}",
213                    name,
214                    path.display(),
215                    e
216                )))
217            })?;
218        }
219
220        // Check write permissions
221        let test_file = path.join(".permission_test");
222        fs::write(&test_file, b"test").map_err(|e| {
223            Error::Config(ConfigError::Message(format!(
224                "No write permission in {} directory {}: {}",
225                name,
226                path.display(),
227                e
228            )))
229        })?;
230        fs::remove_file(&test_file).ok();
231    }
232
233    Ok(())
234}