//! # services/mariadb/mod.rs — MariaDB (REL) Transport Module //! //! MariaDB transport layer. Provides IPL reachability validation for all //! configured REL service nodes. Master failure is fatal; secondary failure //! is always non-fatal. Future phases will add the MariaDbConnection struct //! for authenticated sessions and query dispatch. //! //! ## Calling Agents //! - `ipl()` in main.rs — calls `validate_all()` during the IPL sequence //! //! ## Inputs //! - `HashMap` from the loaded BEDS configuration //! //! ## Outputs //! - `Ok(())` if all configured REL master nodes are reachable //! - `Err(String)` with node name, host:port, and OS error on first master failure //! //! **Author:** mks //! **Version:** 1.0 //! //! ## History //! * `2026-04-04` - mks - original coding (flat mariadb.rs) //! * `2026-04-04` - mks - promoted to services/mariadb/ use std::collections::HashMap; use std::net::TcpStream; use std::time::Duration; use crate::config::{RelInstanceConfig, RelNodeConfig}; /// Validates that all configured MariaDB master nodes are reachable. /// /// Iterates every entry in the `rel_services` config block and validates the /// master instance. Secondary instances are checked but their failure is /// non-fatal — a missing or unreachable secondary is logged as a warning. /// Fails on the first unreachable master. /// /// # Arguments /// /// * `nodes` — map of service name → `RelNodeConfig` from `BedsConfig` /// /// # Returns /// /// `Ok(())` if every master node responds to a TCP connect within the timeout. /// `Err(String)` with the service name and address of the first master failure. /// /// # History /// /// * `2026-04-04` - mks - original coding pub fn validate_all(nodes: &HashMap) -> Result<(), String> { for (name, node) in nodes { validate(&format!("{}.master", name), &node.master)?; if let Some(secondary) = &node.secondary { if let Err(e) = validate(&format!("{}.secondary", name), secondary) { tracing::warn!("MariaDB secondary unreachable (non-fatal): {}", e); } } } Ok(()) } /// Validates that a single MariaDB instance is reachable. /// /// # Arguments /// /// * `label` — descriptive label for error messages (e.g. "app_server.master") /// * `instance` — `RelInstanceConfig` for this instance /// /// # Returns /// /// `Ok(())` if the TCP handshake succeeds within 5 seconds. /// `Err(String)` with a descriptive message on failure. /// /// # History /// /// * `2026-04-04` - mks - original coding pub fn validate(label: &str, instance: &RelInstanceConfig) -> Result<(), String> { let addr_str = format!("{}:{}", instance.host, instance.port); let addr: std::net::SocketAddr = addr_str .parse() .map_err(|e| format!("Invalid MariaDB address for rel_services.{} ({}): {}", label, addr_str, e))?; TcpStream::connect_timeout(&addr, Duration::from_secs(5)) .map_err(|e| format!("MariaDB unreachable at rel_services.{} ({}): {}", label, addr_str, e))?; Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::config::load_from; fn test_cfg() -> crate::config::BedsConfig { load_from("tests/fixtures/beds_test.toml", "") .expect("test fixture beds_test.toml failed to load") } #[test] fn validate_err_on_closed_port() { let cfg = test_cfg(); let node = cfg.rel_services.get("app_server").unwrap(); let mut bad = node.master.clone(); bad.port = 1; assert!(validate("app_server.master", &bad).is_err()); } #[test] fn validate_err_on_bad_address() { let cfg = test_cfg(); let node = cfg.rel_services.get("app_server").unwrap(); let mut bad = node.master.clone(); bad.host = "not_a_valid_host!!!".to_string(); assert!(validate("app_server.master", &bad).is_err()); } }