use core::{marker::PhantomData, panic::AssertUnwindSafe, pin::Pin};
use futures::FutureExt;
use kameo::{
actor::{ActorRef, WeakActorRef},
error::{ActorStopReason, Infallible},
message::{Context, Message},
};
use tokio_util::task::AbortOnDropHandle;
pub struct Task<Fut>(Option<AbortOnDropHandle<()>>, PhantomData<Fut>);
pub type ErasedTask = Task<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>;
async fn run_fut<Fut>(fut: Fut, slf: ActorRef<Task<Fut>>)
where
Fut: Future<Output = ()> + Send + 'static,
{
match AssertUnwindSafe(fut).catch_unwind().await {
Ok(_) => {
if slf.stop_gracefully().await.is_err() {
slf.kill();
}
}
Err(e) => {
slf.kill();
std::panic::resume_unwind(e)
}
}
}
impl<Fut> kameo::Actor for Task<Fut>
where
Fut: Future<Output = ()> + Send + 'static,
{
type Args = Fut;
type Error = core::convert::Infallible;
async fn on_start(fut: Self::Args, slf: ActorRef<Self>) -> Result<Self, Self::Error> {
Ok(Self(
Some(AbortOnDropHandle::new(tokio::spawn(run_fut(fut, slf)))),
PhantomData,
))
}
async fn on_stop(
&mut self,
slf: WeakActorRef<Self>,
reason: ActorStopReason,
) -> Result<(), Self::Error> {
tracing::trace!(?slf, ?reason, "task stopping");
let handle = self.0.take().unwrap();
if handle.is_finished()
&& let Err(e) = handle.detach().await
{
std::panic::resume_unwind(e.into_panic())
}
Ok(())
}
}
impl<Fut> Message<Infallible> for Task<Fut>
where
Fut: Future<Output = ()> + Send + 'static,
{
type Reply = Infallible;
async fn handle(&mut self, _: Infallible, _: &mut Context<Self, Self::Reply>) -> Infallible {
unreachable!()
}
}
#[cfg(test)]
mod test {
use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use kameo::{actor::Spawn, error::HookError};
use super::*;
#[tokio::test]
async fn kill_stops_task() {
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let task = Task::spawn(async move {
loop {
tx.send(()).await.unwrap();
}
});
for _ in 0..5 {
rx.recv().await.unwrap();
}
task.kill();
task.wait_for_shutdown().await;
rx.recv().await.unwrap();
assert!(rx.is_closed());
assert!(rx.is_empty());
}
#[tokio::test]
async fn task_complete_stops_actor() {
let notify = Arc::new(tokio::sync::Notify::new());
let completed = Arc::new(AtomicBool::new(false));
let task = Task::spawn({
let notify = notify.clone();
let completed = completed.clone();
async move {
notify.notified().await;
completed.store(true, Ordering::SeqCst);
}
});
assert!(task.is_alive());
assert!(!completed.load(Ordering::SeqCst));
notify.notify_one();
tokio::time::timeout(Duration::from_millis(100), task.wait_for_shutdown())
.await
.unwrap();
assert!(!task.is_alive());
assert!(completed.load(Ordering::SeqCst));
let result = task.get_shutdown_result().unwrap();
assert!(result.is_ok());
}
#[tokio::test]
async fn task_panic_stops_actor() {
let notify = Arc::new(tokio::sync::Notify::new());
let task = Task::spawn({
let notify = notify.clone();
async move {
notify.notified().await;
panic!();
}
});
assert!(task.is_alive());
notify.notify_one();
tokio::time::timeout(Duration::from_millis(100), task.wait_for_shutdown())
.await
.unwrap();
assert!(!task.is_alive());
let result = task.get_shutdown_result().unwrap().unwrap_err();
assert!(matches!(result, HookError::Panicked(_)));
}
}