use std::sync::Arc;
use tokio::task::{AbortHandle, JoinHandle};
use tracing::Instrument;
use rustvello_core::context::{InvocationContext, RunnerContext, INVOCATION_CTX, RUNNER_CTX};
use rustvello_core::error::{RustvelloError, RustvelloResult};
use rustvello_core::observability::in_w3c_trace_context;
use rustvello_core::task::DynTask;
use rustvello_proto::call::SerializedArguments;
use rustvello_proto::identifiers::InvocationId;
pub(crate) const TASK_CANCELLED: &str = "TaskCancelled";
struct AbortOnDrop(AbortHandle);
impl Drop for AbortOnDrop {
fn drop(&mut self) {
self.0.abort();
}
}
pub(crate) async fn execute_native_async(
task: Arc<dyn DynTask>,
args: SerializedArguments,
invocation_context: InvocationContext,
runner_context: RunnerContext,
) -> RustvelloResult<String> {
let invocation_id = invocation_context.invocation_id.clone();
let body = in_w3c_trace_context(&invocation_context.trace_context, task.execute_async(args));
let body = INVOCATION_CTX
.scope(invocation_context, RUNNER_CTX.scope(runner_context, body))
.instrument(tracing::Span::current());
let handle = tokio::spawn(body);
let _abort_on_drop = AbortOnDrop(handle.abort_handle());
join_task_body(handle, &invocation_id).await
}
async fn join_task_body(
handle: JoinHandle<RustvelloResult<String>>,
invocation_id: &InvocationId,
) -> RustvelloResult<String> {
match handle.await {
Ok(result) => result,
Err(error) if error.is_panic() => Err(crate::runner::executor_common::unwrap_panic(
error.into_panic(),
)),
Err(error) => Err(RustvelloError::TaskExecution {
error_type: TASK_CANCELLED.to_owned(),
message: format!(
"async task body for invocation {invocation_id} was cancelled: {error}"
),
traceback: None,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn aborted_body_is_a_cancelled_task_error() {
let handle = tokio::spawn(std::future::pending::<RustvelloResult<String>>());
handle.abort();
let error = join_task_body(handle, &InvocationId::new())
.await
.unwrap_err();
assert!(
matches!(error, RustvelloError::TaskExecution { ref error_type, .. } if error_type == TASK_CANCELLED),
"{error:?}"
);
}
#[tokio::test]
async fn panicking_body_is_a_task_error() {
let handle = tokio::spawn(async {
tokio::task::yield_now().await;
panic!("boom");
});
let error = join_task_body(handle, &InvocationId::new())
.await
.unwrap_err();
assert!(error.to_string().contains("task panicked: boom"), "{error}");
}
#[tokio::test]
async fn dropping_the_executor_future_aborts_the_body() {
use std::sync::atomic::{AtomicBool, Ordering};
struct SetOnDrop(Arc<AtomicBool>);
impl Drop for SetOnDrop {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
let dropped = Arc::new(AtomicBool::new(false));
let guard = SetOnDrop(Arc::clone(&dropped));
let body = tokio::spawn(async move {
let _guard = guard;
std::future::pending::<RustvelloResult<String>>().await
});
let waiter = {
let abort = AbortOnDrop(body.abort_handle());
async move {
let _abort = abort;
join_task_body(body, &InvocationId::new()).await
}
};
assert!(
tokio::time::timeout(std::time::Duration::from_millis(10), waiter)
.await
.is_err()
);
for _ in 0..100 {
if dropped.load(Ordering::SeqCst) {
break;
}
tokio::task::yield_now().await;
}
assert!(dropped.load(Ordering::SeqCst));
}
}