Files
tsunagi/crates/tsunagi-core/src/task.rs
T

74 lines
2.8 KiB
Rust
Raw Normal View History

2026-09-21 20:41:19 +01:00
//! Stopping the tasks an agent started.
//!
//! Shutdown is cooperative first and bounded always: every loop watches a
//! cancellation token, and anything still running when its grace period ends
//! is aborted. Nothing an agent owns may outlive the agent, and no peer may
//! hold shutdown open by refusing to read.
//!
//! Public because a protocol crate spawns tasks of its own and is held to
//! the same rule: the agent gives each plugin a grace period, but abandoning
//! the future it is awaiting does not stop the task behind it.
use std::time::Duration;
use tokio::task::JoinHandle;
/// How long a task gets to notice cancellation before it is cut off.
///
/// Winding down is cooperative first: a task in the middle of a write to a
/// peer gets a moment to finish it. After that it is aborted, because a peer
/// that stops reading must not be able to hold shutdown open.
pub const TASK_GRACE: Duration = Duration::from_secs(5);
/// Waits for `task` to finish, aborting it if it outlasts `grace`.
///
/// Returns whether it finished on its own. `what` names it in the warning,
/// which is the only signal that something was cut off rather than stopped.
pub async fn wind_down(task: JoinHandle<()>, grace: Duration, what: &str) -> bool {
let abort = task.abort_handle();
if tokio::time::timeout(grace, task).await.is_err() {
tracing::warn!(
task = what,
grace_secs = grace.as_secs_f32(),
"task did not wind down in time; aborting it"
);
abort.abort();
return false;
}
true
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
#[tokio::test]
async fn a_task_that_stops_on_its_own_is_not_aborted() {
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1);
let task = tokio::spawn(async move {
tx.send(()).await.ok();
});
assert!(wind_down(task, Duration::from_secs(5), "cooperative").await);
assert_eq!(rx.recv().await, Some(()), "it ran to completion");
}
#[tokio::test]
async fn a_task_that_ignores_the_grace_is_aborted() {
// A stand-in for a task blocked writing to a peer that stopped
// reading: it will never finish by itself.
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1);
let task = tokio::spawn(async move {
let _held = tx;
std::future::pending::<()>().await;
});
let finished = wind_down(task, Duration::from_millis(50), "wedged").await;
assert!(!finished, "it had to be cut off");
// Aborted for real: the task is gone, so what it held is dropped.
let closed = tokio::time::timeout(Duration::from_secs(5), rx.recv()).await;
assert_eq!(closed, Ok(None), "the aborted task dropped its sender");
}
}