use std::{
any::Any,
collections::HashMap,
error::Error,
fmt,
future::Future,
mem::transmute,
pin::Pin,
ptr::NonNull,
sync::atomic::{AtomicBool, Ordering},
task::{Context, Poll},
};
use tokio::{
runtime::{Handle, RuntimeFlavor},
task::{block_in_place, AbortHandle, Id, JoinSet},
};
type TokioJoinError = tokio::task::JoinError;
#[derive(Default)]
pub struct ScopedJoinSet<'scope, T>
where
T: 'static,
{
join_set: JoinSet<Option<T>>,
holders: HashMap<Id, FutureHolder<'scope, T>>,
}
impl<'scope, T> ScopedJoinSet<'scope, T>
where
T: 'static,
{
pub fn new() -> Self {
Self {
join_set: JoinSet::new(),
holders: HashMap::new(),
}
}
pub fn spawn<F>(&mut self, task: F)
where
F: Future<Output = T> + Send + 'scope,
T: Send,
{
let alive: NonNull<AtomicBool> = Box::leak(Box::new(AtomicBool::new(true))).into();
let future: NonNull<F> = Box::leak(Box::new(task)).into();
let weak_future = unsafe {
type Src<'scope, T> = NonNull<dyn Future<Output = T> + 'scope>;
type Dst<T> = NonNull<dyn Future<Output = T>>;
let future = transmute::<Src<'scope, T>, Dst<T>>(future);
WeakFuture { future, alive }
};
let handle = self.join_set.spawn(weak_future);
let holder = FutureHolder {
abort_handle: handle.clone(),
future,
alive,
};
self.holders.insert(handle.id(), holder);
}
pub fn is_empty(&self) -> bool {
self.join_set.is_empty()
}
pub async fn join_next(&mut self) -> Option<Result<T, JoinError>> {
match self.join_set.join_next_with_id().await? {
Ok((id, Some(value))) => {
self.holders.remove(&id);
Some(Ok(value))
}
Ok((id, None)) => {
self.holders.remove(&id);
Some(Err(JoinError::Cancelled))
}
Err(error) => {
self.holders.remove(&error.id());
Some(Err(error.into()))
}
}
}
}
unsafe impl<'scope, T> Send for ScopedJoinSet<'scope, T> {}
struct FutureHolder<'scope, T>
where
T: 'static,
{
abort_handle: AbortHandle,
future: NonNull<dyn Future<Output = T> + Send + 'scope>,
alive: NonNull<AtomicBool>,
}
impl<'scope, T> Drop for FutureHolder<'scope, T> {
fn drop(&mut self) {
self.abort_handle.abort();
if Handle::current().runtime_flavor() == RuntimeFlavor::MultiThread {
let alive = unsafe { self.alive.as_ref() };
if alive.load(Ordering::Acquire) {
block_in_place(|| {
while alive.load(Ordering::Acquire) {
std::hint::spin_loop();
}
});
}
drop(unsafe { Box::from_raw(self.future.as_ptr()) });
drop(unsafe { Box::from_raw(self.alive.as_mut()) });
} else {
drop(unsafe { Box::from_raw(self.future.as_ptr()) });
}
}
}
struct WeakFuture<T> {
future: NonNull<dyn Future<Output = T>>,
alive: NonNull<AtomicBool>,
}
impl<T> Drop for WeakFuture<T> {
fn drop(&mut self) {
if Handle::current().runtime_flavor() == RuntimeFlavor::MultiThread {
unsafe { self.alive.as_ref() }.store(false, Ordering::Release);
} else {
drop(unsafe { Box::from_raw(self.alive.as_mut()) });
}
}
}
unsafe impl<T: Send> Send for WeakFuture<T> {}
impl<T> Future for WeakFuture<T> {
type Output = Option<T>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let future_ref = unsafe { self.future.as_mut() };
let future_pin = unsafe { Pin::new_unchecked(future_ref) };
Future::poll(future_pin, cx).map(Some)
}
}
#[derive(Debug)]
pub enum JoinError {
Cancelled,
Panicked(Box<dyn Any + Send + 'static>),
}
impl fmt::Display for JoinError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JoinError::Cancelled => write!(f, "task was cancelled"),
JoinError::Panicked(_) => write!(f, "task panicked"),
}
}
}
impl Error for JoinError {}
impl JoinError {
pub fn is_cancelled(&self) -> bool {
matches!(self, JoinError::Cancelled)
}
pub fn is_panic(&self) -> bool {
matches!(self, JoinError::Panicked(_))
}
}
impl From<TokioJoinError> for JoinError {
fn from(err: TokioJoinError) -> Self {
if err.is_cancelled() {
JoinError::Cancelled
} else if err.is_panic() {
JoinError::Panicked(err.into_panic())
} else {
JoinError::Cancelled
}
}
}