Files
foxhunt/testing/api-load/src/scenarios/stress_test.rs
jgrusewski d25c82f8f3 refactor: rename api_gateway → api across workspace, tests, and load crate
- Workspace Cargo.toml: remove web-gateway + api_gateway members, keep api
- trading_service: dep api-gateway → api, update test imports
- testing/api-gateway-load → testing/api-load (crate renamed)
- All test crates: get_api_gateway_addr → get_api_addr + variable renames

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:38:21 +01:00

207 lines
7.3 KiB
Rust

use anyhow::Result;
use tokio::sync::mpsc;
use tokio::task::JoinSet;
use crate::clients::{AuthenticatedClient, MixedWorkloadClient};
use crate::metrics::{collector::MetricsCollector, LoadTestReport, TestConfig};
pub async fn run(
gateway_url: String,
initial_clients: usize,
increment: usize,
increment_interval_secs: u64,
max_p99_latency_ms: f64,
max_error_rate_pct: f64,
) -> Result<LoadTestReport> {
tracing::info!(
"Starting STRESS test: initial {} clients, increment by {} every {}s",
initial_clients,
increment,
increment_interval_secs
);
let (metrics_tx, metrics_rx) = mpsc::unbounded_channel();
let mut collector = MetricsCollector::new(metrics_rx);
// Spawn collector task
let collector_handle = tokio::spawn(async move {
collector.run(initial_clients).await;
collector
});
let mut join_set = JoinSet::new();
#[allow(unused_assignments)]
let mut current_clients = 0usize;
#[allow(unused_assignments)]
let mut max_clients_reached = initial_clients;
let increment_duration = std::time::Duration::from_secs(increment_interval_secs);
let test_start = std::time::Instant::now();
// Helper to spawn a batch of clients
let spawn_clients = |join_set: &mut JoinSet<Result<()>>,
metrics_tx: &mpsc::UnboundedSender<crate::metrics::RequestMetric>,
gateway_url: &String,
start_id: usize,
count: usize| {
for i in 0..count {
let client_id = start_id + i;
let gateway_url = gateway_url.clone();
let metrics_tx = metrics_tx.clone();
join_set.spawn(async move {
let auth_client = AuthenticatedClient::new(
gateway_url,
"test-secret-key-for-load-testing",
&format!("user-{}", client_id),
&format!("loadtest-user-{}", client_id),
)
.await?;
let mut mixed_client = MixedWorkloadClient::new(auth_client, client_id, metrics_tx);
// Run for a long time (clients will be terminated when threshold is hit)
mixed_client
.run_mixed_workload(std::time::Duration::from_secs(3600))
.await
});
}
};
// Spawn initial batch
spawn_clients(
&mut join_set,
&metrics_tx,
&gateway_url,
0_usize,
initial_clients,
);
current_clients = initial_clients;
tracing::info!("Initial {} clients spawned", current_clients);
// Incremental load increase loop
loop {
// Wait for increment interval
tokio::time::sleep(increment_duration).await;
// Check current performance metrics (simplified - in real scenario, query collector)
// For now, we'll use a heuristic: if we've had any errors in recent metrics, consider stopping
// In production, you'd pull latest metrics from collector via a channel
// Spawn next batch
spawn_clients(
&mut join_set,
&metrics_tx,
&gateway_url,
current_clients,
increment,
);
current_clients += increment;
max_clients_reached = current_clients;
tracing::info!(
"Increased load: {} active clients (elapsed: {}s)",
current_clients,
test_start.elapsed().as_secs()
);
// In a real implementation, we'd check live metrics here
// For demonstration, we'll run until a maximum (e.g., 5000 clients or 10 minutes)
if current_clients >= 5000 || test_start.elapsed().as_secs() >= 600 {
tracing::info!(
"Stress test limit reached: {} clients or 10 minutes elapsed",
current_clients
);
break;
}
}
// Let the current load run for one more interval to measure performance
tracing::info!(
"Sustaining max load ({} clients) for {}s to measure breaking point...",
current_clients,
increment_interval_secs
);
tokio::time::sleep(increment_duration).await;
// Abort all client tasks
join_set.shutdown().await;
// Drop the sender to signal collector to finish
drop(metrics_tx);
// Wait for collector to finish and get the report
let collector = collector_handle.await?;
let mut report = collector.generate_report(
"Stress Test".to_owned(),
TestConfig {
num_clients: max_clients_reached,
duration_secs: test_start.elapsed().as_secs(),
test_type: "stress_test".to_owned(),
},
);
// Determine breaking point and capacity limits
let breaking_point_identified = report.metrics.error_rate_pct >= max_error_rate_pct
|| report.metrics.latency_stats.p99_ms >= max_p99_latency_ms;
let recommended_max_clients = if breaking_point_identified {
// Recommend 80% of breaking point as safe limit
usize::try_from(
(f64::from(u32::try_from(max_clients_reached).unwrap_or(u32::MAX)) * 0.8) as u64,
)
.unwrap_or(usize::MAX)
} else {
max_clients_reached
};
let bottleneck = if report.metrics.error_rate_pct >= max_error_rate_pct {
Some(format!(
"Error rate threshold exceeded: {:.2}% (max: {:.2}%)",
report.metrics.error_rate_pct, max_error_rate_pct
))
} else if report.metrics.latency_stats.p99_ms >= max_p99_latency_ms {
Some(format!(
"Latency threshold exceeded: {:.2}ms P99 (max: {:.2}ms)",
report.metrics.latency_stats.p99_ms, max_p99_latency_ms
))
} else {
Some("Test limit reached without failure".to_owned())
};
report.capacity_recommendation = Some(crate::metrics::CapacityRecommendation {
max_sustainable_clients: recommended_max_clients,
max_sustainable_rps: report.metrics.requests_per_second * 0.8,
bottleneck_identified: bottleneck.clone(),
recommendation: if breaking_point_identified {
format!(
"BREAKING POINT IDENTIFIED at {} clients. {}\n\
Recommended production capacity: {} concurrent clients ({} RPS).\n\
System degraded with {:.2}% error rate and {:.2}ms P99 latency.",
max_clients_reached,
bottleneck.unwrap_or_default(),
recommended_max_clients,
usize::try_from((report.metrics.requests_per_second * 0.8) as u64)
.unwrap_or(usize::MAX),
report.metrics.error_rate_pct,
report.metrics.latency_stats.p99_ms
)
} else {
format!(
"System sustained {} clients without failure (test limit reached).\n\
Error rate: {:.2}%, P99 latency: {:.2}ms.\n\
Breaking point not identified within test constraints. \
System capacity exceeds {} concurrent clients.",
max_clients_reached,
report.metrics.error_rate_pct,
report.metrics.latency_stats.p99_ms,
max_clients_reached
)
},
});
tracing::info!("Stress test completed: {:?}", report.metrics);
Ok(report)
}