tower-web icon indicating copy to clipboard operation
tower-web copied to clipboard

signal handling / graceful shutdown

Open qmx opened this issue 7 years ago • 1 comments

It'd be nice to have a simple way of handling SIGINT / SIGTERM for gracefully shutting down stuff

qmx avatar Oct 05 '18 16:10 qmx

One use case in particular to highlight would be testing.

I just ran into this and worked around the lack of a stop() function by running the executable with std::process:Child. I was relatively certain I might have been able to use serve() in a thread and maybe set up a channel to listen for some shutdown message (you can't just kill pthreads), but making a child process and managing killing it was much easier.

The code isn't the greatest but if anyone runs into this:

/// Setup API server for use in testing
pub fn setup_api_server_process() -> Result<APIServerTestInfo, Error> {
    // Generate configuration for the server
    let cfg = AppConfig::new_randomized()?;
    let cfg_toml_contents = toml::to_string(&cfg).expect("failed to config to TOML");

    // Write config to temp file
    let dir = tempdir().unwrap();
    let cfg_path = String::from(dir.path().join("cfg.toml").to_str().unwrap());
    let mut file: File = File::create(&cfg_path).unwrap();
    write!(file, "{}", cfg_toml_contents).expect("Failed to write temporary file");


    // Spawn a child process (that never returns) for the API server
    let child = Command::new("cargo")
        .arg("run")
        .arg("--")
        .arg("-c")
        .arg(cfg_path)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("failed to spawn child thread for test API server");

    // Wait for the child process something
    // FUTURE: this is fragile, would be better to wait for some sort of "server running" message on stdout
    thread::sleep(time::Duration::from_millis(STARTUP_WAIT));
    
    Ok((cfg, child))
}

I found a way to make the child process a tiny bit less fragile by waiting on output of a very specific message on stdout:

    // It is surprisingly hard to redirect output from a child process:
    // https://stackoverflow.com/questions/42726095/how-to-implement-redirection-of-stdout-of-a-child-process-to-a-file
    // https://stackoverflow.com/questions/43949612/redirect-output-of-child-process-spawned-from-rust
    let output_path = String::from(dir.path().join("output.log").to_str().unwrap());
    let output = File::create(output_path.clone()).unwrap().into_raw_fd();
    let raw_output_fd = unsafe {Stdio::from_raw_fd(output)};

    // Spawn a child process (that never returns) for the API server
    let child = Command::new("cargo")
        .arg("run")
        .arg("--")
        .arg("-c")
        .arg(cfg_path)
        .stdout(raw_output_fd)
        .spawn()
        .expect("failed to spawn child thread for test API server");

    // Read from the output file to which child process stdout is going, as the child process writes it
    let mut output_file = File::open(output_path).unwrap();
    let mut log_contents = String::new();

    // Wait until the server says it's ready
    loop {
        output_file.read_to_string(&mut log_contents)?;
        if log_contents.contains(SERVER_STARTED_MESSAGE) {
            thread::sleep(time::Duration::from_millis(STARTUP_EXTRA_WAIT));
            break;
        }
    }

Still not particularly proud of it, but it does work...

t3hmrman avatar Jan 28 '19 16:01 t3hmrman