migrations: - 001_trading_events.sql, 003_audit_system.sql: the hard-coded node_id literals (`trading-node-01`, `audit-node-01`, `ml-node-01`, `system-node-01`, `change-tracker-01`) are overridden per-deployment by later migrations rather than read from the environment. Describe that in the inline comment. - 004_compliance_views.sql: `generate_compliance_report` is a log stub — actual report generation is performed by the compliance service. Say so explicitly. services: - ml_training_service/tests/orchestrator_225_features_test.rs: the empty `#[ignore]`d placeholder for the 225-feature orchestrator loader has been removed; it held no assertions and only tracked a TODO (feedback_no_stubs.md). - trading_agent_service/src/service.rs: portfolio volatility uses the diagonal-only approximation because cross-asset return correlations are not maintained in this service. Document that. - trading_service/src/services/risk.rs: `get_risk_metrics` uses `calculate_marginal_var` + asset-class fallback; describe why `calculate_comprehensive_var` is not wired at this boundary. - trading_service/tests/auth_comprehensive.rs: delete the entire commented-out legacy BackupCodeValidator test block — the old `generate_backup_codes` / `store_backup_code` / `verify_backup_code` surface no longer exists, and MFA integration tests already cover the new API. testing: - harness/grpc_clients.rs: no BacktestingServiceClient proto exists; reword the stale TODO import line. - chaos/*: reword the family of "TODO: Implement ..." stubs as "Currently a no-op / synthetic result" descriptions so readers know exactly how much of the chaos framework is live. - compliance_automation_tests.rs: delete the file; it was a giant /* ... */ block referencing a nonexistent compliance module and was not wired into any Cargo target. - framework.rs: describe why `setup()` uses `println!` instead of `tracing_subscriber` (tracing_subscriber is not a dep of this integration crate). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
750 lines
23 KiB
Rust
750 lines
23 KiB
Rust
//! Chaos Engineering CLI Tool
|
||
//!
|
||
//! Command-line interface for running chaos engineering tests on the Foxhunt HFT system.
|
||
|
||
use anyhow::{Context, Result};
|
||
use clap::{Parser, Subcommand, ValueEnum};
|
||
use std::path::PathBuf;
|
||
use tracing::{error, info, warn};
|
||
use uuid::Uuid;
|
||
|
||
use super::chaos_framework::ChaosOrchestrator;
|
||
use super::ml_training_chaos::{MLChaosConfig, MLTrainingChaosTests, ModelType};
|
||
use super::nightly_chaos_runner::{NightlyChaosConfig, NightlyChaosRunner, ChaosJobEvent, AlertSeverity};
|
||
|
||
/// Chaos Engineering CLI for Foxhunt HFT System
|
||
#[derive(Parser)]
|
||
#[command(name = "foxhunt-chaos")]
|
||
#[command(about = "Chaos engineering tool for Foxhunt HFT trading system")]
|
||
#[command(version = "1.0.0")]
|
||
pub struct ChaosCliArgs {
|
||
#[command(subcommand)]
|
||
pub command: ChaosCommand,
|
||
|
||
/// Enable verbose logging
|
||
#[arg(short, long)]
|
||
pub verbose: bool,
|
||
|
||
/// Configuration file path
|
||
#[arg(short, long, value_name = "FILE")]
|
||
pub config: Option<PathBuf>,
|
||
}
|
||
|
||
#[derive(Subcommand)]
|
||
pub enum ChaosCommand {
|
||
/// Run a single chaos experiment
|
||
Run {
|
||
/// Type of chaos experiment to run
|
||
#[arg(short, long, value_enum)]
|
||
experiment_type: ExperimentType,
|
||
|
||
/// Target service name
|
||
#[arg(short, long, default_value = "ml_training_service")]
|
||
service: String,
|
||
|
||
/// Model type for ML experiments
|
||
#[arg(short, long, value_enum)]
|
||
model: Option<ModelTypeArg>,
|
||
|
||
/// Maximum recovery time in milliseconds
|
||
#[arg(long, default_value = "100")]
|
||
max_recovery_time_ms: u64,
|
||
|
||
/// Experiment duration in seconds
|
||
#[arg(short, long, default_value = "30")]
|
||
duration: u64,
|
||
|
||
/// Recovery timeout in seconds
|
||
#[arg(long, default_value = "60")]
|
||
recovery_timeout: u64,
|
||
},
|
||
|
||
/// Run ML training chaos test suite
|
||
MlSuite {
|
||
/// ML service endpoint
|
||
#[arg(short, long, default_value = "http://localhost:8080")]
|
||
endpoint: String,
|
||
|
||
/// Checkpoint base path
|
||
#[arg(short, long, default_value = "/tmp/ml_checkpoints")]
|
||
checkpoint_path: PathBuf,
|
||
|
||
/// Models to test (if not specified, tests all)
|
||
#[arg(short, long, value_enum)]
|
||
models: Vec<ModelTypeArg>,
|
||
|
||
/// GPU memory threshold in MB
|
||
#[arg(long, default_value = "8192")]
|
||
gpu_memory_mb: u64,
|
||
|
||
/// Generate report after completion
|
||
#[arg(long)]
|
||
generate_report: bool,
|
||
|
||
/// Output report path
|
||
#[arg(long)]
|
||
output_report: Option<PathBuf>,
|
||
},
|
||
|
||
/// Start nightly chaos job scheduler
|
||
Schedule {
|
||
/// Schedule time (HH:MM format)
|
||
#[arg(short, long, default_value = "02:00")]
|
||
time: String,
|
||
|
||
/// Timezone
|
||
#[arg(short, long, default_value = "UTC")]
|
||
timezone: String,
|
||
|
||
/// Exclude weekends
|
||
#[arg(long)]
|
||
exclude_weekends: bool,
|
||
|
||
/// Maximum duration in hours
|
||
#[arg(long, default_value = "3")]
|
||
max_duration: u8,
|
||
|
||
/// Notification webhook URL
|
||
#[arg(long)]
|
||
webhook: Option<String>,
|
||
|
||
/// Report storage path
|
||
#[arg(long, default_value = "./chaos_reports")]
|
||
report_path: PathBuf,
|
||
},
|
||
|
||
/// Validate system readiness for chaos testing
|
||
Validate {
|
||
/// Check ML service connectivity
|
||
#[arg(long)]
|
||
check_ml_service: bool,
|
||
|
||
/// Check database connectivity
|
||
#[arg(long)]
|
||
check_database: bool,
|
||
|
||
/// Check monitoring systems
|
||
#[arg(long)]
|
||
check_monitoring: bool,
|
||
},
|
||
|
||
/// List previous chaos experiment results
|
||
History {
|
||
/// Number of recent results to show
|
||
#[arg(short, long, default_value = "10")]
|
||
limit: usize,
|
||
|
||
/// Filter by status
|
||
#[arg(short, long)]
|
||
status: Option<String>,
|
||
|
||
/// Export to file
|
||
#[arg(short, long)]
|
||
export: Option<PathBuf>,
|
||
},
|
||
}
|
||
|
||
#[derive(ValueEnum, Clone, Debug)]
|
||
pub enum ExperimentType {
|
||
ProcessKill,
|
||
MemoryPressure,
|
||
NetworkPartition,
|
||
DiskIoFailure,
|
||
CpuThrottle,
|
||
GpuExhaustion,
|
||
DatabaseFailure,
|
||
}
|
||
|
||
#[derive(ValueEnum, Clone, Debug)]
|
||
pub enum ModelTypeArg {
|
||
Tlob,
|
||
Mamba2,
|
||
Dqn,
|
||
Ppo,
|
||
Liquid,
|
||
Tft,
|
||
}
|
||
|
||
impl From<ModelTypeArg> for ModelType {
|
||
fn from(arg: ModelTypeArg) -> Self {
|
||
match arg {
|
||
ModelTypeArg::Tlob => ModelType::TLOB,
|
||
ModelTypeArg::Mamba2 => ModelType::MAMBA2,
|
||
ModelTypeArg::Dqn => ModelType::DQN,
|
||
ModelTypeArg::Ppo => ModelType::PPO,
|
||
ModelTypeArg::Liquid => ModelType::Liquid,
|
||
ModelTypeArg::Tft => ModelType::TFT,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Main chaos CLI implementation
|
||
pub struct ChaosCli {
|
||
orchestrator: ChaosOrchestrator,
|
||
config: Option<NightlyChaosConfig>,
|
||
}
|
||
|
||
impl ChaosCli {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
orchestrator: ChaosOrchestrator::new(3), // Max 3 concurrent experiments
|
||
config: None,
|
||
}
|
||
}
|
||
|
||
/// Load configuration from file
|
||
pub async fn load_config(&mut self, config_path: Option<PathBuf>) -> Result<()> {
|
||
self.config = if let Some(path) = config_path {
|
||
let content = tokio::fs::read_to_string(&path)
|
||
.await
|
||
.context(format!("Failed to read config file: {:?}", path))?;
|
||
let config: NightlyChaosConfig =
|
||
toml::from_str(&content).context("Failed to parse configuration file")?;
|
||
Some(config)
|
||
} else {
|
||
Some(NightlyChaosConfig::default())
|
||
};
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Execute the CLI command
|
||
pub async fn execute(&self, args: ChaosCliArgs) -> Result<()> {
|
||
// Initialize logging
|
||
if args.verbose {
|
||
tracing_subscriber::fmt()
|
||
.with_max_level(tracing::Level::DEBUG)
|
||
.init();
|
||
} else {
|
||
tracing_subscriber::fmt()
|
||
.with_max_level(tracing::Level::INFO)
|
||
.init();
|
||
}
|
||
|
||
match args.command {
|
||
ChaosCommand::Run {
|
||
experiment_type,
|
||
service,
|
||
model,
|
||
max_recovery_time_ms,
|
||
duration,
|
||
recovery_timeout,
|
||
} => {
|
||
self.run_single_experiment(
|
||
experiment_type,
|
||
service,
|
||
model,
|
||
max_recovery_time_ms,
|
||
duration,
|
||
recovery_timeout,
|
||
)
|
||
.await
|
||
}
|
||
|
||
ChaosCommand::MlSuite {
|
||
endpoint,
|
||
checkpoint_path,
|
||
models,
|
||
gpu_memory_mb,
|
||
generate_report,
|
||
output_report,
|
||
} => {
|
||
self.run_ml_suite(
|
||
endpoint,
|
||
checkpoint_path,
|
||
models,
|
||
gpu_memory_mb,
|
||
generate_report,
|
||
output_report,
|
||
)
|
||
.await
|
||
}
|
||
|
||
ChaosCommand::Schedule {
|
||
time,
|
||
timezone,
|
||
exclude_weekends,
|
||
max_duration,
|
||
webhook,
|
||
report_path,
|
||
} => {
|
||
self.start_scheduler(
|
||
time,
|
||
timezone,
|
||
exclude_weekends,
|
||
max_duration,
|
||
webhook,
|
||
report_path,
|
||
)
|
||
.await
|
||
}
|
||
|
||
ChaosCommand::Validate {
|
||
check_ml_service,
|
||
check_database,
|
||
check_monitoring,
|
||
} => {
|
||
self.validate_system(check_ml_service, check_database, check_monitoring)
|
||
.await
|
||
}
|
||
|
||
ChaosCommand::History {
|
||
limit,
|
||
status,
|
||
export,
|
||
} => self.show_history(limit, status, export).await,
|
||
}
|
||
}
|
||
|
||
/// Run a single chaos experiment
|
||
async fn run_single_experiment(
|
||
&self,
|
||
experiment_type: ExperimentType,
|
||
service: String,
|
||
model: Option<ModelTypeArg>,
|
||
max_recovery_time_ms: u64,
|
||
duration: u64,
|
||
recovery_timeout: u64,
|
||
) -> Result<()> {
|
||
info!(
|
||
"Running single chaos experiment: {:?} on {}",
|
||
experiment_type, service
|
||
);
|
||
|
||
let experiment_id = Uuid::new_v4();
|
||
let failure_type = self.create_failure_type(&experiment_type)?;
|
||
|
||
let experiment = super::chaos_framework::ChaosExperiment {
|
||
id: experiment_id,
|
||
name: format!("{:?} Test on {}", experiment_type, service),
|
||
description: format!("Single chaos experiment: {:?}", experiment_type),
|
||
target_service: service,
|
||
failure_type,
|
||
duration: std::time::Duration::from_secs(duration),
|
||
recovery_timeout: std::time::Duration::from_secs(recovery_timeout),
|
||
max_recovery_time_ms,
|
||
enabled: true,
|
||
};
|
||
|
||
self.orchestrator.register_experiment(experiment).await?;
|
||
let result = self.orchestrator.execute_experiment(experiment_id).await?;
|
||
|
||
info!("Experiment completed with status: {:?}", result.status);
|
||
if let Some(recovery_time) = result.recovery_time_ms {
|
||
info!("Recovery time: {}ms", recovery_time);
|
||
if recovery_time <= max_recovery_time_ms {
|
||
info!("✅ Recovery time within HFT requirements");
|
||
} else {
|
||
warn!("⚠️ Recovery time exceeds HFT requirements");
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Run ML training chaos test suite
|
||
async fn run_ml_suite(
|
||
&self,
|
||
endpoint: String,
|
||
checkpoint_path: PathBuf,
|
||
models: Vec<ModelTypeArg>,
|
||
gpu_memory_mb: u64,
|
||
generate_report: bool,
|
||
output_report: Option<PathBuf>,
|
||
) -> Result<()> {
|
||
info!("Running ML training chaos test suite");
|
||
|
||
let model_types = if models.is_empty() {
|
||
// Test all models if none specified
|
||
vec![
|
||
ModelType::TLOB,
|
||
ModelType::MAMBA2,
|
||
ModelType::DQN,
|
||
ModelType::PPO,
|
||
ModelType::Liquid,
|
||
ModelType::TFT,
|
||
]
|
||
} else {
|
||
models.into_iter().map(Into::into).collect()
|
||
};
|
||
|
||
let ml_config = MLChaosConfig {
|
||
ml_service_endpoint: endpoint,
|
||
checkpoint_base_path: checkpoint_path,
|
||
model_types,
|
||
training_timeout_secs: 300,
|
||
max_recovery_time_ms: 100, // HFT requirement
|
||
gpu_memory_threshold_mb: gpu_memory_mb,
|
||
};
|
||
|
||
let ml_chaos = MLTrainingChaosTests::new(ml_config);
|
||
let results = ml_chaos.run_ml_chaos_suite().await?;
|
||
|
||
info!("ML chaos suite completed with {} results", results.len());
|
||
|
||
let successful = results
|
||
.iter()
|
||
.filter(|r| r.training_loss_continuity)
|
||
.count();
|
||
let failed = results.len() - successful;
|
||
|
||
info!("Results: {} successful, {} failed", successful, failed);
|
||
|
||
if generate_report {
|
||
let report = ml_chaos.generate_chaos_report(&results).await?;
|
||
|
||
if let Some(output_path) = output_report {
|
||
tokio::fs::write(&output_path, &report)
|
||
.await
|
||
.context("Failed to write report")?;
|
||
info!("Report saved to: {:?}", output_path);
|
||
} else {
|
||
println!("{}", report);
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Start nightly chaos scheduler
|
||
async fn start_scheduler(
|
||
&self,
|
||
time: String,
|
||
timezone: String,
|
||
exclude_weekends: bool,
|
||
max_duration: u8,
|
||
webhook: Option<String>,
|
||
report_path: PathBuf,
|
||
) -> Result<()> {
|
||
info!("Starting nightly chaos scheduler at {} {}", time, timezone);
|
||
|
||
// Parse time
|
||
let schedule_time = chrono::NaiveTime::parse_from_str(&time, "%H:%M")
|
||
.context("Invalid time format (use HH:MM)")?;
|
||
|
||
let config = NightlyChaosConfig {
|
||
enabled: true,
|
||
schedule_time,
|
||
timezone,
|
||
max_duration_hours: max_duration,
|
||
notification_webhook: webhook,
|
||
report_storage_path: report_path,
|
||
ml_chaos_config: self
|
||
.config
|
||
.as_ref()
|
||
.map(|c| c.ml_chaos_config.clone())
|
||
.unwrap_or_default(),
|
||
exclude_weekends,
|
||
retry_on_failure: true,
|
||
max_retries: 2,
|
||
};
|
||
|
||
let runner = NightlyChaosRunner::new(config);
|
||
|
||
// Subscribe to events for logging
|
||
let mut event_receiver = runner.subscribe_events();
|
||
let event_handler = tokio::spawn(async move {
|
||
while let Ok(event) = event_receiver.recv().await {
|
||
match event {
|
||
ChaosJobEvent::JobScheduled {
|
||
id,
|
||
scheduled_time,
|
||
} => {
|
||
info!("📅 Chaos job {} scheduled for {}", id, scheduled_time);
|
||
}
|
||
ChaosJobEvent::JobStarted { id } => {
|
||
info!("🚀 Chaos job {} started", id);
|
||
}
|
||
ChaosJobEvent::JobCompleted { id, summary } => {
|
||
info!(
|
||
"✅ Chaos job {} completed. Avg recovery: {:.1}ms",
|
||
id, summary.average_recovery_time_ms
|
||
);
|
||
}
|
||
ChaosJobEvent::JobFailed { id, error } => {
|
||
error!("❌ Chaos job {} failed: {}", id, error);
|
||
}
|
||
ChaosJobEvent::AlertTriggered {
|
||
message,
|
||
severity,
|
||
} => match severity {
|
||
AlertSeverity::Critical => {
|
||
error!("🚨 {}", message)
|
||
}
|
||
AlertSeverity::Warning => {
|
||
warn!("⚠️ {}", message)
|
||
}
|
||
AlertSeverity::Info => info!("ℹ️ {}", message),
|
||
},
|
||
_ => {}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Start the scheduler
|
||
info!("Chaos scheduler starting... Press Ctrl+C to stop");
|
||
tokio::select! {
|
||
result = runner.start() => {
|
||
if let Err(e) = result {
|
||
error!("Scheduler failed: {}", e);
|
||
}
|
||
}
|
||
_ = tokio::signal::ctrl_c() => {
|
||
info!("Received shutdown signal");
|
||
}
|
||
}
|
||
|
||
runner.stop().await;
|
||
event_handler.abort();
|
||
info!("Chaos scheduler stopped");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Validate system readiness
|
||
async fn validate_system(
|
||
&self,
|
||
check_ml_service: bool,
|
||
check_database: bool,
|
||
check_monitoring: bool,
|
||
) -> Result<()> {
|
||
info!("Validating system readiness for chaos testing");
|
||
|
||
let mut all_checks_passed = true;
|
||
|
||
if check_ml_service {
|
||
info!("Checking ML service connectivity...");
|
||
match self.validate_ml_service().await {
|
||
Ok(()) => info!("✅ ML service connectivity: OK"),
|
||
Err(e) => {
|
||
error!("❌ ML service connectivity: FAILED - {}", e);
|
||
all_checks_passed = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
if check_database {
|
||
info!("Checking database connectivity...");
|
||
match self.validate_database().await {
|
||
Ok(()) => info!("✅ Database connectivity: OK"),
|
||
Err(e) => {
|
||
error!("❌ Database connectivity: FAILED - {}", e);
|
||
all_checks_passed = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
if check_monitoring {
|
||
info!("Checking monitoring systems...");
|
||
match self.validate_monitoring().await {
|
||
Ok(()) => info!("✅ Monitoring systems: OK"),
|
||
Err(e) => {
|
||
error!("❌ Monitoring systems: FAILED - {}", e);
|
||
all_checks_passed = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
if all_checks_passed {
|
||
info!("🎉 All system checks passed. Ready for chaos testing!");
|
||
} else {
|
||
error!("💥 Some system checks failed. Fix issues before running chaos tests.");
|
||
std::process::exit(1);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Show experiment history
|
||
async fn show_history(
|
||
&self,
|
||
limit: usize,
|
||
status: Option<String>,
|
||
export: Option<PathBuf>,
|
||
) -> Result<()> {
|
||
info!("Showing chaos experiment history (limit: {})", limit);
|
||
|
||
// Get results from orchestrator
|
||
let results = self.orchestrator.get_results().await;
|
||
|
||
// Filter by status if specified
|
||
let filtered_results: Vec<_> = if let Some(ref status_filter) = status {
|
||
results
|
||
.into_iter()
|
||
.filter(|r| {
|
||
format!("{:?}", r.status)
|
||
.to_lowercase()
|
||
.contains(&status_filter.to_lowercase())
|
||
})
|
||
.take(limit)
|
||
.collect()
|
||
} else {
|
||
results.into_iter().take(limit).collect()
|
||
};
|
||
|
||
if filtered_results.is_empty() {
|
||
info!("No experiment results found");
|
||
return Ok(());
|
||
}
|
||
|
||
// Display results
|
||
println!("\n📊 Chaos Experiment History\n");
|
||
println!(
|
||
"{:<36} {:<20} {:<15} {:<10} {:<15}",
|
||
"Experiment ID", "Started", "Status", "Recovery", "Checkpoint"
|
||
);
|
||
println!("{}", "─".repeat(100));
|
||
|
||
for result in &filtered_results {
|
||
let started = result
|
||
.started_at
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap_or_default()
|
||
.as_secs();
|
||
let started_str = chrono::DateTime::from_timestamp(started as i64, 0)
|
||
.map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
|
||
.unwrap_or_else(|| "Unknown".to_string());
|
||
|
||
let recovery_str = result
|
||
.recovery_time_ms
|
||
.map(|t| format!("{}ms", t))
|
||
.unwrap_or_else(|| "N/A".to_string());
|
||
|
||
let checkpoint_str = if result.checkpoint_integrity {
|
||
"✅"
|
||
} else {
|
||
"❌"
|
||
};
|
||
|
||
println!(
|
||
"{:<36} {:<20} {:<15} {:<10} {:<15}",
|
||
result.experiment_id,
|
||
started_str,
|
||
format!("{:?}", result.status),
|
||
recovery_str,
|
||
checkpoint_str
|
||
);
|
||
}
|
||
|
||
// Export if requested
|
||
if let Some(export_path) = export {
|
||
let export_data = serde_json::to_string_pretty(&filtered_results)
|
||
.context("Failed to serialize results")?;
|
||
tokio::fs::write(&export_path, export_data)
|
||
.await
|
||
.context("Failed to write export file")?;
|
||
info!("Results exported to: {:?}", export_path);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Create failure type from experiment type
|
||
fn create_failure_type(
|
||
&self,
|
||
experiment_type: &ExperimentType,
|
||
) -> Result<super::chaos_framework::FailureType> {
|
||
use super::chaos_framework::{FailureType, Signal};
|
||
|
||
let failure_type = match experiment_type {
|
||
ExperimentType::ProcessKill => FailureType::ProcessKill {
|
||
signal: Signal::SIGTERM,
|
||
delay_before_restart_ms: 2000,
|
||
},
|
||
ExperimentType::MemoryPressure => FailureType::MemoryPressure {
|
||
target_mb: 4096,
|
||
duration_ms: 30000,
|
||
},
|
||
ExperimentType::NetworkPartition => FailureType::NetworkPartition {
|
||
target_ports: vec![8080, 5432, 6379],
|
||
duration_ms: 15000,
|
||
},
|
||
ExperimentType::DiskIoFailure => FailureType::DiskIoFailure {
|
||
target_paths: vec!["/tmp".to_string(), "/var/log".to_string()],
|
||
failure_rate_percent: 30,
|
||
},
|
||
ExperimentType::CpuThrottle => FailureType::CpuThrottle {
|
||
cpu_limit_percent: 50,
|
||
duration_ms: 20000,
|
||
},
|
||
ExperimentType::GpuExhaustion => FailureType::GpuResourceExhaustion {
|
||
memory_fill_percent: 95,
|
||
duration_ms: 25000,
|
||
},
|
||
ExperimentType::DatabaseFailure => FailureType::DatabaseConnectionFailure {
|
||
connection_string: "postgresql://localhost:5432/foxhunt".to_string(),
|
||
duration_ms: 10000,
|
||
},
|
||
};
|
||
|
||
Ok(failure_type)
|
||
}
|
||
|
||
/// Validate ML service connectivity.
|
||
///
|
||
/// The chaos CLI currently simulates the health check with a 100ms
|
||
/// sleep — a real gRPC round-trip to the ML service health endpoint
|
||
/// is not wired into the chaos harness. Used for timing-sensitive
|
||
/// chaos experiments where a real check would add network noise.
|
||
async fn validate_ml_service(&self) -> Result<()> {
|
||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||
Ok(())
|
||
}
|
||
|
||
/// Validate database connectivity.
|
||
///
|
||
/// Simulated via a 100ms sleep; the chaos harness does not hold a
|
||
/// live DB pool for validation purposes.
|
||
async fn validate_database(&self) -> Result<()> {
|
||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||
Ok(())
|
||
}
|
||
|
||
/// Validate monitoring systems.
|
||
///
|
||
/// Simulated via a 100ms sleep. Prometheus / metrics-endpoint
|
||
/// scraping is not wired into the chaos harness — monitoring
|
||
/// assertions are done out-of-band by the CI runner.
|
||
async fn validate_monitoring(&self) -> Result<()> {
|
||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl Default for ChaosCli {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
/// Main CLI entry point
|
||
pub async fn main() -> Result<()> {
|
||
let args = ChaosCliArgs::parse();
|
||
let mut cli = ChaosCli::new();
|
||
|
||
// Load configuration if specified
|
||
cli.load_config(args.config.clone()).await?;
|
||
|
||
// Execute the command
|
||
cli.execute(args).await?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_model_type_conversion() {
|
||
assert_eq!(ModelType::from(ModelTypeArg::Tlob), ModelType::TLOB);
|
||
assert_eq!(ModelType::from(ModelTypeArg::Dqn), ModelType::DQN);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_cli_creation() {
|
||
let cli = ChaosCli::new();
|
||
assert!(true); // Basic creation test
|
||
}
|
||
}
|