Волга
Главная
Agent Skill
API Docs
GitHub
  • English
  • Русский
Главная
Agent Skill
API Docs
GitHub
  • English
  • Русский
  • Главная
  • Старт

    • Быстрый старт
    • Обработчики
    • Параметры маршрута
    • Параметры запроса
    • Группировка маршрутов
  • Запросы и ответы

    • Заголовки (Headers)
    • Работа с JSON
    • Работа с Form Data
    • Работа с файлами
    • Multipart-ответы
    • Сырое тело запроса
    • Cookies
    • Валидация запросов
  • Middleware и инфраструктура

    • Основы Middleware
    • Пользовательские Middleware
    • Параметризованные Middleware
    • Сжатие ответов
    • Распаковка запросов
    • CORS (Cross-Origin Resource Sharing)
    • Статические файлы
    • Rate Limiting
    • Файлы конфигурации
    • OpenAPI
  • Безопасность и доступ

    • Аутентификация и авторизация
    • OAuth 2.1 и OpenID Connect
    • OAuth 2.1 Клиент
    • Гранты «сервис-сервис»
    • DPoP — токены, привязанные к отправителю
  • Надежность и наблюдаемость

    • Центральный обработчик ошибок
    • Логгирование и Трассировка
    • Отмена запросов
    • Плавное завершение работы
  • Протоколы и realtime

    • Версии и методы HTTP
    • HTTPS
    • WebSockets
    • Server-Sent Events (SSE)
  • Продвинутые паттерны

    • Внедрение Зависимостей
    • Пользовательская обработка методов HEAD, OPTIONS и TRACE
  • Agent Skill

Плавное завершение работы

Волга всегда завершается плавно по сигналу ОС — Ctrl+C и SIGTERM на Unix и их аналоги на Windows. Когда приходит сигнал, слушатель перестаёт принимать новые соединения, уже начатым запросам даётся возможность завершиться в пределах таймаута завершения, и только после этого run() возвращает управление.

Начиная с v0.9.3, то же самое завершение можно инициировать из своего кода с помощью ShutdownHandle — для административного эндпоинта, вотчдога, истёкшей аренды или воркера, доделавшего задачу, ради которой процесс и запускался.

Приложение с хэндлом

App::with_shutdown() возвращает приложение вместе со свежим хэндлом:

use std::time::Duration;
use volga::App;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let (app, shutdown) = App::with_shutdown();

    tokio::spawn(async move {
        tokio::time::sleep(Duration::from_secs(60)).await;
        shutdown.shutdown();
    });

    app.run().await
}

Хэндл дёшево клонируется и может быть передан куда угодно; вызов shutdown() у любого клона запускает одно и то же плавное завершение. Он дополняет встроенный обработчик сигналов, а не заменяет его — срабатывает то, что произошло раньше.

Регистрация внешнего хэндла

Если хэндл принадлежит другому коду — создан при старте, хранится в состоянии приложения, разделяется с фоновыми задачами — зарегистрируйте его на уже созданном приложении через with_shutdown_signal():

use volga::{App, ShutdownHandle};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let handle = ShutdownHandle::new();
    let app = App::new().with_shutdown_signal(handle.clone());

    // теперь `handle` может жить в состоянии, в супервизоре, в CLI-команде...
    app.run().await
}

ShutdownHandle построен поверх CancellationToken из Tokio, поэтому он может принять уже существующий токен через ShutdownHandle::from_token() (или эквивалентный From<CancellationToken>) — так сервер встраивается в дерево отмены, которое уже используется в остальном процессе.

Ещё два метода позволяют наблюдать за состоянием, а не инициировать его:

  • is_shutdown_requested() — было ли запрошено завершение;
  • cancelled() — future, который завершается в этот момент, чтобы фоновые задачи сворачивались вместе с сервером.

Завершение по future

shutdown_on() инициирует плавное завершение, когда переданный future завершается. Это удобно для всего, что уже сигнализирует о себе асинхронно: канал oneshot, внешний вотчдог, уведомление о перезагрузке конфигурации, неудавшееся продление аренды.

