miden_node_utils/
shutdown.rs1use std::fmt::{self, Display, Formatter};
2use std::future::Future;
3use std::time::Duration;
4
5use anyhow::Context;
6pub use tokio_util::sync::CancellationToken;
7
8pub const GRACE_PERIOD: Duration = Duration::from_secs(10);
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum ShutdownSignal {
14 Interrupt,
15 Terminate,
16}
17
18impl Display for ShutdownSignal {
19 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
20 match self {
21 Self::Interrupt => f.write_str("SIGINT"),
22 Self::Terminate => f.write_str("SIGTERM"),
23 }
24 }
25}
26
27pub async fn run_with_shutdown<F, Fut>(service_name: &'static str, run: F) -> anyhow::Result<()>
33where
34 F: FnOnce(CancellationToken) -> Fut,
35 Fut: Future<Output = anyhow::Result<()>>,
36{
37 run_with_shutdown_signal(service_name, run, shutdown_signal()).await
38}
39
40async fn run_with_shutdown_signal<F, Fut, Signal>(
41 service_name: &'static str,
42 run: F,
43 signal: Signal,
44) -> anyhow::Result<()>
45where
46 F: FnOnce(CancellationToken) -> Fut,
47 Fut: Future<Output = anyhow::Result<()>>,
48 Signal: Future<Output = anyhow::Result<ShutdownSignal>>,
49{
50 let token = CancellationToken::new();
51 let service = run(token.clone());
52 tokio::pin!(service);
53 tokio::pin!(signal);
54
55 tokio::select! {
56 result = &mut service => result,
57 result = &mut signal => {
58 let signal = result?;
59 tracing::info!(
60 service.name = service_name,
61 shutdown.signal = %signal,
62 "Shutdown requested",
63 );
64 token.cancel();
65
66 let Ok(result) = tokio::time::timeout(GRACE_PERIOD, &mut service).await else {
67 tracing::error!(
68 service.name = service_name,
69 grace_period = ?GRACE_PERIOD,
70 "Graceful shutdown timed out; exiting process",
71 );
72 std::process::exit(1);
73 };
74
75 result?;
76 tracing::info!(service.name = service_name, "Shutdown complete");
77 Ok(())
78 },
79 }
80}
81
82pub async fn shutdown_signal() -> anyhow::Result<ShutdownSignal> {
84 #[cfg(unix)]
85 {
86 let mut terminate =
87 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
88 .context("failed to install SIGTERM handler")?;
89
90 tokio::select! {
91 _ = terminate.recv() => Ok(ShutdownSignal::Terminate),
92 result = tokio::signal::ctrl_c() => {
93 result
94 .context("failed to install Ctrl-C handler")
95 .map(|()| ShutdownSignal::Interrupt)
96 },
97 }
98 }
99
100 #[cfg(not(unix))]
101 {
102 tokio::signal::ctrl_c()
103 .await
104 .context("failed to install Ctrl-C handler")
105 .map(|()| ShutdownSignal::Interrupt)
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use std::sync::Arc;
112 use std::sync::atomic::{AtomicBool, Ordering};
113
114 use super::*;
115
116 #[tokio::test]
117 async fn signal_cancels_and_waits_for_service() {
118 let cancelled = Arc::new(AtomicBool::new(false));
119 let service_cancelled = Arc::clone(&cancelled);
120
121 run_with_shutdown_signal(
122 "test-service",
123 move |shutdown| async move {
124 shutdown.cancelled().await;
125 service_cancelled.store(true, Ordering::Relaxed);
126 Ok(())
127 },
128 std::future::ready(Ok(ShutdownSignal::Interrupt)),
129 )
130 .await
131 .expect("clean shutdown should succeed");
132
133 assert!(cancelled.load(Ordering::Relaxed));
134 }
135
136 #[tokio::test]
137 async fn signal_handler_error_is_propagated() {
138 let err = run_with_shutdown_signal(
139 "test-service",
140 |shutdown| async move {
141 shutdown.cancelled().await;
142 Ok(())
143 },
144 std::future::ready(Err(anyhow::anyhow!("signal handler failed"))),
145 )
146 .await
147 .expect_err("signal error should be returned");
148
149 assert_eq!(err.to_string(), "signal handler failed");
150 }
151}