Files

122 lines
4.8 KiB
Go
Raw Permalink Normal View History

2026-04-13 08:10:14 +03:00
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
}