fix: per-check client with connect_timeout to guarantee OS-level TCP timeout

This commit is contained in:
M1 2026-03-18 12:42:09 +04:00
parent 7905a8003b
commit b8b0a9d5e2
1 changed files with 16 additions and 13 deletions

View File

@ -70,10 +70,20 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
let method = monitor.method.as_deref().unwrap_or("GET").to_uppercase(); let method = monitor.method.as_deref().unwrap_or("GET").to_uppercase();
let timeout = std::time::Duration::from_millis(monitor.timeout_ms.unwrap_or(30000)); let timeout = std::time::Duration::from_millis(monitor.timeout_ms.unwrap_or(30000));
// Build a per-check client with connect_timeout = monitor timeout.
// This ensures the OS-level TCP connect is bounded, since tokio future
// cancellation alone cannot interrupt a kernel-level SYN wait.
let check_client = reqwest::Client::builder()
.user_agent("PingQL-Monitor/0.1")
.connect_timeout(timeout)
.timeout(timeout)
.build()
.unwrap_or_else(|_| client.clone());
let req_method = reqwest::Method::from_bytes(method.as_bytes()) let req_method = reqwest::Method::from_bytes(method.as_bytes())
.unwrap_or(reqwest::Method::GET); .unwrap_or(reqwest::Method::GET);
let mut req = client.request(req_method, &monitor.url).timeout(timeout); let mut req = check_client.request(req_method, &monitor.url);
if let Some(headers) = &monitor.request_headers { if let Some(headers) = &monitor.request_headers {
for (k, v) in headers { for (k, v) in headers {
@ -93,8 +103,8 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
let is_https = monitor.url.starts_with("https://"); let is_https = monitor.url.starts_with("https://");
let url_for_cert = monitor.url.clone(); let url_for_cert = monitor.url.clone();
let timed = tokio::time::timeout(timeout, async { let result: Result<_, String> = async {
let resp = req.send().await?; let resp = req.send().await.map_err(|e| e.to_string())?;
let status = resp.status(); let status = resp.status();
let headers: HashMap<String, String> = resp.headers().iter() let headers: HashMap<String, String> = resp.headers().iter()
.filter_map(|(k, v)| Some((k.to_string(), v.to_str().ok()?.to_string()))) .filter_map(|(k, v)| Some((k.to_string(), v.to_str().ok()?.to_string())))
@ -106,23 +116,16 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
if content_len > MAX_BODY_BYTES { if content_len > MAX_BODY_BYTES {
format!("[body truncated: Content-Length {} exceeds 10MB limit]", content_len) format!("[body truncated: Content-Length {} exceeds 10MB limit]", content_len)
} else { } else {
let bytes = resp.bytes().await?; let bytes = resp.bytes().await.map_err(|e| e.to_string())?;
let truncated = &bytes[..bytes.len().min(MAX_BODY_BYTES)]; let truncated = &bytes[..bytes.len().min(MAX_BODY_BYTES)];
String::from_utf8_lossy(truncated).into_owned() String::from_utf8_lossy(truncated).into_owned()
} }
}; };
Ok::<_, reqwest::Error>((status, headers, body)) Ok((status, headers, body))
}).await; }.await;
let latency_ms = start.elapsed().as_millis() as u64; let latency_ms = start.elapsed().as_millis() as u64;
// Flatten timeout + reqwest errors into a single result
let result = match timed {
Err(_) => Err(format!("timed out after {}ms", timeout.as_millis())),
Ok(Err(e)) => Err(e.to_string()),
Ok(Ok(v)) => Ok(v),
};
match result { match result {
Err(e) => PingResult { Err(e) => PingResult {
monitor_id: monitor.id.clone(), monitor_id: monitor.id.clone(),