Graceful shutdown в Go: как дождаться HTTP-запросов при остановке

Graceful shutdown в Go: как дождаться HTTP-запросов при остановке
Содержание статьи

Server.Shutdown прекращает приём новых соединений и ждёт завершения активных HTTP-запросов в пределах контекста. Важно, чтобы main дождалась этого ожидания. На Mentoring.digital разберём учебный сервер, который останавливается по сигналу и ограничивает завершение пятью секундами.

Разделите работу сервера и ожидание сигнала

ListenAndServe выполняется в горутине. Буферизованный канал сообщает об ошибке запуска, чтобы занятый порт не оставил программу бесконечно ждать сигнала.

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    signalCtx, stop := signal.NotifyContext(
        context.Background(), os.Interrupt, syscall.SIGTERM,
    )
    defer stop()

    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(time.Second)
        fmt.Fprintln(w, "done")
    })
    server := &http.Server{
        Addr: "127.0.0.1:8080", Handler: mux,
        ReadHeaderTimeout: 3 * time.Second,
    }
    serveErr := make(chan error, 1)
    go func() { serveErr <- server.ListenAndServe() }()

    select {
    case err := <-serveErr:
        if !errors.Is(err, http.ErrServerClosed) {
            log.Printf("server: %v", err)
        }
        return
    case <-signalCtx.Done():
    }
    stop()

    shutdownCtx, cancel := context.WithTimeout(
        context.Background(), 5*time.Second,
    )
    defer cancel()
    if err := server.Shutdown(shutdownCtx); err != nil {
        log.Printf("shutdown: %v", err)
        if closeErr := server.Close(); closeErr != nil {
            log.Printf("close: %v", closeErr)
        }
    }
    if err := <-serveErr; !errors.Is(err, http.ErrServerClosed) {
        log.Printf("server: %v", err)
    }
}

Почему для Shutdown нужен отдельный контекст

Контекст сигнала к этому моменту уже отменён. Если передать его в Shutdown, времени на спокойное завершение не останется. Поэтому пример создаёт новый контекст от Background с отдельным сроком ожидания.

При начале Shutdown вызов ListenAndServe возвращает ErrServerClosed. Это ожидаемый результат, но он ещё не означает, что активные запросы завершились. Именно поэтому ожидание Shutdown находится в main.

Что происходит после истечения срока

Когда срок контекста истекает, Shutdown возвращает ошибку. В примере после неё вызывается Close: это явный переход к принудительному закрытию, при котором оставшиеся запросы могут оборваться. Подберите срок по требованиям приложения, а ошибку сохраните в журнале.

Shutdown не ждёт завершения соединений, переданных через Hijack, например WebSocket. Для них, как и для фоновых задач приложения, нужен отдельный порядок остановки. Этот пример покрывает обычные HTTP-запросы.

Как проверить остановку локально

Запустите сервер в тестовом проекте, начните запрос и отправьте сигнал прерывания, пока обработчик ждёт. Проверьте, завершился ли принятый запрос. Затем увеличьте задержку обработчика выше пяти секунд и проверьте ветку тайм-аута. Отдельно запустите сервер на уже занятом порту.

Управление жизненным циклом можно отработать на курсе Go, а остановку сервиса с несколькими зависимостями разобрать с ментором по Go.

Похожие статьи по данной теме