Files
foxhunt/testing/stress/src/fault_injector.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
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>
2026-02-25 11:56:00 +01:00

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()
}
}