use volga::App;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let (tx, rx) = tokio::sync::oneshot::channel::<()>();

    let app = App::new()
        .bind("127.0.0.1:7878")
        .shutdown_on(async move { let _ = rx.await; });

    // отправка в `tx` позже инициирует плавное завершение
    app.run().await
}

Несколько вызовов shutdown_on() складываются: завершение любого из зарегистрированных future запускает остановку, и все они сочетаются с обработчиком сигналов ОС и с ранее зарегистрированным ShutdownHandle. Если хэндл не регистрировался, он создаётся внутри автоматически.

Совет

Future запускается в рантайме Tokio в момент старта приложения, поэтому shutdown_on() безопасно вызывать до того, как рантайм вообще существует — в том числе перед run_blocking().

Завершение из обработчика

ShutdownHandle — ещё и экстрактор: обработчик или middleware, принимающие его, получают хэндл работающего сервера, независимо от того, передавался ли он приложению через with_shutdown(). Поэтому административный эндпоинт остановки умещается в несколько строк:

use volga::{App, ShutdownHandle, ok};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    app.map_post("/admin/shutdown", |handle: ShutdownHandle| {
        handle.shutdown();
        ok!("shutting down")
    });

    app.run().await
}

Ответ при этом всё равно будет доставлен: запрос, инициировавший остановку, находится в обработке, а именно таких запросов и дожидается плавное завершение.

Примечание

Такой эндпоинт обязательно закрывайте аутентификацией и авторизацией — он останавливает сервер для всех.

Таймаут завершения

Когда начинается завершение, цикл приёма перестаёт принимать новые соединения, а открытым соединениям сообщается, что после текущего ответа их нужно закрыть. run() ждёт их в пределах таймаута завершения — по умолчанию 10 секунд. Соединение, которое всё ещё открыто, когда таймаут истёк, закрывается: CancellationToken его запроса отменяется, а ответ обрывается там, где он есть. run() возвращает управление, когда не остаётся ни одного соединения, включая соединения слушателя HTTPS-редиректа, а сервисы приложения, включая синглтоны, освобождаются уже после этого. Незавершённое TLS-рукопожатие не ждут, а обрывают: запроса за ним ещё нет. Соединение, ответившее до того, как тело запроса было дочитано, — например, 413 на тело больше ограничения, — остаётся открытым, пока до 2 секунд дочитывает и отбрасывает остаток, и его тоже дожидаются.

Таймаут задаётся методом with_shutdown_timeout():

use std::time::Duration;
use volga::App;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let app = App::new().with_shutdown_timeout(Duration::from_secs(30));

    app.run().await
}

или ключом shutdown_timeout_secs секции [server] конфигурационного файла:

[server]
shutdown_timeout_secs = 30

Duration::ZERO закрывает открытые соединения сразу.

Завершение долгих ответов

Ответ, который сам по себе не заканчивается, — поток SSE, проксируемый поток, — держит соединение открытым весь таймаут завершения. Вместо этого он может завершиться сам, как только началась остановка: примите ShutdownHandle и оборвите поток по cancelled(), который возвращает 'static future:

use std::time::Duration;
use futures_util::StreamExt;
use volga::{App, ShutdownHandle, http::sse::{Message, SseStream}, sse_stream};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    app.map_get("/events", |shutdown: ShutdownHandle| async move {
        let events = sse_stream! {
            loop {
                yield Message::new().data("tick");
                tokio::time::sleep(Duration::from_secs(1)).await;
            }
        };
        SseStream::new(events.take_until(shutdown.cancelled()))
    });

    app.run().await
}

Завершение и отмена запроса

Это разные сигналы. Только что начавшееся завершение даёт активным запросам доработать, поэтому CancellationToken запроса в этот момент не отменяется: обработчик, прервавший работу по нему, провалил бы запросы без всякой причины. Токен говорит о том, что ответ больше не нужен: клиент ушёл или таймаут завершения истёк и соединение закрывается.

Долгоиграющий обработчик стоит писать с учётом обоих: следить за CancellationToken, чтобы не работать над ответом, который никто не прочитает, и за ShutdownHandle, чтобы не задерживать завершение до таймаута.

Последнее обновление: 04.10.2026, 06:08
Prev
Отмена запросов