Files
2026-04-13 08:10:14 +03:00

122 lines
4.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"context"
"errors"
"log/slog"
"net/http"
"os/signal"
"syscall"
"time"
"github.com/olezhek28/graceful-shutdown-demo/internal/closer"
"github.com/olezhek28/graceful-shutdown-demo/internal/config"
)
// App — структура приложения.
// Содержит DI-контейнер и HTTP-сервер.
type App struct {
diContainer *diContainer
httpServer *http.Server
}
// New создаёт приложение и инициализирует все зависимости через DI-контейнер.
func New() *App {
a := &App{
diContainer: newDIContainer(),
}
a.initDeps()
return a
}
// initDeps последовательно вызывает функции инициализации.
func (a *App) initDeps() {
inits := []func(){
a.initHTTPServer,
}
for _, fn := range inits {
fn()
}
}
// initHTTPServer создаёт HTTP-сервер.
func (a *App) initHTTPServer() {
a.httpServer = &http.Server{
Addr: config.AppConfig().HTTPAddr,
Handler: a.diContainer.Handler().Routes(),
}
}
// Run запускает HTTP-сервер с graceful shutdown.
//
// Что происходит:
// 1. signal.NotifyContext перехватывает SIGINT (Ctrl+C) и SIGTERM (Kubernetes)
// 2. HTTP-сервер запускается в отдельной горутине
// 3. Main-горутина ждёт сигнал через <-ctx.Done()
// 4. При сигнале: server.Shutdown дожидается текущих запросов
// 5. closer.CloseAll закрывает все ресурсы в обратном порядке (LIFO)
//
// Паттерн "двойной Ctrl+C":
// - Первый Ctrl+C → graceful shutdown
// - stop() снимает custom handler после первого сигнала
// - Второй Ctrl+C → ОС убивает процесс мгновенно (для разработки, когда shutdown завис)
func (a *App) Run() error {
// 1. Перехват сигналов.
// signal.NotifyContext создаёт канал с ёмкостью 1 (буферизованный).
// Если бы канал был unbuffered — сигнал мог бы потеряться,
// пока main ещё инициализирует зависимости и не слушает канал.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
slog.Info("сервер запущен", "addr", config.AppConfig().HTTPAddr)
// 2. Запуск сервера в горутине.
// ListenAndServe блокирует — поэтому запускаем в горутине, а main ждёт сигнал.
// http.ErrServerClosed — нормальное завершение (мы сами вызвали Shutdown), не ошибка.
go func() {
if err := a.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("ошибка сервера", "err", err)
}
}()
// 3. Ожидание сигнала.
<-ctx.Done()
slog.Info("получен сигнал, завершаем...")
// Паттерн "двойной Ctrl+C": снимаем custom handler.
// Теперь второй Ctrl+C убьёт процесс мгновенно (дефолтное поведение ОС).
stop()
// 4. Graceful shutdown HTTP-сервера.
// Таймаут 15 секунд. Используем context.Background(), а не ctx — тот уже отменён.
//
// Что делает server.Shutdown внутри:
// 1) Закрывает listeners — новые TCP-соединения невозможны
// 2) Закрывает idle connections (keep-alive без активных запросов)
// 3) Ждёт активные connections — пока handler вернёт ответ
// 4) Если контекст истёк — возвращает ошибку, НО handlers продолжают работать в фоне
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second)
defer shutdownCancel()
if err := a.httpServer.Shutdown(shutdownCtx); err != nil {
slog.Error("ошибка при остановке сервера", "err", err)
}
slog.Info("сервер остановлен")
// 5. Закрытие всех ресурсов через глобальный closer (LIFO).
// Отдельный контекст с таймаутом 10 секунд — свой бюджет для ресурсов.
// Суммарно: 15с (сервер) + 10с (ресурсы) = 25с из 30с Kubernetes grace period.
closerCtx, closerCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer closerCancel()
if err := closer.CloseAll(closerCtx); err != nil {
slog.Error("ошибки при закрытии ресурсов", "err", err)
}
return nil
}