use std::future::Future;
use tokio::runtime::Handle;
use tokio::task::JoinError;
pub(crate) fn spawn_supervised<F>(
runtime: &Handle,
lane: &'static str,
job: F,
report_lost: impl FnOnce(String) + Send + 'static,
) where
F: Future<Output = ()> + Send + 'static,
{
let inner = runtime.clone();
runtime.spawn(async move {
let Err(e) = inner.spawn(job).await else {
return; };
let message = lost_lane_message(lane, e);
tracing::error!(lane, %message, "an async lane task died without reporting");
report_lost(message);
});
}
pub(crate) fn lost_lane_message(lane: &str, err: JoinError) -> String {
if !err.is_panic() {
return format!("the {lane} task was cancelled before it reported a result");
}
let payload = err.into_panic();
format!(
"the {lane} task panicked and reported nothing: {}",
leviath_core::panic_message(payload.as_ref())
)
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::sync::mpsc;
use crate::test_support::SilentPanics;
fn forward_to(tx: mpsc::UnboundedSender<String>) -> impl FnOnce(String) + Send + 'static {
move |message| {
let _ = tx.send(message);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_panicking_job_is_reported_instead_of_vanishing() {
let _silent = SilentPanics::install();
let (tx, mut rx) = mpsc::unbounded_channel();
spawn_supervised(
&Handle::current(),
"inference",
async {
panic!("the provider adapter blew up");
},
forward_to(tx),
);
let message = tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv())
.await
.expect("the supervisor reports promptly")
.expect("a message");
assert!(message.contains("inference"), "got: {message}");
assert!(
message.contains("the provider adapter blew up"),
"the panic text must survive so the run says why it failed, got: {message}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_job_that_reports_for_itself_is_left_alone() {
let (tx, mut rx) = mpsc::unbounded_channel();
spawn_supervised(&Handle::current(), "inference", async {}, forward_to(tx));
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(
rx.try_recv().is_err(),
"a job that returned normally owes no synthesized outcome"
);
}
#[tokio::test]
async fn lost_lane_message_names_the_lane_and_the_ending() {
let silent = SilentPanics::install();
let err = tokio::spawn(async { panic!("boom") })
.await
.expect_err("the task panicked");
drop(silent);
let message = lost_lane_message("compaction", err);
assert!(message.contains("compaction"), "got: {message}");
assert!(message.contains("boom"), "got: {message}");
let handle = tokio::spawn(std::future::pending::<()>());
tokio::task::yield_now().await;
handle.abort();
let err = handle.await.expect_err("the task was aborted");
let message = lost_lane_message("transition", err);
assert!(message.contains("transition"), "got: {message}");
assert!(message.contains("cancelled"), "got: {message}");
}
}