Files
foxhunt/testing/stress/tests/resource_exhaustion_stress.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

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");
}
}