The serve() method called cleanShutdown() after ctx.Done(), and the fx OnStop hook also called cleanShutdown(). Remove the call in serve() so shutdown happens exactly once via the fx lifecycle.
155 lines
3.4 KiB
Go
155 lines
3.4 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/webhooker/internal/config"
|
|
"sneak.berlin/go/webhooker/internal/globals"
|
|
"sneak.berlin/go/webhooker/internal/handlers"
|
|
"sneak.berlin/go/webhooker/internal/logger"
|
|
"sneak.berlin/go/webhooker/internal/middleware"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
"github.com/go-chi/chi"
|
|
)
|
|
|
|
// ServerParams is a standard fx naming convention for dependency injection
|
|
// nolint:golint
|
|
type ServerParams struct {
|
|
fx.In
|
|
Logger *logger.Logger
|
|
Globals *globals.Globals
|
|
Config *config.Config
|
|
Middleware *middleware.Middleware
|
|
Handlers *handlers.Handlers
|
|
}
|
|
|
|
type Server struct {
|
|
startupTime time.Time
|
|
exitCode int
|
|
sentryEnabled bool
|
|
log *slog.Logger
|
|
ctx context.Context
|
|
cancelFunc context.CancelFunc
|
|
httpServer *http.Server
|
|
router *chi.Mux
|
|
params ServerParams
|
|
mw *middleware.Middleware
|
|
h *handlers.Handlers
|
|
}
|
|
|
|
func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
|
|
s := new(Server)
|
|
s.params = params
|
|
s.mw = params.Middleware
|
|
s.h = params.Handlers
|
|
s.log = params.Logger.Get()
|
|
|
|
lc.Append(fx.Hook{
|
|
OnStart: func(ctx context.Context) error {
|
|
s.startupTime = time.Now()
|
|
go s.Run()
|
|
return nil
|
|
},
|
|
OnStop: func(ctx context.Context) error {
|
|
s.cleanShutdown()
|
|
return nil
|
|
},
|
|
})
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Server) Run() {
|
|
s.configure()
|
|
|
|
// logging before sentry, because sentry logs
|
|
s.enableSentry()
|
|
|
|
s.serve()
|
|
}
|
|
|
|
func (s *Server) enableSentry() {
|
|
s.sentryEnabled = false
|
|
|
|
if s.params.Config.SentryDSN == "" {
|
|
return
|
|
}
|
|
|
|
err := sentry.Init(sentry.ClientOptions{
|
|
Dsn: s.params.Config.SentryDSN,
|
|
Release: fmt.Sprintf("%s-%s", s.params.Globals.Appname, s.params.Globals.Version),
|
|
})
|
|
if err != nil {
|
|
s.log.Error("sentry init failure", "error", err)
|
|
// Don't use fatal since we still want the service to run
|
|
return
|
|
}
|
|
s.log.Info("sentry error reporting activated")
|
|
s.sentryEnabled = true
|
|
}
|
|
|
|
func (s *Server) serve() int {
|
|
s.ctx, s.cancelFunc = context.WithCancel(context.Background())
|
|
|
|
// signal watcher
|
|
go func() {
|
|
c := make(chan os.Signal, 1)
|
|
signal.Ignore(syscall.SIGPIPE)
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
// block and wait for signal
|
|
sig := <-c
|
|
s.log.Info("signal received", "signal", sig.String())
|
|
if s.cancelFunc != nil {
|
|
// cancelling the main context will trigger a clean
|
|
// shutdown via the fx OnStop hook.
|
|
s.cancelFunc()
|
|
}
|
|
}()
|
|
|
|
go s.serveUntilShutdown()
|
|
|
|
<-s.ctx.Done()
|
|
// Shutdown is handled by the fx OnStop hook (cleanShutdown).
|
|
// Do not call cleanShutdown() here to avoid a double invocation.
|
|
return s.exitCode
|
|
}
|
|
|
|
func (s *Server) cleanupForExit() {
|
|
s.log.Info("cleaning up")
|
|
// TODO: close database connections, flush buffers, etc.
|
|
}
|
|
|
|
func (s *Server) cleanShutdown() {
|
|
// initiate clean shutdown
|
|
s.exitCode = 0
|
|
ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer shutdownCancel()
|
|
|
|
if err := s.httpServer.Shutdown(ctxShutdown); err != nil {
|
|
s.log.Error("server clean shutdown failed", "error", err)
|
|
}
|
|
|
|
s.cleanupForExit()
|
|
|
|
if s.sentryEnabled {
|
|
sentry.Flush(2 * time.Second)
|
|
}
|
|
}
|
|
|
|
func (s *Server) MaintenanceMode() bool {
|
|
return s.params.Config.MaintenanceMode
|
|
}
|
|
|
|
func (s *Server) configure() {
|
|
// identify ourselves in the logs
|
|
s.params.Logger.Identify()
|
|
}
|