use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::task::JoinHandle;
pub struct PreflightHandle<T> {
handle: Option<JoinHandle<T>>,
}
impl<T> PreflightHandle<T>
where
T: Send + 'static,
{
pub fn spawn<F>(fut: F) -> Self
where
F: Future<Output = T> + Send + 'static,
{
Self {
handle: Some(tokio::spawn(fut)),
}
}
pub fn cancel(mut self) {
if let Some(h) = self.handle.take() {
h.abort();
}
}
pub fn is_finished(&self) -> bool {
self.handle
.as_ref()
.map(|h| h.is_finished())
.unwrap_or(true)
}
}
impl<T> Future for PreflightHandle<T>
where
T: Send + 'static,
{
type Output = Result<T, PreflightError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let Some(h) = self.handle.as_mut() else {
return Poll::Ready(Err(PreflightError::Cancelled));
};
match Pin::new(h).poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Ok(v)) => {
self.handle = None;
Poll::Ready(Ok(v))
}
Poll::Ready(Err(join_err)) => {
self.handle = None;
Poll::Ready(Err(if join_err.is_cancelled() {
PreflightError::Cancelled
} else {
PreflightError::Panicked
}))
}
}
}
}
impl<T> Drop for PreflightHandle<T> {
fn drop(&mut self) {
if let Some(h) = self.handle.take() {
h.abort();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PreflightError {
Cancelled,
Panicked,
}
impl std::fmt::Display for PreflightError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PreflightError::Cancelled => write!(f, "preflight task was cancelled"),
PreflightError::Panicked => write!(f, "preflight task panicked"),
}
}
}
impl std::error::Error for PreflightError {}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[tokio::test]
async fn handle_returns_value_when_spawned_task_completes() {
let h = PreflightHandle::spawn(async { 42_i32 });
let v = h.await;
assert_eq!(v, Ok(42));
}
#[tokio::test]
async fn handle_returns_value_for_long_task_when_awaited_after_completion() {
let h = PreflightHandle::spawn(async {
tokio::time::sleep(Duration::from_millis(50)).await;
"ok"
});
tokio::time::sleep(Duration::from_millis(100)).await;
let v = h.await;
assert_eq!(v, Ok("ok"));
}
#[tokio::test]
async fn explicit_cancel_aborts_task_and_subsequent_await_errors() {
let started = Arc::new(AtomicBool::new(false));
let started2 = started.clone();
let h = PreflightHandle::spawn(async move {
started2.store(true, Ordering::SeqCst);
tokio::time::sleep(Duration::from_secs(60)).await;
"completed"
});
tokio::time::sleep(Duration::from_millis(10)).await;
assert!(started.load(Ordering::SeqCst));
h.cancel();
}
#[tokio::test]
async fn drop_aborts_task() {
let started = Arc::new(AtomicBool::new(false));
let still_running = Arc::new(AtomicBool::new(true));
{
let s2 = started.clone();
let r2 = still_running.clone();
let _h = PreflightHandle::spawn(async move {
s2.store(true, Ordering::SeqCst);
tokio::time::sleep(Duration::from_secs(60)).await;
r2.store(false, Ordering::SeqCst);
});
tokio::time::sleep(Duration::from_millis(10)).await;
}
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(started.load(Ordering::SeqCst), "task should have started");
assert!(
still_running.load(Ordering::SeqCst),
"cancelled task must not run to completion"
);
}
#[tokio::test]
async fn is_finished_reflects_task_state() {
let h = PreflightHandle::spawn(async {
tokio::time::sleep(Duration::from_millis(20)).await;
7_u32
});
assert!(!h.is_finished(), "should not be finished immediately");
tokio::time::sleep(Duration::from_millis(60)).await;
assert!(h.is_finished(), "should be finished after sleep");
let v = h.await;
assert_eq!(v, Ok(7));
}
#[tokio::test]
async fn polling_after_panic_returns_panicked_error() {
let h: PreflightHandle<i32> = PreflightHandle::spawn(async {
#[allow(clippy::panic)]
{
panic!("boom");
}
});
let v = h.await;
assert_eq!(v, Err(PreflightError::Panicked));
}
#[test]
fn preflight_error_implements_display_and_error_traits() {
let cancelled: Box<dyn std::error::Error> = Box::new(PreflightError::Cancelled);
let panicked: Box<dyn std::error::Error> = Box::new(PreflightError::Panicked);
assert!(cancelled.to_string().contains("cancelled"));
assert!(panicked.to_string().contains("panicked"));
}
}