Promote service modules to services/ directory; add AmqpConnection + async IPL

- Flat src/amqp.rs, src/mongo.rs, src/mariadb.rs promoted to src/services/{amqp,mongo,mariadb}/
- services/amqp/connection.rs: AmqpConnection struct with connect() and declare_exchange()
- services/amqp/error.rs: AmqpError type (thiserror, wraps lapin::Error)
- ipl() made async; #[tokio::main] added to main()
- IPL step 3b: authenticate to RabbitMQ + declare beds.events topic exchange (durable)
- Added lapin = "2" and tokio = { version = "1", features = ["full"] } to Cargo.toml
- 12 unit tests pass
- Docs: README, CLAUDE.md, wiki/04-ipl.md, wiki/06-queue-topology.md updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 16:52:18 -07:00
parent 2a9afe7d77
commit e8fdb39ea2
14 changed files with 1419 additions and 126 deletions

118
src/services/mariadb/mod.rs Normal file
View File

@@ -0,0 +1,118 @@
//! # 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<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 (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<String, RelNodeConfig>) -> 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());
}
}