use std::any::Any;
use std::panic::AssertUnwindSafe;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::Weak;
use std::task::Context;
use std::task::Poll;
use std::task::ready;
use futures::FutureExt;
use tracing::Instrument;
use vortex_error::vortex_panic;
use crate::runtime::AbortHandleRef;
use crate::runtime::Executor;
#[derive(Clone)]
pub struct Handle {
runtime: Weak<dyn Executor>,
}
impl Handle {
pub fn new(runtime: Weak<dyn Executor>) -> Self {
Self { runtime }
}
fn runtime(&self) -> Arc<dyn Executor> {
self.runtime.upgrade().unwrap_or_else(|| {
vortex_panic!("Attempted to use a Handle after its runtime was dropped")
})
}
pub fn find() -> Option<Self> {
#[cfg(feature = "tokio")]
{
use tokio::runtime::Handle as TokioHandle;
use crate::runtime::tokio::TokioRuntime;
if TokioHandle::try_current().is_ok() {
return Some(TokioRuntime::current());
}
}
None
}
pub fn spawn<Fut, R>(&self, f: Fut) -> Task<R>
where
Fut: Future<Output = R> + Send + 'static,
R: Send + 'static,
{
let (send, recv) = oneshot::channel();
let span = tracing::trace_span!(target: "vortex_io::spawn", "spawn");
let abort_handle = self.runtime().spawn(
async move {
let output = AssertUnwindSafe(f).catch_unwind().await;
drop(send.send(output));
}
.instrument(span)
.boxed(),
);
Task {
recv: recv.into_future(),
abort_handle: Some(abort_handle),
}
}
pub fn spawn_nested<F, Fut, R>(&self, f: F) -> Task<R>
where
F: FnOnce(Handle) -> Fut,
Fut: Future<Output = R> + Send + 'static,
R: Send + 'static,
{
self.spawn(f(Handle::new(Weak::clone(&self.runtime))))
}
pub fn spawn_io<Fut, R>(&self, f: Fut) -> Task<R>
where
Fut: Future<Output = R> + Send + 'static,
R: Send + 'static,
{
let (send, recv) = oneshot::channel();
let span = tracing::trace_span!(target: "vortex_io::spawn_io", "spawn_io");
let abort_handle = self.runtime().spawn_io(
async move {
let output = AssertUnwindSafe(f).catch_unwind().await;
drop(send.send(output));
}
.instrument(span)
.boxed(),
);
Task {
recv: recv.into_future(),
abort_handle: Some(abort_handle),
}
}
pub fn spawn_cpu<F, R>(&self, f: F) -> Task<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let (send, recv) = oneshot::channel();
let span = tracing::Span::current();
let abort_handle = self.runtime().spawn_cpu(Box::new(move || {
let _guard = span.enter();
if !send.is_closed() {
let output = std::panic::catch_unwind(AssertUnwindSafe(f));
drop(send.send(output));
}
}));
Task {
recv: recv.into_future(),
abort_handle: Some(abort_handle),
}
}
pub fn spawn_blocking<F, R>(&self, f: F) -> Task<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let (send, recv) = oneshot::channel();
let span = tracing::Span::current();
let abort_handle = self.runtime().spawn_blocking_io(Box::new(move || {
let _guard = span.enter();
if !send.is_closed() {
let output = std::panic::catch_unwind(AssertUnwindSafe(f));
drop(send.send(output));
}
}));
Task {
recv: recv.into_future(),
abort_handle: Some(abort_handle),
}
}
}
type TaskOutput<T> = Result<T, Box<dyn Any + Send>>;
pub enum JoinOutcome<T> {
Completed(T),
Panicked(Box<dyn Any + Send>),
Aborted,
}
#[must_use = "When a Task is dropped without being awaited, it is cancelled"]
pub struct Task<T> {
recv: oneshot::AsyncReceiver<TaskOutput<T>>,
abort_handle: Option<AbortHandleRef>,
}
impl<T> Task<T> {
pub fn detach(mut self) {
drop(self.abort_handle.take());
}
pub fn poll_join(&mut self, cx: &mut Context<'_>) -> Poll<JoinOutcome<T>> {
match ready!(self.recv.poll_unpin(cx)) {
Ok(Ok(output)) => Poll::Ready(JoinOutcome::Completed(output)),
Ok(Err(panic)) => Poll::Ready(JoinOutcome::Panicked(panic)),
Err(_recv_err) => Poll::Ready(JoinOutcome::Aborted),
}
}
}
impl<T> Future for Task<T> {
type Output = T;
#[expect(clippy::panic)]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match ready!(self.get_mut().poll_join(cx)) {
JoinOutcome::Completed(output) => Poll::Ready(output),
JoinOutcome::Panicked(panic) => std::panic::resume_unwind(panic),
JoinOutcome::Aborted => {
panic!("Runtime dropped task without completing it")
}
}
}
}
impl<T> Drop for Task<T> {
fn drop(&mut self) {
if let Some(handle) = self.abort_handle.take() {
handle.abort();
}
}
}
#[cfg(test)]
mod tests {
use futures::task::noop_waker;
use super::*;
#[test]
fn poll_join_reports_aborted_when_channel_closed_without_value() {
let (send, recv) = oneshot::channel::<TaskOutput<()>>();
drop(send);
let mut task = Task::<()> {
recv: recv.into_future(),
abort_handle: None,
};
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
assert!(matches!(
task.poll_join(&mut cx),
Poll::Ready(JoinOutcome::Aborted)
));
}
#[test]
fn poll_join_reports_completed_value() {
let (send, recv) = oneshot::channel::<TaskOutput<u32>>();
drop(send.send(Ok(7)));
let mut task = Task::<u32> {
recv: recv.into_future(),
abort_handle: None,
};
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
assert!(matches!(
task.poll_join(&mut cx),
Poll::Ready(JoinOutcome::Completed(7))
));
}
}