check / check (push) Successful in 1m11s
The server ran os.Exit at the end of its own goroutine, racing fx's teardown and sometimes killing the process before reportbuf's OnStop flushed — silently losing a full flush window of telemetry on every restart, at exit 0. Shutdown now goes through fx.Shutdowner, so every OnStop runs in order. The http.Server is built synchronously in OnStart before the serving goroutine, so shutdown can no longer race or nil-deref it. A listen failure exits non-zero via fx.ExitCode(1). reportbuf's OnStop is guarded by sync.Once. writeTimeout now exceeds the chi per-request budget so that budget is reachable. Dead startupTime, exitCode, and cancelFunc fields are gone. A new test asserts a buffered report reaches disk after the lifecycle stops. Model: opus-4-8
104 lines
2.4 KiB
Go
104 lines
2.4 KiB
Go
// Package server provides the HTTP server lifecycle,
|
|
// including startup, routing, and graceful shutdown. The
|
|
// process lifetime is owned by fx: shutdown is requested
|
|
// through fx.Shutdowner so every component's OnStop hook runs
|
|
// in dependency order.
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"sneak.berlin/go/netwatch/internal/config"
|
|
"sneak.berlin/go/netwatch/internal/globals"
|
|
"sneak.berlin/go/netwatch/internal/handlers"
|
|
"sneak.berlin/go/netwatch/internal/logger"
|
|
"sneak.berlin/go/netwatch/internal/middleware"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"go.uber.org/fx"
|
|
)
|
|
|
|
// Params defines the dependencies for Server.
|
|
type Params struct {
|
|
fx.In
|
|
|
|
Config *config.Config
|
|
Globals *globals.Globals
|
|
Handlers *handlers.Handlers
|
|
Logger *logger.Logger
|
|
Middleware *middleware.Middleware
|
|
Shutdowner fx.Shutdowner
|
|
}
|
|
|
|
// Server is the top-level HTTP server orchestrator.
|
|
type Server struct {
|
|
h *handlers.Handlers
|
|
httpServer *http.Server
|
|
log *slog.Logger
|
|
mw *middleware.Middleware
|
|
params Params
|
|
router *chi.Mux
|
|
shutdowner fx.Shutdowner
|
|
}
|
|
|
|
// New creates a Server and registers lifecycle hooks for
|
|
// starting and stopping it.
|
|
func New(
|
|
lc fx.Lifecycle,
|
|
params Params,
|
|
) (*Server, error) {
|
|
s := new(Server)
|
|
s.params = params
|
|
s.mw = params.Middleware
|
|
s.h = params.Handlers
|
|
s.log = params.Logger.Get()
|
|
s.shutdowner = params.Shutdowner
|
|
|
|
lc.Append(fx.Hook{
|
|
OnStart: func(_ context.Context) error {
|
|
// Build the router and http.Server synchronously
|
|
// here, before spawning the serving goroutine, so
|
|
// httpServer is fully constructed by the time OnStop
|
|
// (or an early signal) can read it. fx guarantees
|
|
// OnStart returns before OnStop runs, so no
|
|
// synchronization or nil check is needed at shutdown.
|
|
s.SetupRoutes()
|
|
s.httpServer = s.newHTTPServer()
|
|
|
|
go s.listenAndServe()
|
|
|
|
return nil
|
|
},
|
|
OnStop: func(ctx context.Context) error {
|
|
return s.shutdown(ctx)
|
|
},
|
|
})
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// ServeHTTP delegates to the chi router.
|
|
func (s *Server) ServeHTTP(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
) {
|
|
s.router.ServeHTTP(w, r)
|
|
}
|
|
|
|
// shutdown gracefully stops the HTTP server within the
|
|
// deadline of the context fx provides for OnStop.
|
|
func (s *Server) shutdown(ctx context.Context) error {
|
|
err := s.httpServer.Shutdown(ctx)
|
|
if err != nil {
|
|
s.log.Error("server clean shutdown failed", "error", err)
|
|
|
|
return err
|
|
}
|
|
|
|
s.log.Info("server stopped")
|
|
|
|
return nil
|
|
}
|