// Package server provides the HTTP server and lifecycle management. package server import ( "context" "errors" "fmt" "log/slog" "net/http" "time" "github.com/getsentry/sentry-go" "github.com/go-chi/chi/v5" "go.uber.org/fx" "sneak.berlin/go/pixa/internal/config" "sneak.berlin/go/pixa/internal/globals" "sneak.berlin/go/pixa/internal/handlers" "sneak.berlin/go/pixa/internal/logger" "sneak.berlin/go/pixa/internal/middleware" ) // Shutdown configuration constants. const ( ShutdownTimeout = 5 * time.Second SentryFlushTimeout = 2 * time.Second ) // errStillProcessing is returned by the server's stop hook when images are // still being processed once ShutdownTimeout has passed. var errStillProcessing = errors.New("images still being processed at shutdown") // Params defines dependencies for Server. type Params struct { fx.In Logger *logger.Logger Globals *globals.Globals Config *config.Config Middleware *middleware.Middleware Handlers *handlers.Handlers Shutdowner fx.Shutdowner } // Server is the main HTTP server. type Server struct { log *slog.Logger config *config.Config globals *globals.Globals mw *middleware.Middleware h *handlers.Handlers shutdowner fx.Shutdowner startupTime time.Time sentryEnabled bool httpServer *http.Server router *chi.Mux } // New creates a new Server instance. Its start hook starts Sentry and the // HTTP server; its stop hook, which fx runs on SIGINT, SIGTERM or a // shutdown request, shuts them down. func New(lc fx.Lifecycle, params Params) (*Server, error) { s := &Server{ log: params.Logger.Get(), config: params.Config, globals: params.Globals, mw: params.Middleware, h: params.Handlers, shutdowner: params.Shutdowner, } lc.Append(fx.Hook{ OnStart: func(_ context.Context) error { s.startupTime = time.Now() err := s.enableSentry() if err != nil { return err } s.SetupRoutes() s.httpServer = s.newHTTPServer() go s.serveUntilShutdown() return nil }, OnStop: s.cleanShutdown, }) return s, nil } // MaintenanceMode returns whether maintenance mode is enabled. func (s *Server) MaintenanceMode() bool { return s.config.MaintenanceMode } func (s *Server) enableSentry() error { s.sentryEnabled = false if s.config.SentryDSN == "" { return nil } err := sentry.Init(sentry.ClientOptions{ Dsn: s.config.SentryDSN, Release: fmt.Sprintf("%s-%s", s.globals.Appname, s.globals.Version), }) if err != nil { return fmt.Errorf("sentry init failure: %w", err) } s.log.Info("sentry error reporting activated") s.sentryEnabled = true return nil } // cleanShutdown stops the HTTP server, waits for the images still being // processed, then flushes Sentry. The first two share ShutdownTimeout. It // returns errStillProcessing when images are still being processed after // that, as their work is abandoned. func (s *Server) cleanShutdown(ctx context.Context) error { s.log.Info("shutting down") ctxShutdown, shutdownCancel := context.WithTimeout(ctx, ShutdownTimeout) defer shutdownCancel() err := s.httpServer.Shutdown(ctxShutdown) if err != nil { s.log.Error("server clean shutdown failed", "error", err) } stillProcessing := s.h.WaitForProcessing(ctxShutdown) if s.sentryEnabled { sentry.Flush(SentryFlushTimeout) } if stillProcessing > 0 { s.log.Error("images still being processed at shutdown", "count", stillProcessing) return errStillProcessing } return nil }