#![cfg(test)]
mod common;
use common::soak::{parse_duration_from_args, record_latency, SoakMonitor, SoakRegression};
use common::MockConnectionFactory;
use std::sync::Arc;
use std::time::{Duration, Instant};
use sz_orm_core::{Pool, PoolConfigBuilder};
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore = "soak test 需显式 --ignored 启动;默认 60s,24h 任务 --soak-duration=24h"]
async fn soak_pool_long_running_steady_state() {
let duration = parse_duration_from_args();
let sample_interval = Duration::from_secs(if duration.as_secs() >= 3600 {
60 } else {
5 });
eprintln!(
"[soak] 启动:duration={:?}, sample_interval={:?}",
duration, sample_interval
);
let db = Arc::new(tokio::sync::Mutex::new(common::InMemoryDb::new()));
let factory = Arc::new(MockConnectionFactory::new(db));
let config = PoolConfigBuilder::new()
.max_size(20)
.min_idle(5)
.acquire_timeout(5)
.idle_timeout(60)
.max_lifetime(300)
.build()
.expect("PoolConfig invalid");
let pool = Arc::new(Pool::new(config, factory).expect("Pool::new failed"));
let mut monitor = SoakMonitor::new(duration, sample_interval);
let ops_counter = monitor.ops_counter();
let errors_counter = monitor.errors_counter();
let latency_window = monitor.latency_window();
const WORKER_COUNT: usize = 8;
let stop_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let mut workers = Vec::new();
for worker_id in 0..WORKER_COUNT {
let pool_clone = pool.clone();
let ops_clone = ops_counter.clone();
let errors_clone = errors_counter.clone();
let latency_clone = latency_window.clone();
let stop_clone = stop_flag.clone();
workers.push(tokio::spawn(async move {
while !stop_clone.load(std::sync::atomic::Ordering::Relaxed) {
let t0 = Instant::now();
match pool_clone.acquire().await {
Ok(conn) => {
tokio::task::yield_now().await;
pool_clone.release(conn).await;
let elapsed_us = t0.elapsed().as_micros() as u64;
record_latency(&latency_clone, elapsed_us);
ops_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
Err(e) => {
errors_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
eprintln!("[soak worker {}] acquire error: {}", worker_id, e);
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
}
}));
}
while !monitor.is_finished() {
tokio::time::sleep(sample_interval).await;
let status = pool.status().await;
let snap = monitor.snapshot((status.idle, status.active, status.max));
eprintln!(
"[soak] t={}s ops={} ops/s={:.1} pool(idle={},active={},max={}) rss={}MB fd={} p99={}us errors={}",
snap.elapsed_secs,
snap.ops_completed,
snap.ops_per_sec,
snap.pool_idle,
snap.pool_active,
snap.pool_max,
snap.rss_bytes / 1024 / 1024,
snap.fd_count,
snap.p99_latency_us,
snap.error_count,
);
}
stop_flag.store(true, std::sync::atomic::Ordering::Release);
for w in workers {
let _ = w.await;
}
tokio::time::sleep(Duration::from_millis(500)).await;
let final_status = pool.status().await;
let final_snap = monitor.snapshot((final_status.idle, final_status.active, final_status.max));
eprintln!(
"[soak] 完成:总操作 {} 次,错误 {} 次,最终 pool(idle={},active={})",
final_snap.ops_completed,
final_snap.error_count,
final_snap.pool_idle,
final_snap.pool_active,
);
let csv_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("target")
.join("soak-report.csv");
let csv_path_str = csv_path.to_str().expect("CSV path not UTF-8");
if let Err(e) = monitor.export_csv(csv_path_str) {
eprintln!("[soak] CSV 导出失败: {}", e);
} else {
eprintln!("[soak] CSV 报告已导出: {}", csv_path_str);
}
let regressions = monitor.detect_regressions();
if regressions.is_empty() {
eprintln!("[soak] ✅ 未检测到退化");
} else {
eprintln!("[soak] ⚠ 检测到 {} 项退化:", regressions.len());
for r in ®ressions {
eprintln!(" - {}", r);
}
}
pool.close_all().await;
assert!(
regressions.is_empty(),
"Soak test 检测到退化:{}",
regressions
.iter()
.map(|r| r.to_string())
.collect::<Vec<_>>()
.join("; ")
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn soak_smoke_10s() {
let duration = Duration::from_secs(10);
let sample_interval = Duration::from_secs(2);
let db = Arc::new(tokio::sync::Mutex::new(common::InMemoryDb::new()));
let factory = Arc::new(MockConnectionFactory::new(db));
let config = PoolConfigBuilder::new()
.max_size(10)
.min_idle(2)
.build()
.expect("PoolConfig invalid");
let pool = Arc::new(Pool::new(config, factory).expect("Pool::new failed"));
let mut monitor = SoakMonitor::new(duration, sample_interval);
let ops = monitor.ops_counter();
let errors = monitor.errors_counter();
let latency = monitor.latency_window();
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let mut workers = Vec::new();
for _ in 0..4 {
let pool_c = pool.clone();
let ops_c = ops.clone();
let err_c = errors.clone();
let lat_c = latency.clone();
let stop_c = stop.clone();
workers.push(tokio::spawn(async move {
while !stop_c.load(std::sync::atomic::Ordering::Relaxed) {
let t0 = Instant::now();
match pool_c.acquire().await {
Ok(conn) => {
tokio::time::sleep(Duration::from_millis(1)).await;
pool_c.release(conn).await;
record_latency(&lat_c, t0.elapsed().as_micros() as u64);
ops_c.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
Err(_) => {
err_c.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
}
}));
}
while !monitor.is_finished() {
tokio::time::sleep(sample_interval).await;
let s = pool.status().await;
let snap = monitor.snapshot((s.idle, s.active, s.max));
eprintln!(
"[soak-smoke] t={}s ops={} pool(idle={},active={})",
snap.elapsed_secs, snap.ops_completed, snap.pool_idle, snap.pool_active
);
}
stop.store(true, std::sync::atomic::Ordering::Release);
for w in workers {
let _ = w.await;
}
tokio::time::sleep(Duration::from_millis(200)).await;
let final_s = pool.status().await;
monitor.snapshot((final_s.idle, final_s.active, final_s.max));
pool.close_all().await;
assert!(
monitor.snapshots().last().unwrap().ops_completed >= 50,
"Soak smoke 应至少完成 50 次操作"
);
let regressions = monitor.detect_regressions();
let critical: Vec<&SoakRegression> = regressions
.iter()
.filter(|r| matches!(r, SoakRegression::PoolLeak { .. }))
.collect();
assert!(
critical.is_empty(),
"Soak smoke 不应有 PoolLeak:{:?}",
critical
);
}