use std::time::Instant;
use reqwest::Client;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::engine::event::{emit, EngineEvent};
use crate::engine::metrics::LatencyCalculator;
use crate::engine::models::LatencyStats;
use crate::engine::PingConfig;
use crate::errors::{EngineError, EngineResult};
pub async fn measure_latency(
client: &Client,
url: &str,
config: &PingConfig,
events: &mpsc::Sender<EngineEvent>,
cancel: &CancellationToken,
) -> EngineResult<LatencyStats> {
let mut calculator = LatencyCalculator::new();
let mut last_failure: Option<String> = None;
for sequence in 1..=config.samples {
if cancel.is_cancelled() {
return Err(EngineError::Cancelled);
}
let request = client
.get(cache_busted(url, sequence))
.header(reqwest::header::CACHE_CONTROL, "no-cache")
.send();
let started = Instant::now();
tokio::select! {
() = cancel.cancelled() => return Err(EngineError::Cancelled),
outcome = tokio::time::timeout(config.timeout, request) => {
match outcome {
Ok(Ok(response)) if response.status().is_success() => {
let latency_ms = started.elapsed().as_secs_f64() * 1000.0;
calculator.record(latency_ms);
emit(events, EngineEvent::PingSample { sequence, latency_ms }).await?;
}
Ok(Ok(response)) => {
calculator.record_failure();
last_failure = Some(format!("server returned HTTP {}", response.status()));
}
Ok(Err(err)) => {
calculator.record_failure();
last_failure = Some(root_cause(&err));
}
Err(_) => {
calculator.record_failure();
last_failure = Some(format!(
"no response within {:.0}s",
config.timeout.as_secs_f64()
));
}
}
}
}
if sequence < config.samples {
tokio::select! {
() = cancel.cancelled() => return Err(EngineError::Cancelled),
() = tokio::time::sleep(config.interval) => {}
}
}
}
calculator.stats().ok_or_else(|| EngineError::NoSamples {
reason: last_failure.unwrap_or_else(|| "no requests were attempted".to_owned()),
})
}
fn root_cause(err: &dyn std::error::Error) -> String {
let mut current = err;
while let Some(source) = current.source() {
current = source;
}
current.to_string()
}
fn cache_busted(url: &str, sequence: u32) -> String {
let separator = if url.contains('?') { '&' } else { '?' };
format!("{url}{separator}r={sequence}")
}