import
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserService — интерфейс сервиса пользователей для хендлера.
|
||||
type UserService interface {
|
||||
GetProfile(token string) string
|
||||
}
|
||||
|
||||
// AuthService — интерфейс сервиса авторизации для хендлера.
|
||||
type AuthService interface {
|
||||
// методы, которые хендлер использует из сервиса авторизации
|
||||
}
|
||||
|
||||
// NotificationService — интерфейс сервиса уведомлений для хендлера.
|
||||
type NotificationService interface {
|
||||
// методы, которые хендлер использует из сервиса уведомлений
|
||||
}
|
||||
|
||||
// Handler — интерфейс обработчика HTTP-запросов.
|
||||
type Handler interface {
|
||||
Routes() http.Handler
|
||||
}
|
||||
|
||||
// handler — конкретная реализация, скрыта от внешних пакетов.
|
||||
// Содержит только хендлеры и роутинг.
|
||||
// Ничего не знает про http.Server, порт или lifecycle —
|
||||
// это ответственность app-слоя.
|
||||
type handler struct {
|
||||
userService UserService
|
||||
authService AuthService
|
||||
notificationService NotificationService
|
||||
}
|
||||
|
||||
// NewHandler создаёт обработчик HTTP-запросов.
|
||||
func NewHandler(
|
||||
userService UserService,
|
||||
authService AuthService,
|
||||
notificationService NotificationService,
|
||||
) Handler {
|
||||
return &handler{
|
||||
userService: userService,
|
||||
authService: authService,
|
||||
notificationService: notificationService,
|
||||
}
|
||||
}
|
||||
|
||||
// Routes возвращает маршрутизатор со всеми зарегистрированными хендлерами.
|
||||
// App-слой использует этот http.Handler при создании http.Server.
|
||||
func (h *handler) Routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /health", h.healthHandler)
|
||||
mux.HandleFunc("GET /users/me", h.getUserProfile)
|
||||
|
||||
// /slow — эндпоинт для демонстрации graceful shutdown.
|
||||
// Имитирует долгий запрос: обращение к БД, вызов внешнего API и т.д.
|
||||
// Пять секунд — чтобы мы успели нажать Ctrl+C, пока запрос выполняется.
|
||||
mux.HandleFunc("GET /slow", h.slowHandler)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func (h *handler) healthHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if _, err := fmt.Fprintln(w, "ok"); err != nil {
|
||||
slog.Error("ошибка записи ответа", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) getUserProfile(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
profile := h.userService.GetProfile(token)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if _, err := fmt.Fprintln(w, profile); err != nil {
|
||||
slog.Error("ошибка записи ответа", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// slowHandler — медленный эндпоинт для демонстрации graceful shutdown.
|
||||
// Имитирует реальную работу: запрос в базу, вызов внешнего сервиса.
|
||||
//
|
||||
// Попробуйте:
|
||||
// 1. curl localhost:8080/slow
|
||||
// 2. Пока запрос висит — нажмите Ctrl+C в терминале сервера
|
||||
// 3. Без graceful shutdown: curl получит "connection reset by peer"
|
||||
// 4. С graceful shutdown: curl дождётся и получит "готово!" — 200 OK
|
||||
func (h *handler) slowHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
slog.Info("обрабатываю медленный запрос...")
|
||||
|
||||
time.Sleep(5 * time.Second) // имитация: запрос в БД, внешний API
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if _, err := fmt.Fprintln(w, "готово!"); err != nil {
|
||||
slog.Error("ошибка записи ответа", "err", err)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/olezhek28/graceful-shutdown-demo/internal/api"
|
||||
"github.com/olezhek28/graceful-shutdown-demo/internal/cache"
|
||||
"github.com/olezhek28/graceful-shutdown-demo/internal/closer"
|
||||
"github.com/olezhek28/graceful-shutdown-demo/internal/config"
|
||||
"github.com/olezhek28/graceful-shutdown-demo/internal/database"
|
||||
"github.com/olezhek28/graceful-shutdown-demo/internal/events"
|
||||
"github.com/olezhek28/graceful-shutdown-demo/internal/repository"
|
||||
"github.com/olezhek28/graceful-shutdown-demo/internal/service"
|
||||
)
|
||||
|
||||
// diContainer — контейнер зависимостей с ленивой инициализацией.
|
||||
//
|
||||
// Это тот же контейнер из видео про DI, но при создании каждого ресурса
|
||||
// с методом Close() мы сразу регистрируем его в глобальном closer.
|
||||
// closer.Add() вызывается прямо в геттере — одна строка, и ресурс
|
||||
// автоматически закроется при graceful shutdown в правильном LIFO-порядке.
|
||||
//
|
||||
// Почему порядок закрытия детерминирован при ленивой инициализации:
|
||||
// initDeps() вызывается в одной горутине. Ленивая инициализация — это
|
||||
// depth-first обход графа зависимостей. Порядок определяется графом:
|
||||
//
|
||||
// Handler() → UserService() → UserRepo() → DB() ← 1-й closer.Add
|
||||
// → AuthService() → SessionRepo() → Cache() ← 2-й closer.Add
|
||||
//
|
||||
// DB всегда создаётся раньше Cache (UserRepo запрашивает DB до того,
|
||||
// как SessionRepo запросит Cache). Граф не меняется → порядок не меняется.
|
||||
type diContainer struct {
|
||||
// Инфраструктура
|
||||
db database.DB
|
||||
cache cache.Cache
|
||||
eventBus events.EventBus
|
||||
|
||||
// Репозитории
|
||||
userRepo repository.UserRepo
|
||||
sessionRepo repository.SessionRepo
|
||||
notificationRepo repository.NotificationRepo
|
||||
|
||||
// Сервисы
|
||||
authService service.AuthService
|
||||
userService service.UserService
|
||||
notificationService service.NotificationService
|
||||
|
||||
// API
|
||||
handler api.Handler
|
||||
}
|
||||
|
||||
// newDIContainer создаёт новый пустой контейнер.
|
||||
func newDIContainer() *diContainer {
|
||||
return &diContainer{}
|
||||
}
|
||||
|
||||
// DB возвращает подключение к базе данных.
|
||||
// При создании — сразу регистрирует Close() в глобальном closer.
|
||||
// БД создаётся одной из первых — значит закроется одной из последних (LIFO).
|
||||
func (d *diContainer) DB() database.DB {
|
||||
if d.db == nil {
|
||||
db, err := database.New(config.AppConfig().DSN)
|
||||
if err != nil {
|
||||
slog.Error("не удалось подключиться к БД", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
closer.Add("база данных", func(_ context.Context) error {
|
||||
return db.Close()
|
||||
})
|
||||
|
||||
d.db = db
|
||||
}
|
||||
|
||||
return d.db
|
||||
}
|
||||
|
||||
// EventBus возвращает шину событий.
|
||||
func (d *diContainer) EventBus() events.EventBus {
|
||||
if d.eventBus == nil {
|
||||
d.eventBus = events.NewEventBus()
|
||||
}
|
||||
|
||||
return d.eventBus
|
||||
}
|
||||
|
||||
// Cache возвращает подключение к кэшу.
|
||||
// При создании — сразу регистрирует Close() в глобальном closer.
|
||||
// Кэш создаётся после БД — значит закроется раньше БД (LIFO).
|
||||
func (d *diContainer) Cache() cache.Cache {
|
||||
if d.cache == nil {
|
||||
c := cache.New(config.AppConfig().RedisAddr)
|
||||
|
||||
closer.Add("кэш", func(_ context.Context) error {
|
||||
return c.Close()
|
||||
})
|
||||
|
||||
d.cache = c
|
||||
}
|
||||
|
||||
return d.cache
|
||||
}
|
||||
|
||||
// UserRepo возвращает репозиторий пользователей.
|
||||
func (d *diContainer) UserRepo() repository.UserRepo {
|
||||
if d.userRepo == nil {
|
||||
d.userRepo = repository.NewUserRepo(d.DB())
|
||||
}
|
||||
|
||||
return d.userRepo
|
||||
}
|
||||
|
||||
// SessionRepo возвращает репозиторий сессий.
|
||||
func (d *diContainer) SessionRepo() repository.SessionRepo {
|
||||
if d.sessionRepo == nil {
|
||||
d.sessionRepo = repository.NewSessionRepo(d.DB(), d.Cache())
|
||||
}
|
||||
|
||||
return d.sessionRepo
|
||||
}
|
||||
|
||||
// NotificationRepo возвращает репозиторий уведомлений.
|
||||
func (d *diContainer) NotificationRepo() repository.NotificationRepo {
|
||||
if d.notificationRepo == nil {
|
||||
d.notificationRepo = repository.NewNotificationRepo(d.DB())
|
||||
}
|
||||
|
||||
return d.notificationRepo
|
||||
}
|
||||
|
||||
// AuthService возвращает сервис авторизации.
|
||||
func (d *diContainer) AuthService() service.AuthService {
|
||||
if d.authService == nil {
|
||||
d.authService = service.NewAuthService(d.UserRepo(), d.SessionRepo(), d.Cache(), d.EventBus())
|
||||
}
|
||||
|
||||
return d.authService
|
||||
}
|
||||
|
||||
// UserService возвращает сервис пользователей.
|
||||
func (d *diContainer) UserService() service.UserService {
|
||||
if d.userService == nil {
|
||||
d.userService = service.NewUserService(d.UserRepo(), d.AuthService(), d.EventBus())
|
||||
}
|
||||
|
||||
return d.userService
|
||||
}
|
||||
|
||||
// NotificationService возвращает сервис уведомлений.
|
||||
func (d *diContainer) NotificationService() service.NotificationService {
|
||||
if d.notificationService == nil {
|
||||
d.notificationService = service.NewNotificationService(d.NotificationRepo(), d.UserService(), d.EventBus())
|
||||
}
|
||||
|
||||
return d.notificationService
|
||||
}
|
||||
|
||||
// Handler возвращает HTTP-хендлер.
|
||||
func (d *diContainer) Handler() api.Handler {
|
||||
if d.handler == nil {
|
||||
d.handler = api.NewHandler(
|
||||
d.UserService(),
|
||||
d.AuthService(),
|
||||
d.NotificationService(),
|
||||
)
|
||||
}
|
||||
|
||||
return d.handler
|
||||
}
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
package cache
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// Cache — интерфейс подключения к кэшу.
|
||||
type Cache interface {
|
||||
Get(key string) (string, error)
|
||||
Set(key, value string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// redisCache — конкретная реализация, скрыта от внешних пакетов.
|
||||
type redisCache struct {
|
||||
addr string
|
||||
}
|
||||
|
||||
// New создаёт подключение к кэшу.
|
||||
func New(addr string) Cache {
|
||||
slog.Info("подключились к кэшу", "addr", addr)
|
||||
return &redisCache{addr: addr}
|
||||
}
|
||||
|
||||
// Get получает значение по ключу.
|
||||
func (c *redisCache) Get(key string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Set устанавливает значение по ключу.
|
||||
func (c *redisCache) Set(key, value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close закрывает подключение к кэшу.
|
||||
func (c *redisCache) Close() error {
|
||||
slog.Info("подключение к кэшу закрыто")
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package config
|
||||
|
||||
// Config хранит конфигурацию приложения.
|
||||
type Config struct {
|
||||
DSN string
|
||||
RedisAddr string
|
||||
HTTPAddr string
|
||||
}
|
||||
|
||||
// appConfig — глобальный экземпляр конфигурации.
|
||||
var appConfig = &Config{
|
||||
DSN: "postgres://localhost:5432/demo",
|
||||
RedisAddr: "localhost:6379",
|
||||
HTTPAddr: ":8080",
|
||||
}
|
||||
|
||||
// AppConfig возвращает конфигурацию приложения.
|
||||
func AppConfig() *Config {
|
||||
return appConfig
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// DB — интерфейс подключения к базе данных.
|
||||
type DB interface {
|
||||
Query(query string) error
|
||||
QueryRow(query string) error
|
||||
Exec(query string) error
|
||||
BeginTx() error
|
||||
BulkInsert(query string, args ...any) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// db — конкретная реализация, скрыта от внешних пакетов.
|
||||
type db struct {
|
||||
dsn string
|
||||
}
|
||||
|
||||
// New создаёт подключение к базе данных.
|
||||
func New(dsn string) (DB, error) {
|
||||
if dsn == "" {
|
||||
return nil, fmt.Errorf("dsn пустой")
|
||||
}
|
||||
|
||||
slog.Info("подключились к базе данных", "dsn", dsn)
|
||||
return &db{dsn: dsn}, nil
|
||||
}
|
||||
|
||||
// Query выполняет запрос на чтение.
|
||||
func (d *db) Query(query string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueryRow выполняет запрос, возвращающий одну строку.
|
||||
func (d *db) QueryRow(query string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exec выполняет запрос на запись.
|
||||
func (d *db) Exec(query string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeginTx начинает транзакцию.
|
||||
func (d *db) BeginTx() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BulkInsert выполняет массовую вставку.
|
||||
func (d *db) BulkInsert(query string, args ...any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close закрывает подключение к базе данных.
|
||||
func (d *db) Close() error {
|
||||
slog.Info("подключение к базе данных закрыто")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package events
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// EventBus — интерфейс шины событий приложения.
|
||||
// Чистый pub/sub брокер без внешних зависимостей.
|
||||
// Сервисы сами публикуют события и подписываются на них.
|
||||
type EventBus interface {
|
||||
Publish(event string)
|
||||
Subscribe(event string, handler func())
|
||||
}
|
||||
|
||||
// eventBus — конкретная реализация, скрыта от внешних пакетов.
|
||||
type eventBus struct{}
|
||||
|
||||
// NewEventBus создаёт шину событий.
|
||||
func NewEventBus() EventBus {
|
||||
slog.Info("шина событий создана")
|
||||
return &eventBus{}
|
||||
}
|
||||
|
||||
// Publish публикует событие.
|
||||
func (b *eventBus) Publish(event string) {
|
||||
slog.Info("событие опубликовано", "event", event)
|
||||
}
|
||||
|
||||
// Subscribe подписывается на событие.
|
||||
func (b *eventBus) Subscribe(event string, handler func()) {
|
||||
slog.Info("подписка на событие", "event", event)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package repository
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// NotificationDB — интерфейс базы данных, который нужен NotificationRepo.
|
||||
type NotificationDB interface {
|
||||
Query(query string) error
|
||||
Exec(query string) error
|
||||
BulkInsert(query string, args ...any) error
|
||||
}
|
||||
|
||||
// NotificationRepo — интерфейс репозитория уведомлений.
|
||||
type NotificationRepo interface{}
|
||||
|
||||
// notificationRepo — конкретная реализация, скрыта от внешних пакетов.
|
||||
type notificationRepo struct {
|
||||
db NotificationDB
|
||||
}
|
||||
|
||||
// NewNotificationRepo создаёт репозиторий уведомлений.
|
||||
func NewNotificationRepo(db NotificationDB) NotificationRepo {
|
||||
slog.Info("репозиторий уведомлений создан")
|
||||
return ¬ificationRepo{db: db}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package repository
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// SessionDB — интерфейс базы данных, который нужен SessionRepo.
|
||||
type SessionDB interface {
|
||||
Query(query string) error
|
||||
Exec(query string) error
|
||||
BeginTx() error
|
||||
}
|
||||
|
||||
// SessionCache — интерфейс кэша, который нужен SessionRepo.
|
||||
type SessionCache interface {
|
||||
Get(key string) (string, error)
|
||||
Set(key, value string) error
|
||||
}
|
||||
|
||||
// SessionRepo — интерфейс репозитория сессий.
|
||||
type SessionRepo interface{}
|
||||
|
||||
// sessionRepo — конкретная реализация, скрыта от внешних пакетов.
|
||||
type sessionRepo struct {
|
||||
db SessionDB
|
||||
cache SessionCache
|
||||
}
|
||||
|
||||
// NewSessionRepo создаёт репозиторий сессий.
|
||||
func NewSessionRepo(db SessionDB, cache SessionCache) SessionRepo {
|
||||
slog.Info("репозиторий сессий создан")
|
||||
return &sessionRepo{db: db, cache: cache}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package repository
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// UserDB — интерфейс базы данных, который нужен UserRepo.
|
||||
type UserDB interface {
|
||||
Query(query string) error
|
||||
QueryRow(query string) error
|
||||
Exec(query string) error
|
||||
}
|
||||
|
||||
// UserRepo — интерфейс репозитория пользователей.
|
||||
type UserRepo interface{}
|
||||
|
||||
// userRepo — конкретная реализация, скрыта от внешних пакетов.
|
||||
type userRepo struct {
|
||||
db UserDB
|
||||
}
|
||||
|
||||
// NewUserRepo создаёт репозиторий пользователей.
|
||||
func NewUserRepo(db UserDB) UserRepo {
|
||||
slog.Info("репозиторий пользователей создан")
|
||||
return &userRepo{db: db}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package service
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// AuthUserRepository — интерфейс репозитория пользователей для AuthService.
|
||||
type AuthUserRepository interface {
|
||||
// методы, которые AuthService использует из репозитория пользователей
|
||||
}
|
||||
|
||||
// AuthSessionRepository — интерфейс репозитория сессий для AuthService.
|
||||
type AuthSessionRepository interface {
|
||||
// методы, которые AuthService использует из репозитория сессий
|
||||
}
|
||||
|
||||
// AuthCache — интерфейс кэша для AuthService (хранит токены).
|
||||
type AuthCache interface {
|
||||
Get(key string) (string, error)
|
||||
Set(key, value string) error
|
||||
}
|
||||
|
||||
// AuthEventPublisher — интерфейс для публикации событий авторизации.
|
||||
type AuthEventPublisher interface {
|
||||
Publish(event string)
|
||||
}
|
||||
|
||||
// AuthService — интерфейс сервиса авторизации.
|
||||
type AuthService interface {
|
||||
ValidateToken(token string) bool
|
||||
}
|
||||
|
||||
// authService — конкретная реализация, скрыта от внешних пакетов.
|
||||
type authService struct {
|
||||
userRepo AuthUserRepository
|
||||
sessionRepo AuthSessionRepository
|
||||
cache AuthCache
|
||||
events AuthEventPublisher
|
||||
}
|
||||
|
||||
// NewAuthService создаёт сервис авторизации.
|
||||
func NewAuthService(
|
||||
userRepo AuthUserRepository,
|
||||
sessionRepo AuthSessionRepository,
|
||||
cache AuthCache,
|
||||
events AuthEventPublisher,
|
||||
) AuthService {
|
||||
slog.Info("сервис авторизации создан")
|
||||
return &authService{
|
||||
userRepo: userRepo,
|
||||
sessionRepo: sessionRepo,
|
||||
cache: cache,
|
||||
events: events,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateToken проверяет токен авторизации.
|
||||
func (s *authService) ValidateToken(token string) bool {
|
||||
slog.Info("проверяем токен", "token", token)
|
||||
|
||||
_, err := s.cache.Get(token)
|
||||
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package service
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// NotificationRepository — интерфейс репозитория уведомлений для NotificationService.
|
||||
type NotificationRepository interface {
|
||||
// методы, которые NotificationService использует из репозитория уведомлений
|
||||
}
|
||||
|
||||
// NotificationUserService — интерфейс сервиса пользователей для NotificationService.
|
||||
type NotificationUserService interface {
|
||||
// методы, которые NotificationService использует из сервиса пользователей
|
||||
}
|
||||
|
||||
// NotificationEventSubscriber — интерфейс для подписки на события.
|
||||
type NotificationEventSubscriber interface {
|
||||
Subscribe(event string, handler func())
|
||||
}
|
||||
|
||||
// NotificationService — интерфейс сервиса уведомлений.
|
||||
type NotificationService interface{}
|
||||
|
||||
// notificationService — конкретная реализация, скрыта от внешних пакетов.
|
||||
type notificationService struct {
|
||||
notificationRepo NotificationRepository
|
||||
userService NotificationUserService
|
||||
events NotificationEventSubscriber
|
||||
}
|
||||
|
||||
// NewNotificationService создаёт сервис уведомлений.
|
||||
func NewNotificationService(
|
||||
notificationRepo NotificationRepository,
|
||||
userService NotificationUserService,
|
||||
events NotificationEventSubscriber,
|
||||
) NotificationService {
|
||||
slog.Info("сервис уведомлений создан")
|
||||
return ¬ificationService{
|
||||
notificationRepo: notificationRepo,
|
||||
userService: userService,
|
||||
events: events,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package service
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// UserRepository — интерфейс репозитория пользователей для UserService.
|
||||
type UserRepository interface {
|
||||
// методы, которые UserService использует из репозитория пользователей
|
||||
}
|
||||
|
||||
// UserAuthService — интерфейс сервиса авторизации для UserService.
|
||||
type UserAuthService interface {
|
||||
ValidateToken(token string) bool
|
||||
}
|
||||
|
||||
// UserEventPublisher — интерфейс для публикации событий пользователей.
|
||||
type UserEventPublisher interface {
|
||||
Publish(event string)
|
||||
}
|
||||
|
||||
// UserService — интерфейс сервиса пользователей.
|
||||
type UserService interface {
|
||||
GetProfile(token string) string
|
||||
}
|
||||
|
||||
// userService — конкретная реализация, скрыта от внешних пакетов.
|
||||
type userService struct {
|
||||
userRepo UserRepository
|
||||
authService UserAuthService
|
||||
events UserEventPublisher
|
||||
}
|
||||
|
||||
// NewUserService создаёт сервис пользователей.
|
||||
func NewUserService(userRepo UserRepository, authService UserAuthService, events UserEventPublisher) UserService {
|
||||
slog.Info("сервис пользователей создан")
|
||||
return &userService{
|
||||
userRepo: userRepo,
|
||||
authService: authService,
|
||||
events: events,
|
||||
}
|
||||
}
|
||||
|
||||
// GetProfile возвращает профиль текущего пользователя.
|
||||
func (s *userService) GetProfile(token string) string {
|
||||
if !s.authService.ValidateToken(token) {
|
||||
return "unauthorized"
|
||||
}
|
||||
|
||||
return "user profile data"
|
||||
}
|
||||
Reference in New Issue
Block a user