package httpx import ( "context" "errors" "fmt" "html/template" "log/slog" "net/http" "time" "bountyboard/internal/auth" "bountyboard/internal/config" "bountyboard/internal/files" "bountyboard/internal/metrics" "bountyboard/internal/store" ) // shutdownBudget is the ยง12 graceful-shutdown drain window. const shutdownBudget = 15 * time.Second type Server struct { cfg *config.Config log *slog.Logger metrics *metrics.Registry store *store.Store files *files.Store templates map[string]*template.Template oidc *auth.OIDCClient loginLimiter *auth.RateLimiter checks []ReadinessCheck startedAt time.Time httpSrv *http.Server // late-bound subsystem hooks (see providers.go) atomizer BreakerInfo performer BreakerInfo syncProvider StatusProvider jobsProvider StatusProvider syncTrigger func(customerID string) } func New(cfg *config.Config, log *slog.Logger, reg *metrics.Registry, st *store.Store, fs *files.Store) *Server { templates, err := parseTemplates() if err != nil { // Templates are embedded; failure is a build defect caught by any // test or first boot, never a runtime condition. panic(fmt.Sprintf("parse templates: %v", err)) } s := &Server{ cfg: cfg, log: log, metrics: reg, store: st, files: fs, templates: templates, oidc: auth.NewOIDCClient(cfg, log), loginLimiter: auth.NewRateLimiter(10, 15*time.Minute), startedAt: time.Now(), } mux := http.NewServeMux() mux.HandleFunc("GET /healthz", s.handleHealthz) mux.HandleFunc("GET /readyz", s.handleReadyz) mux.HandleFunc("GET /metricsz", s.handleMetricsz) s.routesAuth(mux) s.routesProfile(mux) s.routesAdmin(mux) s.routesWeb(mux) s.httpSrv = &http.Server{ Addr: fmt.Sprintf(":%d", cfg.AppPort), Handler: s.withMiddleware(mux), ReadHeaderTimeout: 10 * time.Second, ErrorLog: slog.NewLogLogger(log.Handler(), slog.LevelWarn), } return s } // Handler exposes the full middleware-wrapped handler for tests. func (s *Server) Handler() http.Handler { return s.httpSrv.Handler } // Run serves until ctx is canceled (SIGTERM/SIGINT), then drains in-flight // requests within the shutdown budget. func (s *Server) Run(ctx context.Context) error { errCh := make(chan error, 1) go func() { s.log.Info("http server listening", "addr", s.httpSrv.Addr, "baseUrl", s.cfg.AppBaseURL) if err := s.httpSrv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { errCh <- err return } errCh <- nil }() select { case err := <-errCh: return fmt.Errorf("http server: %w", err) case <-ctx.Done(): } s.log.Info("shutting down", "budget", shutdownBudget.String()) shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownBudget) defer cancel() if err := s.httpSrv.Shutdown(shutdownCtx); err != nil { return fmt.Errorf("graceful shutdown: %w", err) } return <-errCh }