Fail startup when the HTTP listener cannot bind (closes #167)
check / check (push) Waiting to run
check / check (push) Waiting to run
The HTTP server ran ListenAndServe in a goroutine, so a failed bind was only logged and neoircd kept running without its HTTP API. The start hook now binds the listen address and returns the bind error, so neoircd exits 1 with an error naming the address. If serving the bound listener later ends with anything other than a normal shutdown, the server stops the app through fx.Shutdowner with exit code 1; the handler tests now pass it that Shutdowner. Tests check that a taken port fails startup, that a serve failure stops the app with exit code 1, and that a normal stop does not. The server tests no longer run in parallel: building the app writes the global viper instance. Model: opus-5-5
This commit was merged in pull request #169.
This commit is contained in:
+36
-14
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -39,6 +40,7 @@ type Params struct {
|
||||
Config *config.Config
|
||||
Middleware *middleware.Middleware
|
||||
Handlers *handlers.Handlers
|
||||
Shutdowner fx.Shutdowner
|
||||
}
|
||||
|
||||
// Server is the main HTTP server.
|
||||
@@ -66,7 +68,7 @@ func New(
|
||||
}
|
||||
|
||||
lifecycle.Append(fx.Hook{
|
||||
OnStart: func(_ context.Context) error {
|
||||
OnStart: func(ctx context.Context) error {
|
||||
srv.startupTime = time.Now()
|
||||
|
||||
// Build the router before OnStart returns, so srv can
|
||||
@@ -78,9 +80,7 @@ func New(
|
||||
// A closed stdout or stderr must not end the process.
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
|
||||
srv.startHTTP()
|
||||
|
||||
return nil
|
||||
return srv.startHTTP(ctx)
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
srv.cleanShutdown(ctx)
|
||||
@@ -162,15 +162,23 @@ func (srv *Server) configure() {
|
||||
// Server configuration placeholder.
|
||||
}
|
||||
|
||||
// startHTTP creates the HTTP server and serves it from a new
|
||||
// goroutine. srv.httpServer is set before that goroutine
|
||||
// starts and never changes, so it and cleanShutdown read it
|
||||
// without a lock.
|
||||
func (srv *Server) startHTTP() {
|
||||
// startHTTP binds the HTTP listen address, so that a port
|
||||
// that is taken fails startup, then serves the HTTP server
|
||||
// from a new goroutine. srv.httpServer is set before that
|
||||
// goroutine starts and never changes, so it and cleanShutdown
|
||||
// read it without a lock.
|
||||
func (srv *Server) startHTTP(ctx context.Context) error {
|
||||
listenAddr := fmt.Sprintf(
|
||||
":%d", srv.params.Config.Port,
|
||||
)
|
||||
|
||||
var listenConfig net.ListenConfig
|
||||
|
||||
listener, err := listenConfig.Listen(ctx, "tcp", listenAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http listen: %w", err)
|
||||
}
|
||||
|
||||
srv.httpServer = &http.Server{ //nolint:exhaustruct // optional fields
|
||||
Addr: listenAddr,
|
||||
ReadTimeout: httpReadTimeout,
|
||||
@@ -179,16 +187,30 @@ func (srv *Server) startHTTP() {
|
||||
Handler: srv,
|
||||
}
|
||||
|
||||
go srv.serveUntilShutdown()
|
||||
go func() {
|
||||
srv.serveUntilShutdown(listener)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *Server) serveUntilShutdown() {
|
||||
// serveUntilShutdown serves the HTTP server on listener until
|
||||
// cleanShutdown stops it. If serving fails before then, it
|
||||
// stops the app with exit code 1.
|
||||
func (srv *Server) serveUntilShutdown(listener net.Listener) {
|
||||
srv.log.Info(
|
||||
"http begin listen", "listenaddr", srv.httpServer.Addr,
|
||||
)
|
||||
|
||||
err := srv.httpServer.ListenAndServe()
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
srv.log.Error("listen error", "error", err)
|
||||
err := srv.httpServer.Serve(listener)
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
srv.log.Error("listen error", "error", err)
|
||||
|
||||
err = srv.params.Shutdowner.Shutdown(fx.ExitCode(1))
|
||||
if err != nil {
|
||||
srv.log.Error("shutdown failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user