use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::QueryEngine as AdvancedQueryEngine;
#[cfg(feature = "state_machine")]
use crate::query::result::{QueryResultIterator, StreamingConfig};
use crate::query::{executor::QueryResult, prepared::PreparedQuery};
use crate::{Error, Result, Value};
pub(crate) type BoundedFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
pub(crate) async fn bound<T>(
limit: Duration,
operation: &str,
fut: BoundedFuture<'_, T>,
) -> Result<T> {
bound_tracked(limit, operation, fut).await.0
}
async fn bound_tracked<T>(
limit: Duration,
operation: &str,
fut: BoundedFuture<'_, T>,
) -> (Result<T>, bool) {
if limit.is_zero() {
return (fut.await, true);
}
let started = Instant::now();
let Some(deadline) = started.checked_add(limit) else {
return (fut.await, true);
};
bound_tracked_until(deadline, started, limit, operation, fut).await
}
async fn bound_tracked_until<T>(
deadline: Instant,
started: Instant,
limit: Duration,
operation: &str,
fut: BoundedFuture<'_, T>,
) -> (Result<T>, bool) {
let expired = |operation: &str| Error::QueryTimeout {
operation: operation.to_string(),
elapsed: started.elapsed(),
limit,
};
let inner_started = Arc::new(AtomicBool::new(false));
let mut inner = fut;
let checked = std::future::poll_fn({
let inner_started = Arc::clone(&inner_started);
move |cx| {
if Instant::now() >= deadline {
return std::task::Poll::Ready(Err(expired(operation)));
}
inner_started.store(true, Ordering::Relaxed);
inner.as_mut().poll(cx)
}
});
let out = match tokio::time::timeout(limit, checked).await {
Ok(inner) => inner,
Err(_elapsed) => Err(Error::QueryTimeout {
operation: operation.to_string(),
elapsed: started.elapsed(),
limit,
}),
};
(out, inner_started.load(Ordering::Relaxed))
}
impl AdvancedQueryEngine {
async fn bounded<T>(&self, operation: &str, fut: BoundedFuture<'_, T>) -> Result<T> {
let (out, inner_started) =
bound_tracked(self.config.query.max_execution_time, operation, fut).await;
out.inspect_err(|e| {
if matches!(e, Error::QueryTimeout { .. }) {
if !inner_started {
self.inc_total_queries();
}
self.inc_error_queries();
crate::observability::record_error(e, "query");
}
})
}
#[tracing::instrument(
name = "query.execute",
skip(self, cql),
fields(
cqlite.query.plan_type = tracing::field::Empty,
cqlite.query.access_path = tracing::field::Empty,
cqlite.query.rows = tracing::field::Empty,
)
)]
pub async fn execute(&self, cql: &str) -> Result<QueryResult> {
self.bounded("query.execute", Box::pin(self.execute_inner(cql)))
.await
}
#[cfg(feature = "state_machine")]
pub async fn execute_streaming(
&self,
cql: &str,
config: StreamingConfig,
) -> Result<QueryResultIterator> {
self.bounded(
"query.execute_streaming",
Box::pin(self.execute_streaming_inner(cql, config)),
)
.await
}
#[tracing::instrument(
name = "query.execute",
skip(self, cql, params),
fields(
cqlite.query.plan_type = tracing::field::Empty,
cqlite.query.access_path = tracing::field::Empty,
cqlite.query.rows = tracing::field::Empty,
)
)]
pub async fn execute_with_params(&self, cql: &str, params: &[Value]) -> Result<QueryResult> {
self.bounded(
"query.execute_with_params",
Box::pin(self.execute_with_params_inner(cql, params)),
)
.await
}
pub async fn execute_prepared(
&self,
prepared: &PreparedQuery,
params: &[Value],
) -> Result<QueryResult> {
self.bounded(
"query.execute_prepared",
Box::pin(self.execute_prepared_inner(prepared, params)),
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn zero_limit_is_unbounded() {
let out: Result<u32> = bound(
Duration::ZERO,
"test.zero",
Box::pin(async {
for _ in 0..64 {
tokio::task::yield_now().await;
}
Ok(7)
}),
)
.await;
assert_eq!(out.expect("ZERO must not bound anything"), 7);
}
#[tokio::test]
async fn elapsed_budget_yields_query_timeout() {
let limit = Duration::from_millis(1);
let out: Result<u32> = bound(limit, "test.pending", Box::pin(std::future::pending())).await;
match out {
Err(Error::QueryTimeout {
operation,
limit: reported,
..
}) => {
assert_eq!(operation, "test.pending");
assert_eq!(reported, limit);
}
other => panic!("expected Error::QueryTimeout, got {other:?}"),
}
}
#[tokio::test]
async fn timeout_error_is_distinct_from_corruption() {
let err = bound::<u32>(
Duration::from_millis(1),
"test.classify",
Box::pin(std::future::pending()),
)
.await
.expect_err("must elapse");
assert_eq!(
err.obs_category(),
crate::observability::ObsErrorCategory::Timeout
);
assert_ne!(
err.obs_category(),
crate::observability::ObsErrorCategory::Corruption
);
assert_eq!(err.category(), crate::error::ErrorCategory::Query);
assert!(!err.is_recoverable());
}
#[tokio::test]
async fn inner_outcome_passes_through() {
let ok: Result<u32> = bound(
Duration::from_secs(300),
"test.ok",
Box::pin(async { Ok(3) }),
)
.await;
assert_eq!(ok.expect("must pass through"), 3);
let err: Result<u32> = bound(
Duration::from_secs(300),
"test.err",
Box::pin(async { Err(Error::corruption("inner")) }),
)
.await;
assert!(matches!(
err.expect_err("inner Err must be relayed"),
Error::Corruption(_)
));
}
#[tokio::test]
async fn an_expired_budget_reports_the_inner_future_as_unstarted() {
let polled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let seen = std::sync::Arc::clone(&polled);
let past = Instant::now()
.checked_sub(Duration::from_secs(1))
.expect("the process clock is at least 1s past its origin");
let (out, started): (Result<u32>, bool) = bound_tracked_until(
past,
past,
Duration::from_secs(30),
"test.unstarted",
Box::pin(async move {
seen.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(1)
}),
)
.await;
assert!(matches!(out, Err(Error::QueryTimeout { .. })));
assert!(
!polled.load(std::sync::atomic::Ordering::SeqCst),
"precondition: the expired budget must short-circuit BEFORE the body runs"
);
assert!(
!started,
"an inner future that was never polled must be reported as unstarted"
);
}
#[tokio::test]
async fn a_polled_inner_future_is_reported_as_started() {
let (out, started): (Result<u32>, bool) = bound_tracked(
Duration::from_secs(300),
"test.started",
Box::pin(async { Ok(5) }),
)
.await;
assert_eq!(out.expect("must complete"), 5);
assert!(started, "a polled inner future must be reported as started");
let (zero, zero_started): (Result<u32>, bool) =
bound_tracked(Duration::ZERO, "test.zero", Box::pin(async { Ok(6) })).await;
assert_eq!(zero.expect("must complete"), 6);
assert!(
zero_started,
"the unbounded sentinel awaits the inner future directly, so it started"
);
}
#[tokio::test]
async fn elapse_drops_the_inner_future() {
struct DropFlag(std::sync::Arc<std::sync::atomic::AtomicBool>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
let dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let flag = DropFlag(std::sync::Arc::clone(&dropped));
let out: Result<u32> = bound(
Duration::from_millis(1),
"test.drop",
Box::pin(async move {
let _held = flag;
std::future::pending::<()>().await;
Ok(0)
}),
)
.await;
assert!(matches!(out, Err(Error::QueryTimeout { .. })));
assert!(
dropped.load(std::sync::atomic::Ordering::SeqCst),
"the timed-out future must be DROPPED (releasing what it held), not detached"
);
}
}