use std::time::{Duration, Instant};
use tokio::sync::oneshot;
const PROBE_TIMEOUT_SECS: u64 = 5;
const CACHE_TTL_SECS: u64 = 30;
pub type ProbeCache = tokio::sync::Mutex<Option<(bool, Instant)>>;
pub async fn probe_httpfs_cached(cache: &ProbeCache) -> bool {
let mut guard = cache.lock().await;
if let Some((result, ts)) = *guard
&& ts.elapsed().as_secs() < CACHE_TTL_SECS
{
return result;
}
let result = probe_httpfs().await;
*guard = Some((result, Instant::now()));
result
}
pub async fn probe_httpfs() -> bool {
let (tx, rx) = oneshot::channel();
let join = tokio::task::spawn_blocking(move || {
let conn = match duckdb::Connection::open_in_memory() {
Ok(c) => c,
Err(e) => {
tracing::warn!(error = %e, "httpfs probe: failed to open in-memory connection");
return (false, None::<duckdb::Connection>);
}
};
let _ = tx.send(conn.interrupt_handle());
let ok = match conn.execute_batch("INSTALL httpfs;\nLOAD httpfs;") {
Ok(()) => {
tracing::debug!("httpfs probe: extension available");
true
}
Err(e) => {
tracing::warn!(error = %e, "httpfs probe: extension unavailable");
false
}
};
(ok, Some(conn))
});
let watchdog = tokio::spawn(async move {
if let Ok(handle) = rx.await {
tokio::time::sleep(Duration::from_secs(PROBE_TIMEOUT_SECS)).await;
handle.interrupt();
}
});
let joined = join.await;
watchdog.abort();
let _ = watchdog.await;
match joined {
Ok((ok, _conn)) => ok,
Err(e) => {
tracing::warn!(error = %e, "httpfs probe: blocking task panicked");
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[ignore = "requires network access to download the httpfs extension"]
#[tokio::test]
async fn probe_httpfs_available() {
assert!(
probe_httpfs().await,
"httpfs extension should load successfully"
);
}
#[tokio::test]
async fn probe_completes_promptly() {
let _result = probe_httpfs().await;
}
#[tokio::test]
async fn cached_probe_returns_cached_result() {
let cache = ProbeCache::new(Some((true, Instant::now())));
assert!(probe_httpfs_cached(&cache).await);
}
#[tokio::test]
async fn cached_probe_re_runs_when_expired() {
let old = Instant::now() - Duration::from_secs(CACHE_TTL_SECS + 1);
let cache = ProbeCache::new(Some((true, old)));
let _result = probe_httpfs_cached(&cache).await;
}
}