use futures_util::FutureExt;
use std::{future::Future, panic::AssertUnwindSafe};
use tokio::task::JoinHandle;
use tracing::{error, info};
pub fn spawn_monitored<Fut>(name: &'static str, fut: Fut) -> JoinHandle<()>
where
Fut: Future<Output = ()> + Send + 'static,
{
tokio::spawn(async move {
let result = AssertUnwindSafe(fut).catch_unwind().await;
match result {
Ok(()) => {
info!(task = name, "monitored task exited cleanly");
}
Err(panic_info) => {
let panic_msg = extract_panic_message(&panic_info);
error!(
task = name,
panic = %panic_msg,
"monitored task panicked"
);
}
}
})
}
fn extract_panic_message(panic_info: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = panic_info.downcast_ref::<String>() {
s.clone()
} else if let Some(s) = panic_info.downcast_ref::<&str>() {
s.to_string()
} else {
format!("{:?}", panic_info)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn clean_exit_logs_info_and_completes() {
let handle = spawn_monitored("test_clean", async {});
handle.await.expect("monitored handle joins cleanly");
}
#[tokio::test]
async fn panic_is_caught_and_handle_completes() {
let handle = spawn_monitored("test_panic", async {
panic!("test panic message");
});
handle.await.expect("panic absorbed; handle completes Ok");
}
#[test]
fn extract_panic_message_handles_string_and_str() {
let s: Box<dyn std::any::Any + Send> = Box::new(String::from("owned"));
assert_eq!(extract_panic_message(&s), "owned");
let r: Box<dyn std::any::Any + Send> = Box::new("static");
assert_eq!(extract_panic_message(&r), "static");
}
}