From ee6c2d63689f1314325ceee75a33051e2f87fa63 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 6 Mar 2026 00:33:21 +0300 Subject: [PATCH] ncro: wire all components; handle shutdown more gracefully Signed-off-by: NotAShelf Change-Id: I81fab7fa2cfa5a109b8b317fa53f10d86a6a6964 --- cmd/ncro/main.go | 109 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 5 deletions(-) diff --git a/cmd/ncro/main.go b/cmd/ncro/main.go index fd80f78..23da35c 100644 --- a/cmd/ncro/main.go +++ b/cmd/ncro/main.go @@ -1,14 +1,113 @@ package main import ( + "context" + "flag" "log/slog" + "net/http" "os" + "os/signal" + "syscall" + "time" + + "notashelf.dev/ncro/internal/cache" + "notashelf.dev/ncro/internal/config" + "notashelf.dev/ncro/internal/prober" + "notashelf.dev/ncro/internal/router" + "notashelf.dev/ncro/internal/server" ) func main() { - slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ - Level: slog.LevelInfo, - }))) - slog.Info("ncro starting") - os.Exit(0) + configPath := flag.String("config", "", "path to config YAML file") + flag.Parse() + + cfg, err := config.Load(*configPath) + if err != nil { + slog.Error("failed to load config", "error", err) + os.Exit(1) + } + + level := slog.LevelInfo + switch cfg.Logging.Level { + case "debug": + level = slog.LevelDebug + case "warn": + level = slog.LevelWarn + case "error": + level = slog.LevelError + } + var handler slog.Handler + if cfg.Logging.Format == "text" { + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}) + } else { + handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}) + } + slog.SetDefault(slog.New(handler)) + + db, err := cache.Open(cfg.Cache.DBPath, cfg.Cache.MaxEntries) + if err != nil { + slog.Error("failed to open database", "path", cfg.Cache.DBPath, "error", err) + os.Exit(1) + } + defer db.Close() + + expireDone := make(chan struct{}) + go func() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for { + select { + case <-expireDone: + return + case <-ticker.C: + if err := db.ExpireOldRoutes(); err != nil { + slog.Warn("expire routes error", "error", err) + } + } + } + }() + + p := prober.New(cfg.Cache.LatencyAlpha) + upstreamURLs := make([]string, len(cfg.Upstreams)) + for i, u := range cfg.Upstreams { + upstreamURLs[i] = u.URL + } + p.InitUpstreams(upstreamURLs) + for _, u := range upstreamURLs { + go p.ProbeUpstream(u) + } + + probeDone := make(chan struct{}) + go p.RunProbeLoop(30*time.Second, probeDone) + + r := router.New(db, p, cfg.Cache.TTL.Duration, 5*time.Second) + srv := &http.Server{ + Addr: cfg.Server.Listen, + Handler: server.New(r, p, cfg.Upstreams), + ReadTimeout: cfg.Server.ReadTimeout.Duration, + WriteTimeout: cfg.Server.WriteTimeout.Duration, + } + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + + go func() { + slog.Info("ncro listening", "addr", cfg.Server.Listen, "upstreams", len(cfg.Upstreams)) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + slog.Error("server error", "error", err) + os.Exit(1) + } + }() + + <-stop + slog.Info("shutting down") + + close(expireDone) + close(probeDone) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + slog.Error("shutdown error", "error", err) + } }