feat(service): implement graceful shutdown for HTTP server

Add a context with timeout to handle graceful shutdown of the HTTP 
server. Update error handling during the server's close to include 
context-aware shutdown. Ensure that the server properly logs only 
non-closed errors when listening.
This commit is contained in:
2025-04-12 10:43:40 +02:00
parent 8e02bfb0a2
commit aaa111dd20
5 changed files with 124 additions and 22 deletions
+18 -16
View File
@@ -2,7 +2,9 @@ package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
@@ -17,8 +19,6 @@ import (
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/alecthomas/kong"
"github.com/apex/log"
"github.com/apex/log/handlers/json"
"github.com/getsentry/sentry-go"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/rs/cors"
@@ -32,6 +32,7 @@ import (
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/graph"
"gitlab.com/unboundsoftware/schemas/graph/generated"
"gitlab.com/unboundsoftware/schemas/logging"
"gitlab.com/unboundsoftware/schemas/middleware"
"gitlab.com/unboundsoftware/schemas/store"
)
@@ -59,9 +60,7 @@ const serviceName = "schemas"
func main() {
var cli CLI
_ = kong.Parse(&cli)
log.SetHandler(json.New(os.Stdout))
log.SetLevelFromString(cli.LogLevel)
logger := log.WithField("service", serviceName)
logger := logging.SetupLogger(cli.LogLevel, serviceName, buildVersion)
closeEvents := make(chan error)
if err := start(
@@ -70,11 +69,11 @@ func main() {
ConnectAMQP,
cli,
); err != nil {
logger.WithError(err).Error("process error")
logger.With("error", err).Error("process error")
}
}
func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url string) (Connection, error), cli CLI) error {
func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(url string) (Connection, error), cli CLI) error {
if err := setupSentry(logger, cli.SentryConfig); err != nil {
return err
}
@@ -123,7 +122,7 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
return fmt.Errorf("caching subgraphs: %w", err)
}
setups := []goamqp.Setup{
goamqp.UseLogger(logger.Error),
goamqp.UseLogger(func(s string) { logger.Error(s) }),
goamqp.CloseListener(closeEvents),
goamqp.WithPrefetchLimit(20),
goamqp.EventStreamPublisher(publisher),
@@ -169,7 +168,7 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
defer wg.Done()
err := <-closeEvents
if err != nil {
logger.WithError(err).Error("received close from AMQP")
logger.With("error", err).Error("received close from AMQP")
rootCancel()
}
}()
@@ -179,8 +178,11 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
defer wg.Done()
<-rootCtx.Done()
if err := httpSrv.Close(); err != nil {
logger.WithError(err).Error("close http server")
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := httpSrv.Shutdown(shutdownCtx); err != nil {
logger.With("error", err).Error("close http server")
}
close(sigint)
close(closeEvents)
@@ -235,10 +237,10 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
),
))
logger.Infof("connect to http://localhost:%d/ for GraphQL playground", cli.Port)
logger.Info(fmt.Sprintf("connect to http://localhost:%d/ for GraphQL playground", cli.Port))
if err := httpSrv.ListenAndServe(); err != nil {
logger.WithError(err).Error("listen http")
if err := httpSrv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
logger.With("error", err).Error("listen http")
}
}()
@@ -287,7 +289,7 @@ func healthFunc(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("OK"))
}
func setupSentry(logger log.Interface, args SentryConfig) error {
func setupSentry(logger *slog.Logger, args SentryConfig) error {
if args.Environment == "" {
return fmt.Errorf("no Sentry environment supplied, exiting")
}
@@ -315,7 +317,7 @@ func setupSentry(logger log.Interface, args SentryConfig) error {
if err := sentry.Init(cfg); err != nil {
return fmt.Errorf("sentry setup: %w", err)
}
logger.Infof("configured Sentry for env: %s", args.Environment)
logger.With("environment", args.Environment).Info("configured Sentry")
return nil
}