check / check (push) Successful in 4m10s
fx alone handles SIGINT and SIGTERM; the server's own handler, which only cancelled a context that fx's stop did not wait for, is gone. fx's Run exits with the shutdown's code: the one a shutdown request carries, 0 for a signal, 1 when the app fails to start or stop. A listen error asks fx to shut down with exit code 1. A Sentry DSN that cannot be used fails the server's start hook, so fx stops what had already started. The server's stop hook stops the HTTP server, then waits for the images still being processed, both within ShutdownTimeout; images still being processed after that are logged and fail the stop, so the exit code is 1. Model: opus-5-5
149 lines
3.5 KiB
Go
149 lines
3.5 KiB
Go
// 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
|
|
}
|