Understanding graceful shutdown and Drop

I'm going through the graceful shutdown chapter of Tokio's guide and have a few questions. Consider the simple async program below.

use futures::StreamExt;
use std::io;
use tempfile::TempDir;
use tokio::net::TcpListener;
use tokio_util::codec::Decoder;
use tokio_util::codec::LinesCodec;

async fn run_server(listener: TcpListener) -> io::Result<()> {
    let tempdir = TempDir::new().unwrap();
    eprintln!("tempdir at: {:?}", tempdir.path());

    loop {
        let (socket, _) = listener.accept().await?;

        tokio::spawn(async move {
            let mut framed = LinesCodec::new().framed(socket);
            while let Some(message) = framed.next().await {
                match message {
                    Ok(bytes) => {
                        eprintln!("got line: {}", bytes);
                    }
                    Err(err) => println!("Socket closed with error: {:?}", err),
                }
            }
            println!("Socket received FIN packet and closed connection");
        });
    }
}

#[tokio::main]
async fn main() -> io::Result<()> {
    // Bind the listener to the address
    let listener = TcpListener::bind("127.0.0.1:2345").await.unwrap();

    tokio::select! {
        _ = run_server(listener) => {}
        // _ = tokio::signal::ctrl_c() => {
        //     // The shutdown signal has been received.
        //     eprintln!("shutting down");
        // }
    }

    Ok(())
}

With this Cargo.toml:

[package]
name = "async-graceful-shutdown"
version = "0.1.0"
edition = "2021"

[dependencies]
futures = "0.3.24"
tempfile = "3.3.0"
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7.3", features = ["full"] }

As written, killing the program with Ctrl+C leaks the tempdir. But just uncommenting the code that handles Ctrl+C in the select! block is enough to get the tempdir cleaned up on Ctrl+C. Can someone please explain why this is the case?

I'm assuming it has to do with the default SIGTERM handler installed by Rust? I guess it doesn't unwind stacks and call drop()?

Taking it a step further: given that the simple no-op Ctrl+C handler seems to be enough to unwind stacks and cleanup things with drop, how much of the machinery described in Telling things to shut down is really necessary? As the article mentions, I understand that it's necessary if I want things to happen before a socket closes down, e.g. send a "closing" message to clients. But it seems like the framed.next().await is automatically cancelled, for example.

It's because there's no default SIGTERM handler installed by Rust. The OS forcefully kill the process after SIGTERM if no handler is installed.

1 Like

OK, thanks. So with the SIGTERM handler installed, the runtime is dropped and shutdown? And that's what causes the tempdir in my example above to ultimately be dropped?

With the SIGTERM handler installed, tokio::select! selects the handler and then exits the block, letting the main function to finish ordinarily and therefore drop everything it held, including the future created by run_server, which holds the TempDir.

Without it, however, run_server just loops indefinitely, and SIGTERM aborts the program in the middle of the loop.

4 Likes