Add MariaDB IPL validation, topology docs in beds.toml, and developer wiki

- Add MariaDB (REL) IPL validation — master required, secondary non-fatal
- Add RelNodeConfig / RelInstanceConfig structs with master/secondary pattern
- Add rel_services section to beds.toml and test fixture
- Add detailed topology commentary to beds.toml covering standalone,
  master/replica, Galera cluster, and multi-DB-per-node configurations
- Add developer wiki (wiki/) covering:
    - Origin story — PHP Namaste history, production record, why Rust
    - Architecture overview — full system diagram, all layers explained
    - The four nodes — appServer, admin, segundo, tercero with real-world context
    - IPL sequence — every step documented with rationale for ordering
    - Configuration system — layering, env selection, adding new sections
    - Queue topology — exchanges, routing keys, broker bindings, vhost isolation
    - Template system — REC/REL, TLA convention, cache map, warehousing
    - Event lineage — compound event IDs, parent/child tracking, msLogs schema
    - Glossary
- Update README with wiki index and MariaDB status

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 15:41:28 -07:00
parent 2ce87710ff
commit 2a9afe7d77
19 changed files with 1798 additions and 12 deletions

View File

@@ -26,7 +26,7 @@
//! * `2026-04-02` - mks - refactored into load() + load_from() for testability
mod structs;
pub use structs::{BedsConfig, BrokerServicesConfig, RecNodeConfig};
pub use structs::{BedsConfig, BrokerServicesConfig, RecNodeConfig, RelNodeConfig, RelInstanceConfig};
use config::{Config, File, FileFormat};

View File

@@ -11,6 +11,7 @@ pub struct BedsConfig {
pub journal_on: bool,
pub broker_services: BrokerServicesConfig,
pub rec_services: HashMap<String, RecNodeConfig>,
pub rel_services: HashMap<String, RelNodeConfig>,
}
#[derive(Debug, Deserialize)]
@@ -60,3 +61,18 @@ pub struct RecNodeConfig {
pub database: String,
pub use_ssl: bool,
}
#[derive(Debug, Deserialize)]
pub struct RelNodeConfig {
pub master: RelInstanceConfig,
pub secondary: Option<RelInstanceConfig>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct RelInstanceConfig {
pub host: String,
pub port: u16,
pub user: String,
pub pass: String,
pub database: String,
}

View File

@@ -16,4 +16,5 @@
pub mod amqp;
pub mod config;
pub mod logging;
pub mod mariadb;
pub mod mongo;

View File

@@ -24,6 +24,7 @@
mod amqp;
mod config;
mod logging;
mod mariadb;
mod mongo;
/// Executes the BEDS Initial Program Load (IPL) sequence.
@@ -81,6 +82,18 @@ fn ipl() -> Result<(), String> {
}
}
// validate MariaDB reachability — fatal in production, non-fatal in all other envs
// secondary instance failures are always non-fatal (handled inside validate_all)
match mariadb::validate_all(&cfg.rel_services) {
Ok(()) => tracing::info!("MariaDB reachable"),
Err(e) => {
if cfg.id.env_name == "production" {
return Err(e);
}
tracing::warn!("MariaDB unreachable (non-fatal in {}): {}", cfg.id.env_name, e);
}
}
Ok(())
}

122
src/mariadb.rs Normal file
View File

@@ -0,0 +1,122 @@
//! # mariadb.rs — MariaDB (REL) Transport Layer
//!
//! Manages all MariaDB interactions for the BEDS node. At IPL, validates that
//! the master instance of each configured REL service node is reachable before
//! the node proceeds. The secondary instance is optional — its absence or
//! unreachability is logged but never fatal.
//!
//! Future phases will add connection pooling, authentication, and query
//! dispatch via the adapter layer.
//!
//! ## Calling Agents
//! - `ipl()` in main.rs — calls `validate_all()` during the IPL sequence
//!
//! ## Inputs
//! - `HashMap<String, RelNodeConfig>` 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
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<String, RelNodeConfig>) -> Result<(), String> {
for (name, node) in nodes {
// master is required — failure is propagated to the caller
validate(&format!("{}.master", name), &node.master)?;
// secondary is optional — log absence but do not fail
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());
}
}