122 lines
4.8 KiB
Go
122 lines
4.8 KiB
Go
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
|
||
}
|