feat(server): запуск планировщика, /metrics, graceful shutdown
This commit is contained in:
+70
-11
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user