1use std::time::Duration;
8
9use tokio::signal;
10
11const DEFAULT_SHUTDOWN_TIMEOUT_SECS: u64 = 30;
13
14pub async fn shutdown_signal() {
27 let ctrl_c = async {
28 signal::ctrl_c()
29 .await
30 .expect("failed to install Ctrl+C handler");
31 };
32
33 #[cfg(unix)]
34 let terminate = async {
35 signal::unix::signal(signal::unix::SignalKind::terminate())
36 .expect("failed to install SIGTERM handler")
37 .recv()
38 .await;
39 };
40
41 #[cfg(not(unix))]
42 let terminate = std::future::pending::<()>();
43
44 tokio::select! {
45 _ = ctrl_c => {
46 tracing::info!("received Ctrl+C, initiating graceful shutdown");
47 }
48 _ = terminate => {
49 tracing::info!("received SIGTERM, initiating graceful shutdown");
50 }
51 }
52}
53
54pub async fn shutdown_cleanup(db: &sqlx::SqlitePool, timeout: Option<Duration>) {
60 let timeout = timeout.unwrap_or(Duration::from_secs(DEFAULT_SHUTDOWN_TIMEOUT_SECS));
61 tracing::info!(timeout_secs = timeout.as_secs(), "running shutdown cleanup");
62
63 let cleanup = async {
64 tracing::debug!("closing database connection pool");
65 db.close().await;
66 tracing::debug!("database pool closed");
67 };
68
69 match tokio::time::timeout(timeout, cleanup).await {
70 Ok(()) => tracing::info!("shutdown cleanup completed"),
71 Err(_) => tracing::warn!("shutdown cleanup timed out"),
72 }
73}
74
75#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn default_timeout_is_30_seconds() {
85 assert_eq!(DEFAULT_SHUTDOWN_TIMEOUT_SECS, 30);
86 }
87
88 #[tokio::test]
89 async fn shutdown_cleanup_completes_with_fresh_pool() {
90 let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
91 shutdown_cleanup(&pool, Some(Duration::from_secs(5))).await;
92 assert!(pool.is_closed());
93 }
94
95 #[tokio::test]
96 async fn shutdown_cleanup_handles_already_closed_pool() {
97 let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
98 pool.close().await;
99 shutdown_cleanup(&pool, Some(Duration::from_secs(1))).await;
101 }
102}