aboutsummaryrefslogtreecommitdiffstats
path: root/http/server.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--http/server.go146
1 files changed, 146 insertions, 0 deletions
diff --git a/http/server.go b/http/server.go
new file mode 100644
index 0000000..e13bc67
--- /dev/null
+++ b/http/server.go
@@ -0,0 +1,146 @@
+// Package http provides utilities for managing HTTP servers and listeners.
+//
+// It supports opening multiple network listeners from configuration,
+// running multiple HTTP servers concurrently with graceful shutdown,
+// and integrates structured logging with context.
+//
+// This package is designed to be used with context cancellation for concurrency control.
+
+package http
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+ "net"
+ "net/http"
+ "sync"
+ "time"
+
+ "go.sudomsg.com/kit/logging"
+ "golang.org/x/sync/errgroup"
+)
+
+// Listeners is a slice of net.Listener interfaces representing multiple network listeners.
+type Listeners []net.Listener
+
+// CloseAll closes all listeners in the slice.
+// It aggregates all errors returned by individual Close calls using errors.Join.
+//
+// If no errors occur, it returns nil.
+func (ls Listeners) CloseAll() error {
+ var errs []error
+ for _, l := range ls {
+ if err := l.Close(); err != nil {
+ errs = append(errs, err)
+ }
+ }
+ if len(errs) > 0 {
+ return errors.Join(errs...)
+ }
+ return nil
+}
+
+// ServerConfig defines a single network listener configuration.
+//
+// Network is the network type, e.g., "tcp".
+// Address is the socket address, e.g., ":8080".
+type ServerConfig struct {
+ Network string `toml:"network"`
+ Address string `toml:"network"`
+}
+
+// OpenConfigListeners opens network listeners as specified by the provided ServerConfig slice.
+//
+// It attempts to open all listeners concurrently and returns them if all succeed.
+//
+// If any listener fails to open, it closes all previously opened listeners and returns an error.
+//
+// The context controls cancellation of the opening process.
+func OpenConfigListners(ctx context.Context, config []ServerConfig) (Listeners, error) {
+ lns := make(Listeners, 0, len(config))
+ var mu sync.Mutex
+ g, ctx := errgroup.WithContext(ctx)
+ var lc net.ListenConfig
+ for _, cfg := range config {
+ g.Go(func() error {
+ network := cfg.Network
+ if network == "" {
+ network = "tcp"
+ }
+ ln, err := lc.Listen(ctx, network, cfg.Address)
+ if err != nil {
+ return fmt.Errorf("failed to listen on %s %s: %w", network, cfg.Address, err)
+ }
+
+ mu.Lock()
+ lns = append(lns, ln)
+ mu.Unlock()
+ return nil
+ })
+ }
+ if err := g.Wait(); err != nil {
+ _ = lns.CloseAll()
+ return nil, err
+ }
+ return lns, nil
+}
+
+func OpenListeners(ctx context.Context, config []ServerConfig) (Listeners, error) {
+ return OpenConfigListners(ctx, config)
+}
+
+// RunHTTPServers runs HTTP servers concurrently on all provided listeners.
+//
+// The provided handler is used for all servers.
+// Servers respond to context cancellation by performing a graceful shutdown with a timeout of 10 seconds.
+//
+// Logging is performed using the slog.Logger extracted from context.
+// Each server logs startup, shutdown, and errors.
+//
+// This function blocks until all servers have stopped or an error occurs.
+func RunHTTPServers(ctx context.Context, lns Listeners, handler http.Handler) error {
+ g, ctx := errgroup.WithContext(ctx)
+
+ for _, ln := range lns {
+ g.Go(func() error {
+ logger, ctx := logging.With(ctx, "address", ln.Addr)
+
+ srv := &http.Server{
+ Addr: ln.Addr().String(),
+ Handler: handler,
+ BaseContext: func(l net.Listener) context.Context {
+ return ctx
+ },
+ ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
+ }
+
+ logger.Log(ctx, slog.LevelInfo, "HTTP server serving")
+
+ if err := httpServeContext(ctx, srv, ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
+ return fmt.Errorf("HTTP server Serve Error: %w", err)
+ }
+ return nil
+ })
+
+ }
+ return g.Wait()
+}
+
+func httpServeContext(ctx context.Context, srv *http.Server, ln net.Listener) error {
+ logger := logging.FromContext(ctx)
+ go func() {
+ <-ctx.Done()
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ err := srv.Shutdown(ctx)
+ if err != nil {
+ logger.Log(ctx, slog.LevelWarn, "HTTP server Shutdown Error", slog.Any("error", err))
+ } else {
+ logger.Log(ctx, slog.LevelInfo, "HTTP Server Shutdown Complete")
+ }
+ }()
+ return srv.Serve(ln)
+}