#[allow(dead_code)]
pub(crate) struct PhaseHandle<E> {
pub rx: tokio::sync::mpsc::Receiver<E>,
pub task: tokio::task::JoinHandle<()>,
}
impl<E> Drop for PhaseHandle<E> {
fn drop(&mut self) {
self.task.abort();
}
}
impl<E: Send + 'static> PhaseHandle<E> {
#[allow(dead_code)] pub(crate) fn spawn<F, Fut>(capacity: usize, body: F) -> Self
where
F: FnOnce(tokio::sync::mpsc::Sender<E>) -> Fut,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
let (tx, rx) = tokio::sync::mpsc::channel::<E>(capacity.max(1));
let task = tokio::spawn(body(tx));
PhaseHandle { rx, task }
}
}
#[cfg(feature = "plugin")]
pub(crate) fn spawn_detached_plugin<F>(
pm: std::sync::Arc<std::sync::Mutex<crate::plugin::PluginManager>>,
label: &'static str,
body: F,
) where
F: FnOnce(&mut crate::plugin::PluginManager) + Send + 'static,
{
tokio::spawn(async move {
let joined = tokio::task::spawn_blocking(move || {
use crate::sync_util::LockExt;
let mut mgr = pm.lock_ignore_poison();
body(&mut mgr);
})
.await;
if let Err(e) = joined {
tracing::warn!(
target: "dirge::plugin",
hook = label,
error = %e,
"detached plugin hook task panicked",
);
}
});
}
#[cfg(test)]
mod tests {
use super::PhaseHandle;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
#[tokio::test]
async fn drop_aborts_the_task() {
let ran_to_completion = Arc::new(AtomicBool::new(false));
let flag = ran_to_completion.clone();
let handle = PhaseHandle::<()>::spawn(1, move |_tx| async move {
tokio::time::sleep(Duration::from_secs(30)).await;
flag.store(true, Ordering::SeqCst);
});
let abort = handle.task.abort_handle();
drop(handle);
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(
abort.is_finished(),
"dropping the handle must abort the task"
);
assert!(
!ran_to_completion.load(Ordering::SeqCst),
"aborted task must not have run to completion"
);
}
#[tokio::test]
async fn terminal_event_is_delivered() {
let mut handle = PhaseHandle::<u32>::spawn(1, |tx| async move {
let _ = tx.send(42).await;
});
assert_eq!(handle.rx.recv().await, Some(42));
}
#[cfg(feature = "plugin")]
#[tokio::test]
async fn detached_plugin_hook_still_fires() {
use crate::plugin::PluginManager;
use crate::sync_util::LockExt;
use std::sync::{Arc, Mutex};
let mut manager = PluginManager::try_new().unwrap();
manager.eval("(var fired false)").unwrap();
manager
.eval("(defn on-turn-start [ctx] (set fired true) nil)")
.unwrap();
manager.register("on-turn-start", "on-turn-start");
let pm = Arc::new(Mutex::new(manager));
super::spawn_detached_plugin(pm.clone(), "on-turn-start", |mgr| {
let _ = mgr.dispatch("on-turn-start", "@{:index 0}");
});
let mut fired = false;
for _ in 0..200 {
if pm.lock_ignore_poison().eval("fired").as_deref() == Ok("true") {
fired = true;
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(fired, "detached on-turn-start hook never fired");
}
}