// Package server is documented in router.go. server.go owns the HTTP listener // lifecycle: it binds the configured TCP listener, serves the supplied // http.Handler, and shuts down within the configured budget. package server import ( "context" "errors" "fmt" "net" "net/http" "sync" "time" "galaxy/backend/internal/config" "go.uber.org/zap" ) // Server owns the HTTP listener exposed by the backend. type Server struct { cfg config.HTTPConfig handler http.Handler logger *zap.Logger stateMu sync.RWMutex server *http.Server listener net.Listener } // NewServer constructs an HTTP server bound to cfg. handler is the prebuilt // http.Handler returned by NewRouter. A nil logger is replaced with zap.NewNop. func NewServer(cfg config.HTTPConfig, handler http.Handler, logger *zap.Logger) *Server { if logger == nil { logger = zap.NewNop() } if handler == nil { handler = http.NotFoundHandler() } return &Server{ cfg: cfg, handler: handler, logger: logger.Named("http"), } } // Run binds the listener and serves requests until Shutdown closes the server. func (s *Server) Run(ctx context.Context) error { if ctx == nil { return errors.New("run backend HTTP server: nil context") } if err := ctx.Err(); err != nil { return err } listener, err := net.Listen("tcp", s.cfg.Addr) if err != nil { return fmt.Errorf("run backend HTTP server: listen on %q: %w", s.cfg.Addr, err) } server := &http.Server{ Handler: s.handler, ReadTimeout: s.cfg.ReadTimeout, WriteTimeout: s.cfg.WriteTimeout, IdleTimeout: s.cfg.ReadTimeout, } s.stateMu.Lock() s.server = server s.listener = listener s.stateMu.Unlock() s.logger.Info("backend HTTP server started", zap.String("addr", listener.Addr().String())) defer func() { s.stateMu.Lock() s.server = nil s.listener = nil s.stateMu.Unlock() }() err = server.Serve(listener) switch { case err == nil: return nil case errors.Is(err, http.ErrServerClosed): s.logger.Info("backend HTTP server stopped") return nil default: return fmt.Errorf("run backend HTTP server: serve on %q: %w", s.cfg.Addr, err) } } // Shutdown gracefully stops the HTTP server within ctx, applying the // configured per-listener shutdown timeout when it is shorter. func (s *Server) Shutdown(ctx context.Context) error { if ctx == nil { return errors.New("shutdown backend HTTP server: nil context") } s.stateMu.RLock() server := s.server s.stateMu.RUnlock() if server == nil { return nil } shutdownCtx, cancel := boundedContext(ctx, s.cfg.ShutdownTimeout) defer cancel() if err := server.Shutdown(shutdownCtx); err != nil && !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("shutdown backend HTTP server: %w", err) } return nil } func boundedContext(parent context.Context, limit time.Duration) (context.Context, context.CancelFunc) { if limit <= 0 { return context.WithCancel(parent) } return context.WithTimeout(parent, limit) }