diff --git a/crates/hotkey/src/supervisor.rs b/crates/hotkey/src/supervisor.rs index 3c6274f..b8a5c74 100644 --- a/crates/hotkey/src/supervisor.rs +++ b/crates/hotkey/src/supervisor.rs @@ -26,10 +26,12 @@ use tokio::task::JoinHandle; use tokio::time::timeout; /// How long to wait for any single task to drain on `shutdown()` before -/// we give up and abort it. Two seconds is generous for cooperative -/// shutdown via the broadcast channel — anything slower is treated as a -/// stuck task and force-aborted with a warning so the operator can -/// investigate. +/// we give up on it. Two seconds is generous for cooperative shutdown +/// via the broadcast channel — anything slower is treated as a stuck +/// task and detached (NOT aborted: `timeout(d, handle).await` consumes +/// the `JoinHandle` by value and dropping a `JoinHandle` detaches the +/// task, so the task keeps running until the tokio runtime tears it +/// down). A warning is logged so the operator can investigate. const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2); /// Capacity of the broadcast shutdown channel. Eight is far more than we @@ -207,8 +209,16 @@ mod tests { ); } + /// A stuck task — one that does not subscribe to broadcast shutdown + /// and would otherwise run forever — must not block `shutdown()` + /// past the per-task timeout. Note: the supervisor does NOT abort + /// the task; it detaches it. Verifying detach behaviour directly is + /// not possible from this test because `register()` moves the + /// `JoinHandle` into the supervisor's inner Vec. The bounded-elapsed + /// assertion below is what guards against a regression that + /// reintroduces an unbounded `handle.await` in `shutdown()`. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn shutdown_force_aborts_stuck_tasks_after_timeout() { + async fn shutdown_does_not_block_on_stuck_tasks_after_timeout() { let sup = SupervisorHandle::new(); let handle = tokio::spawn(async move { // Sleep forever — does NOT subscribe to shutdown.