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}