← Оглавление

8.5 Прежде чем дверь закроется

Теперь в журнале видно всё. Каждый запрос оставляет запись: что пришло, что вернули, когда и почему. Одного записать не получается: момент, когда журнал перестаёт вестись. Ctrl+C не оставляет записи. Процесс исчезает - и в журнале последнее, что было до этого. Конец без метки.

Что нужно

Ctrl+C посылает процессу сигнал завершения. По умолчанию процесс прекращает работу немедленно - независимо от того, что происходило внутри. Запрос в середине обработки обрывается: клиент не получает ответа, запись на диск может остаться незавершённой.

Graceful shutdown решает это в три шага: прекратить принимать новые подключения, дождаться завершения запросов в полёте, выйти. Axum поддерживает это напрямую - нужно только описать, когда именно сервер должен остановиться.

Воспроизводим

Чтобы увидеть проблему, нужен запрос, который живёт достаточно долго. Добавляем в crates/api/src/routes.rs обработчик, который намеренно ждёт 30 секунд:

use std::time::Duration;
use tokio::time::sleep;

async fn slow() -> &'static str {
    sleep(Duration::from_secs(30)).await;
    "done\n"
}

И в функции router - маршрут:

.route("/slow", get(slow))

Открыть два терминала. В первом - запустить сервер:

$ make serve
2026-06-25T10:00:00Z  INFO tq_api: listening on 0.0.0.0:3000

Во втором - отправить запрос:

$ curl localhost:3000/slow

Вернуться в первый терминал и нажать Ctrl+C. Во втором:

curl: (52) Empty reply from server

Сервер исчезает. Соединение обрывается. Запрос пропадает без следа.

Постройка

В crates/api/src/main.rs - новая функция-сигнал:

async fn shutdown_signal() {
    tokio::signal::ctrl_c().await.unwrap();
    tracing::info!("shutdown signal received");
}

И в конце main, вместо axum::serve(listener, app).await.unwrap():

axum::serve(listener, app)
    .with_graceful_shutdown(shutdown_signal())
    .await
    .unwrap();
tracing::info!("server stopped");

tokio::signal::ctrl_c() ждёт Ctrl+C и завершается ровно один раз. .with_graceful_shutdown(shutdown_signal()) говорит серверу: когда shutdown_signal() завершился - перестань принимать новые подключения, дождись текущих запросов и только тогда выйди. Строка tracing::info!("server stopped") выполняется, когда сервер действительно остановился - не раньше.

Результат

Повторить сценарий из “Воспроизводим”: make serve в одном окне, curl localhost:3000/slow во втором, Ctrl+C в первом. Теперь во втором окне - не обрыв, а ответ:

$ curl localhost:3000/slow
done

И в первом окне журнал показывает весь путь до конца:

2026-06-25T10:00:00Z  INFO tq_api: listening on 0.0.0.0:3000
^C
2026-06-25T10:00:05Z  INFO tq_api: shutdown signal received
2026-06-25T10:00:35Z  INFO tq_api: server stopped

Теперь в журнале есть метка для конца. make ci проходит.

Полный код tq для этой главы - 8-ready/05-before-the-door-closes/.


Lore: Сигналы

Ctrl+C - не просто сочетание клавиш. Терминал преобразует его в SIGINT - сигнал операционной системы, который посылается процессу. Без обработки процесс завершается немедленно.

tokio::signal::ctrl_c() перехватывает этот сигнал - вместо немедленной смерти процесс дожидается его через .await и сам решает, что делать дальше. with_graceful_shutdown использует эту возможность: дочитать, дописать, закрыть соединения.

Lore: когда это встречается

/slow - синтетический пример: реальные запросы занимают миллисекунды, и поймать один в момент остановки в одиночку сложно. При высокой нагрузке это перестаёт быть редкостью - при тысяче запросов в секунду несколько всегда в полёте. Без graceful shutdown каждый перезапуск роняет часть из них.