// Package system — служебные эндпоинты: живость, готовность, сведения о сервере. package system import ( "context" "log/slog" "sync" "time" "pipu/api/internal/api/gen" ) // Checker проверяет одну зависимость (БД, Redis, ...). Возвращает ошибку, если она недоступна. type Checker interface { Check(ctx context.Context) error } // CheckerFunc адаптирует функцию к Checker. type CheckerFunc func(ctx context.Context) error // Check реализует Checker. func (f CheckerFunc) Check(ctx context.Context) error { return f(ctx) } // Info — статические сведения о сервере. type Info struct { Version string SupportedLocales []string MinAndroidVersionCode int } // Handler реализует системную часть gen.StrictServerInterface. type Handler struct { info Info log *slog.Logger checks map[string]Checker checkTimeout time.Duration } // NewHandler создаёт обработчик. checks — именованные проверки для /readyz. func NewHandler(info Info, log *slog.Logger, checks map[string]Checker) *Handler { if log == nil { log = slog.New(slog.DiscardHandler) } return &Handler{info: info, log: log, checks: checks, checkTimeout: 2 * time.Second} } // GetHealthz сообщает, что процесс жив; зависимости не проверяет. func (h *Handler) GetHealthz(context.Context, gen.GetHealthzRequestObject) (gen.GetHealthzResponseObject, error) { return gen.GetHealthz200JSONResponse{Status: gen.HealthStatusStatusOk}, nil } // GetReadyz параллельно проверяет все зависимости; 503, если хотя бы одна недоступна. // /readyz доступен снаружи, поэтому текст ошибки пишется только в лог, а в ответ — "fail". func (h *Handler) GetReadyz(ctx context.Context, _ gen.GetReadyzRequestObject) (gen.GetReadyzResponseObject, error) { ctx, cancel := context.WithTimeout(ctx, h.checkTimeout) defer cancel() var ( mu sync.Mutex wg sync.WaitGroup results = make(map[string]string, len(h.checks)) healthy = true ) for name, c := range h.checks { wg.Add(1) go func() { defer wg.Done() err := c.Check(ctx) mu.Lock() defer mu.Unlock() if err != nil { h.log.WarnContext(ctx, "readiness check failed", "check", name, "err", err) results[name] = "fail" healthy = false return } results[name] = "ok" }() } wg.Wait() if !healthy { return gen.GetReadyz503JSONResponse{Status: gen.ReadyStatusStatusDegraded, Checks: results}, nil } return gen.GetReadyz200JSONResponse{Status: gen.ReadyStatusStatusOk, Checks: results}, nil } // GetServerInfo отдаёт сведения о сервере для экрана подключения клиентов. func (h *Handler) GetServerInfo(context.Context, gen.GetServerInfoRequestObject) (gen.GetServerInfoResponseObject, error) { return gen.GetServerInfo200JSONResponse{ Name: "pipu-music", Version: h.info.Version, ApiVersion: "v1", SupportedLocales: h.info.SupportedLocales, MinAndroidVersionCode: h.info.MinAndroidVersionCode, }, nil }