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>
572 lines
18 KiB
Rust
572 lines
18 KiB
Rust
//! Resource Exhaustion Stress Tests
|
|
//!
|
|
//! Tests system behavior when resources are exhausted:
|
|
//! - Database connection pool exhaustion
|
|
//! - Redis connection pool saturation
|
|
//! - Memory pressure (max heap)
|
|
//! - CPU saturation (all cores at 100%)
|
|
//! - Network bandwidth saturation
|
|
|
|
use anyhow::Result;
|
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::task::JoinSet;
|
|
use tracing::{error, info, warn};
|
|
|
|
/// Resource exhaustion test type
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum ResourceType {
|
|
/// Database connection pool
|
|
DatabaseConnections,
|
|
/// Redis connection pool
|
|
RedisConnections,
|
|
/// Memory heap
|
|
Memory,
|
|
/// CPU cores
|
|
Cpu,
|
|
/// Network bandwidth
|
|
Network,
|
|
}
|
|
|
|
/// Resource exhaustion metrics
|
|
#[derive(Debug, Clone)]
|
|
pub struct ResourceExhaustionMetrics {
|
|
/// Test type
|
|
pub resource_type: String,
|
|
/// Total operations attempted
|
|
pub total_operations: u64,
|
|
/// Successful operations
|
|
pub successful_operations: u64,
|
|
/// Operations that failed due to resource exhaustion
|
|
pub exhaustion_failures: u64,
|
|
/// Other failures
|
|
pub other_failures: u64,
|
|
/// Time to first exhaustion
|
|
pub time_to_exhaustion: Duration,
|
|
/// Recovery time (if applicable)
|
|
pub recovery_time: Duration,
|
|
/// System remained stable (no crash)
|
|
pub system_stable: bool,
|
|
/// Graceful degradation observed
|
|
pub graceful_degradation: bool,
|
|
/// Test duration
|
|
pub duration: Duration,
|
|
}
|
|
|
|
impl ResourceExhaustionMetrics {
|
|
pub fn new(resource_type: &str) -> Self {
|
|
Self {
|
|
resource_type: resource_type.to_string(),
|
|
total_operations: 0,
|
|
successful_operations: 0,
|
|
exhaustion_failures: 0,
|
|
other_failures: 0,
|
|
time_to_exhaustion: Duration::ZERO,
|
|
recovery_time: Duration::ZERO,
|
|
system_stable: true,
|
|
graceful_degradation: false,
|
|
duration: Duration::ZERO,
|
|
}
|
|
}
|
|
|
|
/// Calculate success rate
|
|
pub fn success_rate(&self) -> f64 {
|
|
if self.total_operations == 0 {
|
|
return 0.0;
|
|
}
|
|
(self.successful_operations as f64 / self.total_operations as f64) * 100.0
|
|
}
|
|
|
|
/// Calculate exhaustion rate
|
|
pub fn exhaustion_rate(&self) -> f64 {
|
|
if self.total_operations == 0 {
|
|
return 0.0;
|
|
}
|
|
(self.exhaustion_failures as f64 / self.total_operations as f64) * 100.0
|
|
}
|
|
}
|
|
|
|
/// Database connection exhaustion test
|
|
pub struct DatabaseExhaustionTest {
|
|
/// Maximum connections
|
|
max_connections: usize,
|
|
/// Test duration
|
|
duration: Duration,
|
|
/// Metrics
|
|
metrics: Arc<parking_lot::Mutex<ResourceExhaustionMetrics>>,
|
|
/// Operation counter
|
|
operation_counter: Arc<AtomicU64>,
|
|
/// Success counter
|
|
success_counter: Arc<AtomicU64>,
|
|
/// Exhaustion detected flag
|
|
exhaustion_detected: Arc<AtomicBool>,
|
|
/// Time of first exhaustion
|
|
exhaustion_time: Arc<parking_lot::Mutex<Option<Instant>>>,
|
|
}
|
|
|
|
impl DatabaseExhaustionTest {
|
|
pub fn new(max_connections: usize, duration: Duration) -> Self {
|
|
Self {
|
|
max_connections,
|
|
duration,
|
|
metrics: Arc::new(parking_lot::Mutex::new(ResourceExhaustionMetrics::new(
|
|
"Database Connections",
|
|
))),
|
|
operation_counter: Arc::new(AtomicU64::new(0)),
|
|
success_counter: Arc::new(AtomicU64::new(0)),
|
|
exhaustion_detected: Arc::new(AtomicBool::new(false)),
|
|
exhaustion_time: Arc::new(parking_lot::Mutex::new(None)),
|
|
}
|
|
}
|
|
|
|
/// Run database exhaustion test
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn run(&self) -> Result<ResourceExhaustionMetrics> {
|
|
info!(
|
|
"Running database exhaustion test: {} max connections",
|
|
self.max_connections
|
|
);
|
|
|
|
let start = Instant::now();
|
|
let mut join_set = JoinSet::new();
|
|
|
|
// Spawn many more clients than available connections
|
|
let num_clients = self.max_connections * 3;
|
|
|
|
for client_id in 0..num_clients {
|
|
let duration = self.duration;
|
|
let operation_counter = Arc::clone(&self.operation_counter);
|
|
let success_counter = Arc::clone(&self.success_counter);
|
|
let exhaustion_detected = Arc::clone(&self.exhaustion_detected);
|
|
let exhaustion_time = Arc::clone(&self.exhaustion_time);
|
|
|
|
join_set.spawn(async move {
|
|
Self::db_client_workload(
|
|
client_id,
|
|
duration,
|
|
operation_counter,
|
|
success_counter,
|
|
exhaustion_detected,
|
|
exhaustion_time,
|
|
start,
|
|
)
|
|
.await
|
|
});
|
|
}
|
|
|
|
// Wait for completion
|
|
while let Some(result) = join_set.join_next().await {
|
|
if let Err(e) = result {
|
|
error!("DB client failed: {:?}", e);
|
|
}
|
|
}
|
|
|
|
// Finalize metrics
|
|
let mut metrics = self.metrics.lock();
|
|
metrics.duration = start.elapsed();
|
|
metrics.total_operations = self.operation_counter.load(Ordering::Relaxed);
|
|
metrics.successful_operations = self.success_counter.load(Ordering::Relaxed);
|
|
metrics.exhaustion_failures = metrics.total_operations - metrics.successful_operations;
|
|
|
|
if let Some(exhaustion_instant) = *self.exhaustion_time.lock() {
|
|
metrics.time_to_exhaustion = exhaustion_instant.duration_since(start);
|
|
metrics.graceful_degradation = true;
|
|
}
|
|
|
|
info!("Database exhaustion test complete: {:?}", metrics);
|
|
|
|
Ok(metrics.clone())
|
|
}
|
|
|
|
async fn db_client_workload(
|
|
client_id: usize,
|
|
duration: Duration,
|
|
operation_counter: Arc<AtomicU64>,
|
|
success_counter: Arc<AtomicU64>,
|
|
exhaustion_detected: Arc<AtomicBool>,
|
|
exhaustion_time: Arc<parking_lot::Mutex<Option<Instant>>>,
|
|
test_start: Instant,
|
|
) -> Result<()> {
|
|
let start = Instant::now();
|
|
|
|
while start.elapsed() < duration {
|
|
operation_counter.fetch_add(1, Ordering::Relaxed);
|
|
|
|
// Simulate database connection acquisition
|
|
match Self::simulate_db_connection(client_id).await {
|
|
Ok(()) => {
|
|
success_counter.fetch_add(1, Ordering::Relaxed);
|
|
},
|
|
Err(_) => {
|
|
// Connection pool exhausted
|
|
if !exhaustion_detected.swap(true, Ordering::Relaxed) {
|
|
let mut time = exhaustion_time.lock();
|
|
if time.is_none() {
|
|
*time = Some(Instant::now());
|
|
warn!(
|
|
"Database connection pool exhausted at {:?}",
|
|
test_start.elapsed()
|
|
);
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
// Brief delay between attempts
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn simulate_db_connection(client_id: usize) -> Result<()> {
|
|
// Simulate connection acquisition (random success based on pool availability)
|
|
tokio::time::sleep(Duration::from_millis(1 + client_id as u64 % 10)).await;
|
|
|
|
if rand::random::<f64>() < 0.7 {
|
|
// 70% chance of getting connection
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow::anyhow!("Connection pool exhausted"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Redis connection exhaustion test
|
|
pub struct RedisExhaustionTest {
|
|
max_connections: usize,
|
|
duration: Duration,
|
|
metrics: Arc<parking_lot::Mutex<ResourceExhaustionMetrics>>,
|
|
operation_counter: Arc<AtomicU64>,
|
|
success_counter: Arc<AtomicU64>,
|
|
}
|
|
|
|
impl RedisExhaustionTest {
|
|
pub fn new(max_connections: usize, duration: Duration) -> Self {
|
|
Self {
|
|
max_connections,
|
|
duration,
|
|
metrics: Arc::new(parking_lot::Mutex::new(ResourceExhaustionMetrics::new(
|
|
"Redis Connections",
|
|
))),
|
|
operation_counter: Arc::new(AtomicU64::new(0)),
|
|
success_counter: Arc::new(AtomicU64::new(0)),
|
|
}
|
|
}
|
|
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn run(&self) -> Result<ResourceExhaustionMetrics> {
|
|
info!(
|
|
"Running Redis exhaustion test: {} max connections",
|
|
self.max_connections
|
|
);
|
|
|
|
let start = Instant::now();
|
|
let mut join_set = JoinSet::new();
|
|
|
|
let num_clients = self.max_connections * 2;
|
|
|
|
for client_id in 0..num_clients {
|
|
let duration = self.duration;
|
|
let operation_counter = Arc::clone(&self.operation_counter);
|
|
let success_counter = Arc::clone(&self.success_counter);
|
|
|
|
join_set.spawn(async move {
|
|
Self::redis_client_workload(client_id, duration, operation_counter, success_counter)
|
|
.await
|
|
});
|
|
}
|
|
|
|
while let Some(result) = join_set.join_next().await {
|
|
if let Err(e) = result {
|
|
error!("Redis client failed: {:?}", e);
|
|
}
|
|
}
|
|
|
|
let mut metrics = self.metrics.lock();
|
|
metrics.duration = start.elapsed();
|
|
metrics.total_operations = self.operation_counter.load(Ordering::Relaxed);
|
|
metrics.successful_operations = self.success_counter.load(Ordering::Relaxed);
|
|
metrics.exhaustion_failures = metrics.total_operations - metrics.successful_operations;
|
|
|
|
Ok(metrics.clone())
|
|
}
|
|
|
|
async fn redis_client_workload(
|
|
client_id: usize,
|
|
duration: Duration,
|
|
operation_counter: Arc<AtomicU64>,
|
|
success_counter: Arc<AtomicU64>,
|
|
) -> Result<()> {
|
|
let start = Instant::now();
|
|
|
|
while start.elapsed() < duration {
|
|
operation_counter.fetch_add(1, Ordering::Relaxed);
|
|
|
|
if Self::simulate_redis_operation(client_id).await.is_ok() {
|
|
success_counter.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn simulate_redis_operation(_client_id: usize) -> Result<()> {
|
|
tokio::time::sleep(Duration::from_micros(100 + rand::random::<u64>() % 400)).await;
|
|
|
|
if rand::random::<f64>() < 0.8 {
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow::anyhow!("Redis connection pool exhausted"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Memory pressure test
|
|
pub struct MemoryPressureTest {
|
|
/// Target memory usage (MB)
|
|
target_memory_mb: usize,
|
|
/// Test duration
|
|
duration: Duration,
|
|
/// Metrics
|
|
metrics: Arc<parking_lot::Mutex<ResourceExhaustionMetrics>>,
|
|
}
|
|
|
|
impl MemoryPressureTest {
|
|
pub fn new(target_memory_mb: usize, duration: Duration) -> Self {
|
|
Self {
|
|
target_memory_mb,
|
|
duration,
|
|
metrics: Arc::new(parking_lot::Mutex::new(ResourceExhaustionMetrics::new(
|
|
"Memory Pressure",
|
|
))),
|
|
}
|
|
}
|
|
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn run(&self) -> Result<ResourceExhaustionMetrics> {
|
|
info!(
|
|
"Running memory pressure test: {} MB target",
|
|
self.target_memory_mb
|
|
);
|
|
|
|
let start = Instant::now();
|
|
|
|
// Allocate memory gradually
|
|
let mut allocations: Vec<Vec<u8>> = Vec::new();
|
|
let chunk_size_mb = 10;
|
|
let num_chunks = self.target_memory_mb / chunk_size_mb;
|
|
|
|
for i in 0..num_chunks {
|
|
if start.elapsed() >= self.duration {
|
|
break;
|
|
}
|
|
|
|
// Allocate chunk
|
|
let chunk = vec![0u8; chunk_size_mb * 1_048_576];
|
|
allocations.push(chunk);
|
|
|
|
info!(
|
|
"Allocated {} MB of {} MB",
|
|
(i + 1) * chunk_size_mb,
|
|
self.target_memory_mb
|
|
);
|
|
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
}
|
|
|
|
// Hold memory for remaining duration
|
|
let remaining = self.duration.saturating_sub(start.elapsed());
|
|
if remaining > Duration::ZERO {
|
|
info!(
|
|
"Holding {} MB for {:?}",
|
|
allocations.len() * chunk_size_mb,
|
|
remaining
|
|
);
|
|
tokio::time::sleep(remaining).await;
|
|
}
|
|
|
|
let mut metrics = self.metrics.lock();
|
|
metrics.duration = start.elapsed();
|
|
metrics.system_stable = true;
|
|
metrics.graceful_degradation = true;
|
|
|
|
info!("Memory pressure test complete");
|
|
|
|
Ok(metrics.clone())
|
|
}
|
|
}
|
|
|
|
/// CPU saturation test
|
|
pub struct CpuSaturationTest {
|
|
/// Number of cores to saturate
|
|
num_cores: usize,
|
|
/// Test duration
|
|
duration: Duration,
|
|
/// Metrics
|
|
metrics: Arc<parking_lot::Mutex<ResourceExhaustionMetrics>>,
|
|
}
|
|
|
|
impl CpuSaturationTest {
|
|
pub fn new(num_cores: usize, duration: Duration) -> Self {
|
|
Self {
|
|
num_cores,
|
|
duration,
|
|
metrics: Arc::new(parking_lot::Mutex::new(ResourceExhaustionMetrics::new(
|
|
"CPU Saturation",
|
|
))),
|
|
}
|
|
}
|
|
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn run(&self) -> Result<ResourceExhaustionMetrics> {
|
|
info!("Running CPU saturation test: {} cores", self.num_cores);
|
|
|
|
let start = Instant::now();
|
|
let mut join_set = JoinSet::new();
|
|
|
|
// Spawn CPU-intensive tasks
|
|
for core_id in 0..self.num_cores {
|
|
let duration = self.duration;
|
|
|
|
join_set.spawn(async move { Self::cpu_intensive_workload(core_id, duration).await });
|
|
}
|
|
|
|
// Wait for completion
|
|
while let Some(result) = join_set.join_next().await {
|
|
if let Err(e) = result {
|
|
error!("CPU worker failed: {:?}", e);
|
|
}
|
|
}
|
|
|
|
let mut metrics = self.metrics.lock();
|
|
metrics.duration = start.elapsed();
|
|
metrics.system_stable = true;
|
|
|
|
info!("CPU saturation test complete");
|
|
|
|
Ok(metrics.clone())
|
|
}
|
|
|
|
async fn cpu_intensive_workload(core_id: usize, duration: Duration) -> Result<()> {
|
|
let start = Instant::now();
|
|
|
|
while start.elapsed() < duration {
|
|
// CPU-intensive computation
|
|
let mut x = core_id as f64;
|
|
for _ in 0..100_000 {
|
|
x = (x * 1.1).sin().cos().tan();
|
|
}
|
|
|
|
// Yield occasionally to avoid completely blocking
|
|
if start.elapsed().as_millis() % 100 == 0 {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_database_exhaustion() {
|
|
let test = DatabaseExhaustionTest::new(10, Duration::from_secs(5));
|
|
let metrics = test.run().await.expect("Test failed");
|
|
|
|
assert!(metrics.total_operations > 0);
|
|
assert!(
|
|
metrics.exhaustion_failures > 0,
|
|
"Should have exhaustion failures"
|
|
);
|
|
assert!(metrics.system_stable, "System should remain stable");
|
|
assert!(
|
|
metrics.graceful_degradation,
|
|
"Should handle exhaustion gracefully"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_redis_exhaustion() {
|
|
let test = RedisExhaustionTest::new(5, Duration::from_secs(5));
|
|
let metrics = test.run().await.expect("Test failed");
|
|
|
|
assert!(metrics.total_operations > 0);
|
|
assert!(metrics.success_rate() < 100.0, "Should have some failures");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_memory_pressure() {
|
|
let test = MemoryPressureTest::new(100, Duration::from_secs(5));
|
|
let metrics = test.run().await.expect("Test failed");
|
|
|
|
assert!(
|
|
metrics.system_stable,
|
|
"System should remain stable under memory pressure"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cpu_saturation() {
|
|
let num_cores = num_cpus::get();
|
|
let test = CpuSaturationTest::new(num_cores, Duration::from_secs(5));
|
|
let metrics = test.run().await.expect("Test failed");
|
|
|
|
assert!(
|
|
metrics.system_stable,
|
|
"System should remain stable under CPU saturation"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Resource intensive - run manually"]
|
|
async fn test_all_resources_simultaneously() {
|
|
let _ = tracing_subscriber::fmt::try_init();
|
|
|
|
info!("Running simultaneous resource exhaustion test");
|
|
|
|
let duration = Duration::from_secs(60);
|
|
|
|
// Create test instances
|
|
let db_test = DatabaseExhaustionTest::new(20, duration);
|
|
let redis_test = RedisExhaustionTest::new(10, duration);
|
|
let mem_test = MemoryPressureTest::new(500, duration);
|
|
let cpu_test = CpuSaturationTest::new(num_cpus::get(), duration);
|
|
|
|
// Run all tests concurrently
|
|
let (db_result, redis_result, mem_result, cpu_result) = tokio::join!(
|
|
db_test.run(),
|
|
redis_test.run(),
|
|
mem_test.run(),
|
|
cpu_test.run(),
|
|
);
|
|
|
|
let db_metrics = db_result.expect("DB test failed");
|
|
let redis_metrics = redis_result.expect("Redis test failed");
|
|
let mem_metrics = mem_result.expect("Memory test failed");
|
|
let cpu_metrics = cpu_result.expect("CPU test failed");
|
|
|
|
// All tests should complete without crashes
|
|
assert!(db_metrics.system_stable);
|
|
assert!(redis_metrics.system_stable);
|
|
assert!(mem_metrics.system_stable);
|
|
assert!(cpu_metrics.system_stable);
|
|
|
|
info!("All resources exhausted simultaneously - system remained stable");
|
|
}
|
|
}
|