144 lines
3.9 KiB
Go
144 lines
3.9 KiB
Go
package httpx
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// Timeouts configures a listener's deadlines.
|
|
//
|
|
// WriteTimeout is deliberately optional and left at zero for the static
|
|
// listener: a global write deadline covers the whole response, so a large file
|
|
// over a slow link gets its connection torn down mid-download even though
|
|
// nothing is wrong. Per-response deadlines belong to the handler, via
|
|
// http.ResponseController.
|
|
type Timeouts struct {
|
|
ReadHeader time.Duration
|
|
Read time.Duration
|
|
Idle time.Duration
|
|
Write time.Duration // 0 = none
|
|
}
|
|
|
|
// Server is an http.Server whose listener is already bound.
|
|
//
|
|
// Binding at construction time means a port conflict is reported before any
|
|
// background work starts, and it lets a caller pass ":0" and read back the
|
|
// chosen address — which is what the integration tests do.
|
|
type Server struct {
|
|
Name string
|
|
|
|
srv *http.Server
|
|
ln net.Listener
|
|
log *slog.Logger
|
|
}
|
|
|
|
// Listen binds addr and prepares a server for h.
|
|
func Listen(name, addr string, h http.Handler, t Timeouts, log *slog.Logger) (*Server, error) {
|
|
ln, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s := &Server{
|
|
Name: name,
|
|
ln: ln,
|
|
log: log,
|
|
srv: &http.Server{
|
|
Handler: h,
|
|
ReadHeaderTimeout: t.ReadHeader,
|
|
ReadTimeout: t.Read,
|
|
IdleTimeout: t.Idle,
|
|
WriteTimeout: t.Write,
|
|
// Route net/http's own errors (malformed requests, TLS handshake
|
|
// failures) into the structured log rather than bare stderr.
|
|
ErrorLog: slog.NewLogLogger(log.With("listener", name).Handler(), slog.LevelWarn),
|
|
},
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// Addr is the address actually bound, which differs from the requested one when
|
|
// port 0 was asked for.
|
|
func (s *Server) Addr() string { return s.ln.Addr().String() }
|
|
|
|
// Serve blocks until the server stops. It returns nil on a graceful shutdown.
|
|
func (s *Server) Serve() error {
|
|
err := s.srv.Serve(s.ln)
|
|
if errors.Is(err, http.ErrServerClosed) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Shutdown stops accepting connections and waits for in-flight requests, up to
|
|
// ctx's deadline. Past the deadline the remaining connections are closed.
|
|
func (s *Server) Shutdown(ctx context.Context) error {
|
|
err := s.srv.Shutdown(ctx)
|
|
if err != nil {
|
|
// Shutdown only fails by running out of time; Close is then the only way
|
|
// to release the port.
|
|
s.log.Warn("graceful shutdown timed out, closing connections",
|
|
"listener", s.Name, "err", err)
|
|
return s.srv.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Group runs several servers with a shared lifetime: if one fails, all stop.
|
|
type Group struct {
|
|
Servers []*Server
|
|
Grace time.Duration
|
|
Log *slog.Logger
|
|
}
|
|
|
|
// Run serves until ctx is cancelled or a server fails, then shuts every server
|
|
// down within Grace. It returns the first non-nil error.
|
|
func (g *Group) Run(ctx context.Context) error {
|
|
errs := make(chan error, len(g.Servers))
|
|
for _, s := range g.Servers {
|
|
go func() {
|
|
g.Log.Info("listening", "listener", s.Name, "addr", s.Addr())
|
|
errs <- s.Serve()
|
|
}()
|
|
}
|
|
|
|
var first error
|
|
done := 0
|
|
select {
|
|
case <-ctx.Done():
|
|
g.Log.Info("shutting down", "grace", g.Grace)
|
|
case err := <-errs:
|
|
done++
|
|
first = err
|
|
if err != nil {
|
|
g.Log.Error("listener failed, stopping", "err", err)
|
|
}
|
|
}
|
|
|
|
// The shutdown deadline must survive the cancellation that triggered it,
|
|
// otherwise ctx.Done() would make Shutdown return immediately.
|
|
shutCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), g.Grace)
|
|
defer cancel()
|
|
for _, s := range g.Servers {
|
|
if err := s.Shutdown(shutCtx); err != nil && first == nil {
|
|
first = err
|
|
}
|
|
}
|
|
|
|
// Drain the remaining Serve results so no goroutine is left blocked on send.
|
|
for ; done < len(g.Servers); done++ {
|
|
select {
|
|
case err := <-errs:
|
|
if err != nil && first == nil {
|
|
first = err
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
return first
|
|
}
|
|
}
|
|
return first
|
|
}
|