Rust & Tokio: How to handle more signals than just sigint i.e. sigquit?

377 views Asked by At

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.

1

There are 1 answers

1
Finomnis On BEST ANSWER

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
}