use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::broadcast;
#[derive(Debug, Clone)]
pub struct ShutdownConfig {
pub timeout_secs: u64,
pub wait_for_jobs: bool,
pub ws_drain_ms: u64,
}
impl Default for ShutdownConfig {
fn default() -> Self {
Self {
timeout_secs: 30,
wait_for_jobs: true,
ws_drain_ms: 1000,
}
}
}
impl ShutdownConfig {
pub fn quick() -> Self {
Self {
timeout_secs: 5,
wait_for_jobs: false,
ws_drain_ms: 100,
}
}
pub fn with_timeout(timeout_secs: u64) -> Self {
Self {
timeout_secs,
..Default::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShutdownResult {
Success,
Timeout { pending_jobs: usize },
Error(String),
}
impl ShutdownResult {
pub fn is_success(&self) -> bool {
matches!(self, ShutdownResult::Success)
}
pub fn pending_jobs(&self) -> Option<usize> {
match self {
ShutdownResult::Timeout { pending_jobs } => Some(*pending_jobs),
_ => None,
}
}
}
pub struct ShutdownCoordinator {
config: ShutdownConfig,
shutdown_tx: broadcast::Sender<ShutdownSignal>,
is_shutting_down: Arc<AtomicBool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShutdownSignal {
Graceful,
Immediate,
}
impl ShutdownCoordinator {
pub fn new(config: ShutdownConfig) -> Self {
let (shutdown_tx, _) = broadcast::channel(16);
Self {
config,
shutdown_tx,
is_shutting_down: Arc::new(AtomicBool::new(false)),
}
}
pub fn config(&self) -> &ShutdownConfig {
&self.config
}
pub fn trigger_shutdown(&self) {
self.is_shutting_down.store(true, Ordering::SeqCst);
let _ = self.shutdown_tx.send(ShutdownSignal::Graceful);
}
pub fn trigger_immediate(&self) {
self.is_shutting_down.store(true, Ordering::SeqCst);
let _ = self.shutdown_tx.send(ShutdownSignal::Immediate);
}
pub fn is_shutting_down(&self) -> bool {
self.is_shutting_down.load(Ordering::SeqCst)
}
pub fn subscribe(&self) -> broadcast::Receiver<ShutdownSignal> {
self.shutdown_tx.subscribe()
}
pub fn subscriber_count(&self) -> usize {
self.shutdown_tx.receiver_count()
}
pub async fn wait_for_shutdown(&self) -> ShutdownSignal {
let mut rx = self.subscribe();
rx.recv().await.unwrap_or(ShutdownSignal::Graceful)
}
pub fn timeout(&self) -> Duration {
Duration::from_secs(self.config.timeout_secs)
}
}
impl Clone for ShutdownCoordinator {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
shutdown_tx: self.shutdown_tx.clone(),
is_shutting_down: self.is_shutting_down.clone(),
}
}
}
pub async fn wait_for_shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("Failed to setup SIGINT handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("Failed to setup SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}
pub async fn graceful_shutdown<F, Fut>(
coordinator: &ShutdownCoordinator,
pending_job_count: F,
) -> ShutdownResult
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = usize>,
{
if !coordinator.config.wait_for_jobs {
return ShutdownResult::Success;
}
let timeout = coordinator.timeout();
let start = std::time::Instant::now();
loop {
let pending = pending_job_count().await;
if pending == 0 {
return ShutdownResult::Success;
}
if start.elapsed() >= timeout {
return ShutdownResult::Timeout {
pending_jobs: pending,
};
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shutdown_config_default() {
let config = ShutdownConfig::default();
assert_eq!(config.timeout_secs, 30);
assert!(config.wait_for_jobs);
assert_eq!(config.ws_drain_ms, 1000);
}
#[test]
fn test_shutdown_config_quick() {
let config = ShutdownConfig::quick();
assert_eq!(config.timeout_secs, 5);
assert!(!config.wait_for_jobs);
assert_eq!(config.ws_drain_ms, 100);
}
#[test]
fn test_shutdown_config_with_timeout() {
let config = ShutdownConfig::with_timeout(60);
assert_eq!(config.timeout_secs, 60);
assert!(config.wait_for_jobs);
}
#[test]
fn test_shutdown_coordinator_new() {
let config = ShutdownConfig::default();
let coordinator = ShutdownCoordinator::new(config.clone());
assert_eq!(coordinator.config().timeout_secs, 30);
assert!(!coordinator.is_shutting_down());
}
#[tokio::test]
async fn test_shutdown_signal_send_receive() {
let coordinator = ShutdownCoordinator::new(ShutdownConfig::default());
let mut rx = coordinator.subscribe();
coordinator.trigger_shutdown();
let signal = rx.recv().await.unwrap();
assert_eq!(signal, ShutdownSignal::Graceful);
assert!(coordinator.is_shutting_down());
}
#[tokio::test]
async fn test_shutdown_signal_immediate() {
let coordinator = ShutdownCoordinator::new(ShutdownConfig::default());
let mut rx = coordinator.subscribe();
coordinator.trigger_immediate();
let signal = rx.recv().await.unwrap();
assert_eq!(signal, ShutdownSignal::Immediate);
assert!(coordinator.is_shutting_down());
}
#[tokio::test]
async fn test_shutdown_broadcast_to_multiple() {
let coordinator = ShutdownCoordinator::new(ShutdownConfig::default());
let mut rx1 = coordinator.subscribe();
let mut rx2 = coordinator.subscribe();
let mut rx3 = coordinator.subscribe();
assert_eq!(coordinator.subscriber_count(), 3);
coordinator.trigger_shutdown();
assert_eq!(rx1.recv().await.unwrap(), ShutdownSignal::Graceful);
assert_eq!(rx2.recv().await.unwrap(), ShutdownSignal::Graceful);
assert_eq!(rx3.recv().await.unwrap(), ShutdownSignal::Graceful);
}
#[tokio::test]
async fn test_graceful_shutdown_success() {
let coordinator = ShutdownCoordinator::new(ShutdownConfig::with_timeout(5));
let result = graceful_shutdown(&coordinator, || async { 0 }).await;
assert!(result.is_success());
assert_eq!(result, ShutdownResult::Success);
}
#[tokio::test]
async fn test_graceful_shutdown_timeout() {
let mut config = ShutdownConfig::default();
config.timeout_secs = 1; let coordinator = ShutdownCoordinator::new(config);
let result = graceful_shutdown(&coordinator, || async { 5 }).await;
assert!(!result.is_success());
assert_eq!(result.pending_jobs(), Some(5));
}
#[test]
fn test_shutdown_result_variants() {
let success = ShutdownResult::Success;
assert!(success.is_success());
assert_eq!(success.pending_jobs(), None);
let timeout = ShutdownResult::Timeout { pending_jobs: 3 };
assert!(!timeout.is_success());
assert_eq!(timeout.pending_jobs(), Some(3));
let error = ShutdownResult::Error("test error".to_string());
assert!(!error.is_success());
assert_eq!(error.pending_jobs(), None);
}
#[test]
fn test_shutdown_coordinator_clone() {
let coordinator1 = ShutdownCoordinator::new(ShutdownConfig::default());
let coordinator2 = coordinator1.clone();
coordinator1.trigger_shutdown();
assert!(coordinator2.is_shutting_down());
}
#[tokio::test]
async fn test_shutdown_coordinator_wait_for_shutdown() {
let coordinator = ShutdownCoordinator::new(ShutdownConfig::default());
let coordinator_clone = coordinator.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
coordinator_clone.trigger_shutdown();
});
let signal = coordinator.wait_for_shutdown().await;
assert_eq!(signal, ShutdownSignal::Graceful);
}
#[test]
fn test_shutdown_timeout_duration() {
let config = ShutdownConfig::with_timeout(45);
let coordinator = ShutdownCoordinator::new(config);
assert_eq!(coordinator.timeout(), Duration::from_secs(45));
}
#[tokio::test]
async fn test_graceful_shutdown_no_wait() {
let mut config = ShutdownConfig::default();
config.wait_for_jobs = false;
let coordinator = ShutdownCoordinator::new(config);
let result = graceful_shutdown(&coordinator, || async { 10 }).await;
assert!(result.is_success());
}
}