Ok,
I have a working microservice that provides gRPC via tokio / tonic and exposes metrics via warp. Both http and gRPC services shutdown correctly when receiving a sigint signal i.e. kill via terminal.
Full code on Github, and the relevant code below:
// Sigint signal handler that closes the DB connection upon shutdown
let signal = grpc_sigint(dbm.clone());
// Construct health service for gRPC server
let (mut health_reporter, health_svc) = tonic_health::server::health_reporter();
health_reporter.set_serving::<JobRunnerServer<MyJobRunner>>().await;
// Build gRPC server with health service and signal sigint handler
let grpc_server = TonicServer::builder()
.add_service(grpc_svc)
.add_service(health_svc)
.serve_with_shutdown(grpc_addr, signal);
The sigint handler is fairly basic but gets the job done.
async fn grpc_sigint(dbm: DBManager) {
let _ = signal(SignalKind::terminate())
.expect("failed to create a new SIGINT signal handler for gRPC")
.recv()
.await;
// Shutdown the DB connection.
dbm.close_db().await.expect("Failed to close database connection");
println!("gRPC shutdown complete");
}
This works fine, but the service doesn't capture ctrl-c and doesn't respond to other signals such as sig-quit because it only has one signal handler for sigint. Because the service is supposed to run on multiple platforms, it's not given that sig-int is the only signal to handle.
I did some online search, but somehow most examples were similar to the one I am using so it's not so clear to me how to handle multiple signals in tokio to trigger a graceful shutdown whenever any of the signals fires.
Therefore, my question is, how do you modify the signal handler so that it captures multiple shutdown signals such as sigint, ctr-c or sig-quit?
Thank you in advance.
You claim to listen to SIGINT
(= Ctrl+C
), but SignalType::terminate()
actually listens to SIGTERM
(= kill
or system shutdown).
I'm not sure if listening for SIGQUIT
is really necessary; in most situations SIGINT
and SIGTERM
are sufficient.
You can of course listen to both, through tokio::select!()
.
Here is an example. The code is taken from tokio-graceful-shutdown
. (Disclaimer: I'm the author of this crate)
/// Waits for a signal that requests a graceful shutdown, like SIGTERM or SIGINT.
#[cfg(unix)]
async fn wait_for_signal_impl() {
use tokio::signal::unix::{signal, SignalKind};
// Infos here:
// https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html
let mut signal_terminate = signal(SignalKind::terminate()).unwrap();
let mut signal_interrupt = signal(SignalKind::interrupt()).unwrap();
tokio::select! {
_ = signal_terminate.recv() => tracing::debug!("Received SIGTERM."),
_ = signal_interrupt.recv() => tracing::debug!("Received SIGINT."),
};
}
/// Waits for a signal that requests a graceful shutdown, Ctrl-C (SIGINT).
#[cfg(windows)]
async fn wait_for_signal_impl() {
use tokio::signal::windows;
// Infos here:
// https://learn.microsoft.com/en-us/windows/console/handlerroutine
let mut signal_c = windows::ctrl_c().unwrap();
let mut signal_break = windows::ctrl_break().unwrap();
let mut signal_close = windows::ctrl_close().unwrap();
let mut signal_shutdown = windows::ctrl_shutdown().unwrap();
tokio::select! {
_ = signal_c.recv() => tracing::debug!("Received CTRL_C."),
_ = signal_break.recv() => tracing::debug!("Received CTRL_BREAK."),
_ = signal_close.recv() => tracing::debug!("Received CTRL_CLOSE."),
_ = signal_shutdown.recv() => tracing::debug!("Received CTRL_SHUTDOWN."),
};
}
/// Registers signal handlers and waits for a signal that
/// indicates a shutdown request.
pub(crate) async fn wait_for_signal() {
wait_for_signal_impl().await
}