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

132 lines
5.6 KiB
Go

package closer
import (
"context"
"errors"
"log/slog"
"sync"
"time"
)
// closeFn — одна функция закрытия с именем ресурса.
// Имя нужно для логирования: при shutdown видно, какой именно ресурс закрывается.
type closeFn struct {
name string
fn func(context.Context) error
}
// closer управляет graceful shutdown приложения.
//
// Принцип работы — как defer: последний добавленный ресурс закрывается первым (LIFO).
// Это важно для зависимостей: если кэш добавлен после базы данных,
// то при shutdown кэш закроется первым, а база — последней.
// Так гарантируется, что ни один ресурс не обращается к уже закрытой зависимости.
//
// Потокобезопасен: Add() можно вызывать из разных горутин
// при ленивой инициализации зависимостей в DI-контейнере.
// CloseAll() безопасен для повторного вызова — выполнится только один раз (sync.Once).
//
// Структура приватная — снаружи пакета доступны только функции Add() и CloseAll(),
// работающие с глобальным экземпляром.
type closer struct {
mu sync.Mutex // защищает слайс funcs от конкурентной записи
once sync.Once // гарантирует что CloseAll выполнится только один раз
funcs []closeFn // накопленные функции закрытия в порядке добавления
}
// globalCloser — глобальный экземпляр.
// Позволяет вызывать closer.Add() и closer.CloseAll() из любого места,
// не передавая экземпляр через конструкторы и DI-контейнер.
var globalCloser = &closer{}
// Add добавляет функцию закрытия в глобальный closer.
// Вызывается при создании каждого ресурса, например:
//
// closer.Add("база данных", func(_ context.Context) error {
// return db.Close()
// })
func Add(name string, fn func(context.Context) error) {
globalCloser.add(name, fn)
}
// CloseAll вызывает все функции закрытия глобального closer-а в обратном порядке (LIFO).
// Принимает context с таймаутом — если ресурс не закрылся вовремя, context отменится.
//
// Важно: таймаут в ctx — общий на все ресурсы, а не на каждый по отдельности.
// Если БД закрывалась 20 секунд из 25 — на кэш останется только 5.
//
// Безопасен для повторного вызова — выполнится только один раз.
func CloseAll(ctx context.Context) error {
return globalCloser.closeAll(ctx)
}
// add добавляет функцию закрытия с именем ресурса.
func (c *closer) add(name string, fn func(context.Context) error) {
c.mu.Lock()
defer c.mu.Unlock()
c.funcs = append(c.funcs, closeFn{name: name, fn: fn})
}
// closeAll вызывает все зарегистрированные функции закрытия в обратном порядке (LIFO).
//
// Порядок закрытия — обратный порядку добавления:
//
// closer.Add("база данных", dbClose) // добавлен первым
// closer.Add("кэш", cacheClose) // добавлен вторым
// closer.Add("HTTP-сервер", srvStop) // добавлен третьим
//
// При вызове CloseAll:
// 1. HTTP-сервер (добавлен последним — закрывается первым)
// 2. кэш
// 3. база данных (добавлена первой — закрывается последней)
//
// Если один ресурс не закрылся — остальные всё равно закроются.
func (c *closer) closeAll(ctx context.Context) error {
var result error
c.once.Do(func() {
// Забираем все функции под мьютексом и обнуляем слайс,
// чтобы не держать ссылки на ресурсы после закрытия.
c.mu.Lock()
funcs := c.funcs
c.funcs = nil
c.mu.Unlock()
if len(funcs) == 0 {
return
}
slog.Info("начинаем плавное завершение", "count", len(funcs))
var errs []error
// Идём от конца к началу — LIFO, как defer.
for i := len(funcs) - 1; i >= 0; i-- {
f := funcs[i]
start := time.Now()
slog.Info("закрываем ресурс", "name", f.name)
if err := f.fn(ctx); err != nil {
// Логируем ошибку, но продолжаем закрывать остальные ресурсы.
slog.Error("ошибка при закрытии ресурса",
"name", f.name,
"error", err,
"duration", time.Since(start),
)
errs = append(errs, err)
} else {
slog.Info("ресурс закрыт", "name", f.name, "duration", time.Since(start))
}
}
slog.Info("все ресурсы закрыты")
result = errors.Join(errs...)
})
return result
}