import
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user