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