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:
+18
-16
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user