132 lines
5.6 KiB
Go
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
|
|
}
|