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 каждый
перезапуск роняет часть из них.