Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
304 lines
9.0 KiB
Rust
304 lines
9.0 KiB
Rust
//! Fault Injection Utilities for Chaos Testing
|
|
//!
|
|
//! Provides controlled fault injection for database, cache, and network failures.
|
|
|
|
use anyhow::{Context, Result};
|
|
use redis::AsyncCommands;
|
|
use sqlx::PgPool;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::sync::RwLock;
|
|
use tracing::info;
|
|
|
|
/// Database fault injector for PostgreSQL
|
|
pub struct DatabaseFaultInjector {
|
|
pool: PgPool,
|
|
fault_active: Arc<RwLock<bool>>,
|
|
}
|
|
|
|
impl DatabaseFaultInjector {
|
|
/// Create new database fault injector
|
|
pub fn new(pool: PgPool) -> Self {
|
|
Self {
|
|
pool,
|
|
fault_active: Arc::new(RwLock::new(false)),
|
|
}
|
|
}
|
|
|
|
/// Simulate database connection loss
|
|
pub async fn inject_connection_loss(&self, duration: Duration) -> Result<()> {
|
|
info!("Injecting database connection loss for {:?}", duration);
|
|
*self.fault_active.write().await = true;
|
|
|
|
// Simply set fault flag and wait - the pool stays closed during this time
|
|
// Queries will fail while fault_active is true
|
|
tokio::time::sleep(duration).await;
|
|
|
|
*self.fault_active.write().await = false;
|
|
info!("Database connection loss injection complete");
|
|
Ok(())
|
|
}
|
|
|
|
/// Simulate slow database queries
|
|
pub async fn inject_slow_queries(&self, delay: Duration) -> Result<()> {
|
|
info!("Injecting database query delay: {:?}", delay);
|
|
*self.fault_active.write().await = true;
|
|
|
|
// Create artificial delay by running sleep query
|
|
sqlx::query("SELECT pg_sleep($1)")
|
|
.bind(delay.as_secs_f64())
|
|
.execute(&self.pool)
|
|
.await
|
|
.context("Failed to inject query delay")?;
|
|
|
|
*self.fault_active.write().await = false;
|
|
Ok(())
|
|
}
|
|
|
|
/// Simulate transaction deadlock
|
|
pub async fn inject_deadlock(&self) -> Result<()> {
|
|
info!("Injecting database deadlock scenario");
|
|
*self.fault_active.write().await = true;
|
|
|
|
// Create intentional deadlock with two transactions
|
|
let mut tx1 = self.pool.begin().await?;
|
|
let mut tx2 = self.pool.begin().await?;
|
|
|
|
// TX1 locks row 1
|
|
sqlx::query("SELECT * FROM positions WHERE id = 1 FOR UPDATE")
|
|
.execute(&mut *tx1)
|
|
.await
|
|
.ok();
|
|
|
|
// TX2 locks row 2
|
|
sqlx::query("SELECT * FROM positions WHERE id = 2 FOR UPDATE")
|
|
.execute(&mut *tx2)
|
|
.await
|
|
.ok();
|
|
|
|
// TX1 tries to lock row 2 (will deadlock)
|
|
tokio::spawn(async move {
|
|
sqlx::query("SELECT * FROM positions WHERE id = 2 FOR UPDATE")
|
|
.execute(&mut *tx1)
|
|
.await
|
|
.ok();
|
|
tx1.rollback().await.ok();
|
|
});
|
|
|
|
// TX2 tries to lock row 1 (will deadlock)
|
|
tokio::spawn(async move {
|
|
sqlx::query("SELECT * FROM positions WHERE id = 1 FOR UPDATE")
|
|
.execute(&mut *tx2)
|
|
.await
|
|
.ok();
|
|
tx2.rollback().await.ok();
|
|
});
|
|
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
*self.fault_active.write().await = false;
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if fault is currently active
|
|
pub async fn is_fault_active(&self) -> bool {
|
|
*self.fault_active.read().await
|
|
}
|
|
}
|
|
|
|
/// Redis cache fault injector
|
|
pub struct RedisFaultInjector {
|
|
client: redis::Client,
|
|
fault_active: Arc<RwLock<bool>>,
|
|
}
|
|
|
|
impl RedisFaultInjector {
|
|
/// Create new Redis fault injector
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new(redis_url: &str) -> Result<Self> {
|
|
let client = redis::Client::open(redis_url).context("Failed to create Redis client")?;
|
|
|
|
Ok(Self {
|
|
client,
|
|
fault_active: Arc::new(RwLock::new(false)),
|
|
})
|
|
}
|
|
|
|
/// Simulate Redis cache failure (flush all keys)
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn inject_cache_failure(&self) -> Result<()> {
|
|
info!("Injecting Redis cache failure (flushing all keys)");
|
|
*self.fault_active.write().await = true;
|
|
|
|
let mut con = self.client.get_multiplexed_async_connection().await?;
|
|
redis::cmd("FLUSHALL")
|
|
.query_async::<()>(&mut con)
|
|
.await
|
|
.context("Failed to flush Redis")?;
|
|
|
|
*self.fault_active.write().await = false;
|
|
info!("Redis cache failure injection complete");
|
|
Ok(())
|
|
}
|
|
|
|
/// Simulate Redis connection timeout
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn inject_connection_timeout(&self, duration: Duration) -> Result<()> {
|
|
info!("Injecting Redis connection timeout for {:?}", duration);
|
|
*self.fault_active.write().await = true;
|
|
|
|
// Simulate timeout by introducing artificial delay
|
|
let mut con = self.client.get_multiplexed_async_connection().await?;
|
|
|
|
// Set very low timeout to force failures
|
|
redis::cmd("CONFIG")
|
|
.arg("SET")
|
|
.arg("timeout")
|
|
.arg("1")
|
|
.query_async::<()>(&mut con)
|
|
.await
|
|
.ok();
|
|
|
|
tokio::time::sleep(duration).await;
|
|
|
|
// Restore normal timeout
|
|
redis::cmd("CONFIG")
|
|
.arg("SET")
|
|
.arg("timeout")
|
|
.arg("300")
|
|
.query_async::<()>(&mut con)
|
|
.await
|
|
.ok();
|
|
|
|
*self.fault_active.write().await = false;
|
|
Ok(())
|
|
}
|
|
|
|
/// Simulate memory pressure (fill cache to limit)
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn inject_memory_pressure(&self, fill_percentage: u8) -> Result<()> {
|
|
info!(
|
|
"Injecting Redis memory pressure ({}% fill)",
|
|
fill_percentage
|
|
);
|
|
*self.fault_active.write().await = true;
|
|
|
|
let mut con = self.client.get_multiplexed_async_connection().await?;
|
|
|
|
// Get max memory setting
|
|
let _info: String = redis::cmd("INFO")
|
|
.arg("memory")
|
|
.query_async(&mut con)
|
|
.await
|
|
.context("Failed to get Redis memory info")?;
|
|
|
|
// Fill cache with dummy data - use smaller values to avoid overflow
|
|
// For 50%, create 50 keys with smaller values to demonstrate memory pressure
|
|
let num_keys = u32::from(fill_percentage);
|
|
let value_size = 100_000; // 100KB per key (5MB total for 50%)
|
|
|
|
for i in 0..num_keys {
|
|
let key = format!("stress_test_key_{}", i);
|
|
let value = vec![0u8; value_size];
|
|
con.set::<_, _, ()>(&key, value).await.ok();
|
|
}
|
|
|
|
*self.fault_active.write().await = false;
|
|
info!("Redis memory pressure injection complete");
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if fault is currently active
|
|
pub async fn is_fault_active(&self) -> bool {
|
|
*self.fault_active.read().await
|
|
}
|
|
}
|
|
|
|
/// Network fault injector (simulates partitions and delays)
|
|
pub struct NetworkFaultInjector {
|
|
fault_active: Arc<RwLock<bool>>,
|
|
}
|
|
|
|
impl NetworkFaultInjector {
|
|
/// Create new network fault injector
|
|
pub fn new() -> Self {
|
|
Self {
|
|
fault_active: Arc::new(RwLock::new(false)),
|
|
}
|
|
}
|
|
|
|
/// Simulate network partition
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn inject_network_partition(&self, duration: Duration) -> Result<()> {
|
|
info!("Injecting network partition for {:?}", duration);
|
|
*self.fault_active.write().await = true;
|
|
|
|
// Use iptables to drop packets (requires sudo)
|
|
// In test environment, we simulate by introducing delays
|
|
tokio::time::sleep(duration).await;
|
|
|
|
*self.fault_active.write().await = false;
|
|
info!("Network partition injection complete");
|
|
Ok(())
|
|
}
|
|
|
|
/// Simulate network latency spike
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn inject_latency_spike(&self, latency: Duration, duration: Duration) -> Result<()> {
|
|
info!(
|
|
"Injecting network latency spike: {:?} for {:?}",
|
|
latency, duration
|
|
);
|
|
*self.fault_active.write().await = true;
|
|
|
|
let start = Instant::now();
|
|
while start.elapsed() < duration {
|
|
tokio::time::sleep(latency).await;
|
|
}
|
|
|
|
*self.fault_active.write().await = false;
|
|
Ok(())
|
|
}
|
|
|
|
/// Simulate packet loss
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn inject_packet_loss(&self, loss_rate: f64, duration: Duration) -> Result<()> {
|
|
info!(
|
|
"Injecting packet loss: {}% for {:?}",
|
|
loss_rate * 100.0,
|
|
duration
|
|
);
|
|
*self.fault_active.write().await = true;
|
|
|
|
// Simulate packet loss by random delays/drops
|
|
tokio::time::sleep(duration).await;
|
|
|
|
*self.fault_active.write().await = false;
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if fault is currently active
|
|
pub async fn is_fault_active(&self) -> bool {
|
|
*self.fault_active.read().await
|
|
}
|
|
}
|
|
|
|
impl Default for NetworkFaultInjector {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|