use std::time::Duration;
use thiserror::Error;
use tokio::sync::watch;
use tokio::task::{JoinError, JoinSet};
use tracing::{error, info};
#[derive(Debug, Error)]
pub enum ShutdownError {
#[error("A background task panicked during shutdown")]
Panic(#[from] JoinError),
#[error("Graceful shutdown timed out after {0:?}")]
Timeout(Duration),
}
pub struct ShutdownManager {
tasks: JoinSet<()>,
shutdown_tx: watch::Sender<()>,
}
impl ShutdownManager {
pub fn new() -> Self {
let (shutdown_tx, _) = watch::channel(());
Self { tasks: JoinSet::new(), shutdown_tx }
}
pub fn spawn_task<F>(&mut self, task: F)
where
F: std::future::Future<Output = ()> + Send + 'static,
{
self.tasks.spawn(task);
}
pub fn subscribe(&self) -> watch::Receiver<()> {
self.shutdown_tx.subscribe()
}
pub fn abort_all(&mut self) {
self.tasks.abort_all();
}
pub async fn graceful_shutdown(self, timeout: Duration) -> Result<(), ShutdownError> {
let ShutdownManager { mut tasks, shutdown_tx } = self;
info!("Broadcasting shutdown signal to all {} background tasks...", tasks.len());
drop(shutdown_tx);
info!("Waiting for tasks to complete...");
let join_all_logic = async {
while let Some(res) = tasks.join_next().await {
res?;
}
Ok(())
};
match tokio::time::timeout(timeout, join_all_logic).await {
Ok(Ok(_)) => {
info!("All background tasks completed gracefully.");
Ok(())
}
Ok(Err(e)) => {
error!(error = %e, "A background task panicked during shutdown.");
Err(ShutdownError::Panic(e))
}
Err(_) => {
error!("Shutdown timeout of {:?} exceeded. Aborting remaining tasks.", timeout);
tasks.abort_all();
Err(ShutdownError::Timeout(timeout))
}
}
}
}
impl Default for ShutdownManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::{sleep, Duration};
use tracing::info;
#[tokio::test]
async fn test_basic_shutdown() {
let mut manager = ShutdownManager::new();
let mut rx = manager.subscribe();
manager.spawn_task(async move {
info!("Task started, waiting for shutdown...");
let _ = rx.changed().await;
info!("Task received shutdown signal.");
});
let res = manager.graceful_shutdown(Duration::from_secs(1)).await;
assert!(res.is_ok(), "Expected graceful shutdown to succeed");
}
#[tokio::test]
async fn test_timeout() {
let mut manager = ShutdownManager::new();
manager.spawn_task(async {
info!("Long task started...");
sleep(Duration::from_secs(10)).await;
});
let res = manager.graceful_shutdown(Duration::from_millis(100)).await;
assert!(res.is_err(), "Expected shutdown to return error due to timeout");
assert!(matches!(res, Err(ShutdownError::Timeout(_))), "Expected a timeout error");
}
#[tokio::test]
async fn test_panic_propagation() {
let mut manager = ShutdownManager::new();
manager.spawn_task(async {
info!("Task about to panic...");
panic!("Simulated panic");
});
let res = manager.graceful_shutdown(Duration::from_secs(1)).await;
assert!(res.is_err(), "Expected shutdown to return error due to task panic");
assert!(matches!(res, Err(ShutdownError::Panic(_))), "Expected a panic error");
}
#[tokio::test]
async fn test_multiple_tasks() {
let mut manager = ShutdownManager::new();
let mut rx1 = manager.subscribe();
let mut rx2 = manager.subscribe();
manager.spawn_task(async move {
info!("Task 1 waiting for shutdown...");
let _ = rx1.changed().await;
info!("Task 1 shutdown complete");
});
manager.spawn_task(async move {
info!("Task 2 waiting for shutdown...");
let _ = rx2.changed().await;
info!("Task 2 shutdown complete");
});
let res = manager.graceful_shutdown(Duration::from_secs(1)).await;
assert!(res.is_ok(), "Expected all tasks to shutdown gracefully");
}
#[tokio::test]
async fn test_shutdown_with_no_tasks() {
let manager = ShutdownManager::new();
let res = manager.graceful_shutdown(Duration::from_secs(1)).await;
assert!(res.is_ok(), "Shutdown should succeed immediately with no tasks");
}
#[tokio::test]
async fn test_drop_does_not_panic() {
let mut manager = ShutdownManager::new();
manager.spawn_task(async { sleep(Duration::from_secs(10)).await });
}
#[tokio::test]
async fn test_task_ignores_shutdown() {
let mut manager = ShutdownManager::new();
manager.spawn_task(async {
info!("Task ignoring shutdown...");
sleep(Duration::from_secs(10)).await;
});
let res = manager.graceful_shutdown(Duration::from_millis(100)).await;
assert!(res.is_err(), "Expected timeout error");
assert!(matches!(res, Err(ShutdownError::Timeout(_))), "Expected a timeout error");
}
#[tokio::test]
async fn test_abort_all() {
let mut manager = ShutdownManager::new();
manager.spawn_task(async {
sleep(Duration::from_secs(60)).await;
});
manager.abort_all();
let res = manager.tasks.join_next().await;
assert!(res.is_some(), "Expected a result from the aborted task");
let task_res = res.unwrap();
assert!(task_res.is_err(), "Expected the task result to be an error");
assert!(
task_res.unwrap_err().is_cancelled(),
"Expected the JoinError to be of type 'cancelled'"
);
}
#[tokio::test]
async fn test_task_finishes_before_shutdown() {
let mut manager = ShutdownManager::new();
manager.spawn_task(async {
info!("Task that finishes early has started and finished.");
});
sleep(Duration::from_millis(50)).await;
let res = manager.graceful_shutdown(Duration::from_secs(1)).await;
assert!(res.is_ok(), "Shutdown should succeed even if tasks are already complete");
}
#[tokio::test]
async fn test_partial_panic_scenario() {
let mut manager = ShutdownManager::new();
let mut rx = manager.subscribe();
manager.spawn_task(async move {
info!("Normal task waiting for shutdown...");
let _ = rx.changed().await;
info!("Normal task completed");
});
manager.spawn_task(async {
info!("Task about to panic...");
panic!("Simulated panic in mixed scenario");
});
let res = manager.graceful_shutdown(Duration::from_secs(1)).await;
assert!(res.is_err(), "Expected error due to panic");
assert!(
matches!(res, Err(ShutdownError::Panic(_))),
"Expected panic error even with other tasks completing normally"
);
}
#[tokio::test]
async fn test_task_spawns_subtask() {
let mut manager = ShutdownManager::new();
let mut rx = manager.subscribe();
manager.spawn_task(async move {
info!("Parent task starting...");
let subtask = tokio::spawn(async {
sleep(Duration::from_millis(100)).await;
info!("Subtask completed");
});
let _ = rx.changed().await;
let _ = subtask.await;
info!("Parent task completed after subtask");
});
let res = manager.graceful_shutdown(Duration::from_secs(1)).await;
assert!(res.is_ok(), "Should handle tasks that spawn their own subtasks");
}
#[tokio::test]
async fn test_multiple_receivers_same_task() {
let mut manager = ShutdownManager::new();
let mut rx1 = manager.subscribe();
let mut rx2 = manager.subscribe();
manager.spawn_task(async move {
info!("Task with multiple receivers starting...");
let _ = rx1.changed().await;
info!("First receiver got signal");
let _ = rx2.changed().await;
info!("Second receiver got signal");
});
let res = manager.graceful_shutdown(Duration::from_secs(1)).await;
assert!(res.is_ok(), "Task using multiple receivers should complete");
}
}