mod common;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use axum::body::Body;
use axum::http::{Request, StatusCode};
use common::create_test_app_with_state;
use serde_json::json;
use tempfile::TempDir;
use tower::ServiceExt;
use velesdb_core::Point;
const ROW_COUNT: u64 = 2_000;
const INSERT_BATCH: u64 = 2_000;
const SLOW_REQUESTS: usize = 64;
fn query_request(query: &str) -> Request<Body> {
Request::builder()
.method("POST")
.uri("/query")
.header("content-type", "application/json")
.body(Body::from(json!({ "query": query }).to_string()))
.expect("build /query request")
}
async fn seed_metadata_collection(state: &Arc<velesdb_server::AppState>) {
state
.db
.create_metadata_collection("bulk")
.expect("create metadata collection");
let coll = state
.db
.get_any_collection("bulk")
.expect("collection registered");
tokio::task::spawn_blocking(move || {
let mut start = 0u64;
while start < ROW_COUNT {
let end = (start + INSERT_BATCH).min(ROW_COUNT);
let points: Vec<Point> = (start..end)
.map(|i| {
Point::new(
i,
vec![],
Some(json!({
"category": format!("cat-{}", i % 50),
"value": i,
"note": format!("padding payload for row {i} to make the scan cost real"),
})),
)
})
.collect();
coll.upsert(points).expect("seed upsert");
start = end;
}
})
.await
.expect("seeding task");
}
fn spawn_hard_watchdog(done: Arc<AtomicBool>) {
std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(300));
if !done.load(Ordering::SeqCst) {
eprintln!(
"handler_blocking_discipline: hard watchdog fired after 300s — aborting process"
);
std::process::exit(1);
}
});
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn health_stays_responsive_while_slow_query_scans_run() {
let done = Arc::new(AtomicBool::new(false));
spawn_hard_watchdog(Arc::clone(&done));
let temp_dir = TempDir::new().expect("temp dir");
let (app, state) = create_test_app_with_state(&temp_dir);
seed_metadata_collection(&state).await;
let completed = Arc::new(AtomicUsize::new(0));
let wave_started = Instant::now();
let mut slow_handles = Vec::with_capacity(SLOW_REQUESTS);
for i in 0..SLOW_REQUESTS {
let slow_app = app.clone();
let counter = Arc::clone(&completed);
let id = 1_000_000 + i;
let sql = format!("INSERT INTO bulk (id, category, value) VALUES ({id}, 'cat-slow', {i})");
slow_handles.push(tokio::spawn(async move {
let response = slow_app
.oneshot(query_request(&sql))
.await
.expect("slow /query request");
counter.fetch_add(1, Ordering::SeqCst);
response.status()
}));
}
let health_app = app.clone();
let probe_started = Instant::now();
let health_status = tokio::time::timeout(
Duration::from_secs(30),
tokio::spawn(async move {
health_app
.oneshot(
Request::builder()
.uri("/health")
.body(Body::empty())
.expect("build /health request"),
)
.await
.expect("health request")
.status()
}),
)
.await
.expect("watchdog: /health did not complete within 30s — the async worker is starved")
.expect("health task panicked");
let health_latency = probe_started.elapsed();
let completed_at_probe = completed.load(Ordering::SeqCst);
for handle in slow_handles {
let status = tokio::time::timeout(Duration::from_secs(120), handle)
.await
.expect("watchdog: slow /query did not finish within 120s")
.expect("slow query task panicked");
assert_eq!(status, StatusCode::OK, "every /query INSERT must succeed");
}
let wave_elapsed = wave_started.elapsed();
done.store(true, Ordering::SeqCst);
println!(
"health latency: {health_latency:?}; wave total: {wave_elapsed:?}; \
inserts completed when probe returned: {completed_at_probe}/{SLOW_REQUESTS}"
);
assert_eq!(health_status, StatusCode::OK, "/health must succeed");
assert!(
completed_at_probe < SLOW_REQUESTS,
"/health only completed after all {SLOW_REQUESTS} fsync-bearing /query \
INSERTs had finished (health latency {health_latency:?}, wave total \
{wave_elapsed:?}): the sole runtime worker was blocked by inline core \
calls in the /query handler instead of parking on spawn_blocking"
);
assert!(
health_latency < Duration::from_secs(5),
"/health took {health_latency:?} while the /query wave was in flight — \
the sole runtime worker was blocked by an inline core call"
);
}