Грациозное завершение работы сервера

Серверные соединения hyper имеют возможность инициировать грациозное завершение работы. Часто возникает необходимость координировать грациозное завершение работы всех активных соединений. Эту задачу мы и рассмотрим в данном руководстве.

Грациозное завершение работы — это когда соединение перестает принимать новые запросы, позволяя при этом завершиться текущим выполняющимся запросам.

Для этого нам понадобится несколько компонентов:

  1. Сигнал для начала завершения работы.
  2. Цикл принятия (accept loop) для обработки вновь поступающих соединений.
  3. Наблюдатель (watcher) для координации завершения работы.

Определение сигнала завершения

Вы можете использовать любой механизм для сигнализации о начале грациозного завершения работы. Это может быть обработчик сигналов процесса, таймер, специальный HTTP-запрос или что-либо еще.

В этом руководстве мы будем использовать обработчик сигнала CTRL+C. В Tokio есть простая поддержка для его создания:

extern crate tokio;
async fn shutdown_signal() {
    // Ждем сигнала CTRL+C
    tokio::signal::ctrl_c()
        .await
        .expect("failed to install CTRL+C signal handler");
}
fn main() {}

Модификация цикла принятия соединений сервера

Нестабильно: Код, обсуждаемый в этом руководстве, находится в hyper-util, который не так стабилен, как код в hyper. Он готов к использованию в production, но изменения могут происходить чаще.

Мы предполагаем, что у вас есть цикл принятия соединений для вашего сервера, аналогичный показанному в руководстве Hello World. Поэтому мы просто модифицируем его здесь:

extern crate hyper;
extern crate http_body_util;
extern crate hyper_util;
extern crate tokio;
mod no_run {
use std::convert::Infallible;
use std::net::SocketAddr;

use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
async fn shutdown_signal() {}
async fn hello(
    _: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>, Infallible> {
    Ok(Response::new(Full::new(Bytes::from("Hello World!"))))
}
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let listener = TcpListener::bind(addr).await?;
// Указываем настройки HTTP (http1, http2, auto — все работает)
let mut http = http1::Builder::new();
// Наблюдатель для грациозного завершения
let graceful = hyper_util::server::graceful::GracefulShutdown::new();
// Когда этот сигнал завершится, начинаем завершение работы
let mut signal = std::pin::pin!(shutdown_signal());

// Наш цикл принятия соединений сервера
loop {
    tokio::select! {
        Ok((stream, _addr)) = listener.accept() => {
            let io = TokioIo::new(stream);
            let conn = http.serve_connection(io, service_fn(hello));
            // Наблюдаем за этим соединением
            let fut = graceful.watch(conn);
            tokio::spawn(async move {
                if let Err(e) = fut.await {
                    eprintln!("Error serving connection: {:?}", e);
                }
            });
        },

        _ = &mut signal => {
            drop(listener);
            eprintln!("graceful shutdown signal received");
            // Останавливаем цикл принятия
            break;
        }
    }
}

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

tokio::select! {
    _ = graceful.shutdown() => {
        eprintln!("all connections gracefully closed");
    },
    _ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
        eprintln!("timed out wait for all connections to close");
    }
}
Ok(())
}
}
fn main() {}