feat(server): запуск планировщика, /metrics, graceful shutdown
This commit is contained in:
+69
-10
@@ -2,9 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
@@ -13,8 +17,11 @@ import (
|
|||||||
"github.com/vasyakrg/dns-autoresolver/internal/auth"
|
"github.com/vasyakrg/dns-autoresolver/internal/auth"
|
||||||
"github.com/vasyakrg/dns-autoresolver/internal/config"
|
"github.com/vasyakrg/dns-autoresolver/internal/config"
|
||||||
"github.com/vasyakrg/dns-autoresolver/internal/crypto"
|
"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/registry"
|
||||||
"github.com/vasyakrg/dns-autoresolver/internal/provider/selectel"
|
"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/service"
|
||||||
"github.com/vasyakrg/dns-autoresolver/internal/store"
|
"github.com/vasyakrg/dns-autoresolver/internal/store"
|
||||||
"github.com/vasyakrg/dns-autoresolver/internal/web"
|
"github.com/vasyakrg/dns-autoresolver/internal/web"
|
||||||
@@ -24,6 +31,16 @@ import (
|
|||||||
// user must re-authenticate.
|
// user must re-authenticate.
|
||||||
const sessionTTL = 720 * time.Hour
|
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
|
// 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 —
|
// than the SPA. "/api" (no trailing slash) counts as an API path too —
|
||||||
// only strings.HasPrefix(path, "/api/") would otherwise miss it and fall
|
// only strings.HasPrefix(path, "/api/") would otherwise miss it and fall
|
||||||
@@ -33,7 +50,9 @@ func isAPIPath(path string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
cfg, err := config.Load()
|
cfg, err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("config: %v", err)
|
log.Fatalf("config: %v", err)
|
||||||
@@ -58,7 +77,14 @@ func main() {
|
|||||||
reg.Register(selectel.New())
|
reg.Register(selectel.New())
|
||||||
|
|
||||||
svc := service.New(st, st, reg, cipher)
|
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)
|
apiRouter := api.NewRouter(a)
|
||||||
|
|
||||||
webHandler, err := web.Handler()
|
webHandler, err := web.Handler()
|
||||||
@@ -66,20 +92,53 @@ func main() {
|
|||||||
log.Printf("web: static UI unavailable: %v", err)
|
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) {
|
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)
|
apiRouter.ServeHTTP(w, r)
|
||||||
return
|
case webHandler != nil:
|
||||||
}
|
|
||||||
if webHandler != nil {
|
|
||||||
webHandler.ServeHTTP(w, r)
|
webHandler.ServeHTTP(w, r)
|
||||||
return
|
default:
|
||||||
}
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
srv := &http.Server{Addr: cfg.ListenAddr, Handler: mux}
|
||||||
|
|
||||||
|
serveErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
log.Printf("listening on %s", cfg.ListenAddr)
|
log.Printf("listening on %s", cfg.ListenAddr)
|
||||||
if err := http.ListenAndServe(cfg.ListenAddr, mux); err != nil {
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Fatal(err)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package notify
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -76,3 +78,24 @@ func (d *Dispatcher) Send(ctx context.Context, projectID uuid.UUID, ev Event) er
|
|||||||
}
|
}
|
||||||
return errors.Join(errs...)
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendTest sends a single synthetic Event directly through the Notifier for
|
||||||
|
// channelType, bypassing project/channel lookup entirely. It satisfies
|
||||||
|
// api.TestSender and backs POST /channels/{cid}/test, letting a user verify
|
||||||
|
// a channel's bot_token/chat_id or webhook URL works before enabling the
|
||||||
|
// schedule — the api layer resolves the channel and decrypts its secret; this
|
||||||
|
// method only performs the actual delivery attempt.
|
||||||
|
func (d *Dispatcher) SendTest(ctx context.Context, channelType string, config json.RawMessage, secret string) error {
|
||||||
|
n, ok := d.byType[channelType]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("notify: unknown channel type %q", channelType)
|
||||||
|
}
|
||||||
|
ev := Event{
|
||||||
|
Project: "test",
|
||||||
|
Domain: "test",
|
||||||
|
Status: "test",
|
||||||
|
Summary: "test notification",
|
||||||
|
At: time.Now(),
|
||||||
|
}
|
||||||
|
return n.Send(ctx, config, secret, ev)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user