feat(server): запуск планировщика, /metrics, graceful shutdown

This commit is contained in:
2026-07-04 14:14:00 +07:00
parent 9475af441e
commit b31f886ae2
2 changed files with 93 additions and 11 deletions
+70 -11
View File
@@ -2,9 +2,13 @@ package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/jackc/pgx/v5/pgxpool"
@@ -13,8 +17,11 @@ import (
"github.com/vasyakrg/dns-autoresolver/internal/auth"
"github.com/vasyakrg/dns-autoresolver/internal/config"
"github.com/vasyakrg/dns-autoresolver/internal/crypto"
"github.com/vasyakrg/dns-autoresolver/internal/metrics"
"github.com/vasyakrg/dns-autoresolver/internal/notify"
"github.com/vasyakrg/dns-autoresolver/internal/provider/registry"
"github.com/vasyakrg/dns-autoresolver/internal/provider/selectel"
"github.com/vasyakrg/dns-autoresolver/internal/scheduler"
"github.com/vasyakrg/dns-autoresolver/internal/service"
"github.com/vasyakrg/dns-autoresolver/internal/store"
"github.com/vasyakrg/dns-autoresolver/internal/web"
@@ -24,6 +31,16 @@ import (
// user must re-authenticate.
const sessionTTL = 720 * time.Hour
// schedulerTick is how often the in-process scheduler checks for due project
// schedules. Individual projects only actually run when their own
// schedules.interval_seconds has elapsed (see internal/store ListDueSchedules) —
// this is just the polling granularity.
const schedulerTick = time.Minute
// shutdownTimeout bounds how long graceful shutdown waits for in-flight HTTP
// requests to finish before forcing the listener closed.
const shutdownTimeout = 10 * time.Second
// isAPIPath reports whether path must be routed to the API router rather
// than the SPA. "/api" (no trailing slash) counts as an API path too —
// only strings.HasPrefix(path, "/api/") would otherwise miss it and fall
@@ -33,7 +50,9 @@ func isAPIPath(path string) bool {
}
func main() {
ctx := context.Background()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
@@ -58,7 +77,14 @@ func main() {
reg.Register(selectel.New())
svc := service.New(st, st, reg, cipher)
a := &api.API{Svc: svc, Store: st, Cipher: cipher, Reg: reg, Auth: st, Sessions: sessions}
m := metrics.New()
dispatcher := notify.NewDispatcher(st, cipher)
a := &api.API{
Svc: svc, Store: st, Cipher: cipher, Reg: reg, Auth: st, Sessions: sessions,
Schedule: st, Dispatch: dispatcher,
}
apiRouter := api.NewRouter(a)
webHandler, err := web.Handler()
@@ -66,20 +92,53 @@ func main() {
log.Printf("web: static UI unavailable: %v", err)
}
// The scheduler only checks and notifies — it never applies zone changes
// (Apply stays a manual, explicit API call). Its own errors are logged
// internally and never stop the loop; ctx cancellation (signal) is the
// only thing that ends Run.
sched := scheduler.New(st, svc, dispatcher, m)
go sched.Run(ctx, schedulerTick)
mux := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isAPIPath(r.URL.Path) {
switch {
case r.URL.Path == "/metrics":
// Public by design (no auth) — Metrics.Handler only ever exposes
// aggregate counters/gauges, never per-domain or secret data.
m.Handler().ServeHTTP(w, r)
case isAPIPath(r.URL.Path):
apiRouter.ServeHTTP(w, r)
return
}
if webHandler != nil {
case webHandler != nil:
webHandler.ServeHTTP(w, r)
return
default:
http.NotFound(w, r)
}
http.NotFound(w, r)
})
log.Printf("listening on %s", cfg.ListenAddr)
if err := http.ListenAndServe(cfg.ListenAddr, mux); err != nil {
log.Fatal(err)
srv := &http.Server{Addr: cfg.ListenAddr, Handler: mux}
serveErr := make(chan error, 1)
go func() {
log.Printf("listening on %s", cfg.ListenAddr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
serveErr <- err
return
}
serveErr <- nil
}()
select {
case <-ctx.Done():
log.Printf("shutdown signal received, draining connections (timeout %s)", shutdownTimeout)
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("server: graceful shutdown failed: %v", err)
}
<-serveErr
log.Printf("server stopped")
case err := <-serveErr:
if err != nil {
log.Fatalf("server: %v", err)
}
}
}